Harmster 98 Posted June 6, 2013 Hey, The Form API has CSRF protection build in, but if you for some reason don't want to use the API you can however use the CSRF protection. Its very simple but it took some time for me to find out, so i figured i share my findings with the rest. What is CSRF? First you need to create a token and a token name you do that as following: $tokenName = $this->session->CSRF->getTokenName(); $tokenValue = $this->session->CSRF->getTokenValue(); Very simple. Now what you want to do is create a hidden input field like this: $html .= '<input type="hidden" id="_post_token" name="' . $tokenName . '" value="' . $tokenValue . '"/>'; Now this will generate something that will look like this: <input type="hidden" id="_post_token" name="TOKEN1470842875" value="fe8ce9c1b9e6b9e361830df3525c49317a35332fbf626aa8793777a3b705824a"> You are done on the form side. You can now go to the part where you are receiving the post. Then use: $session->CSRF->validate(); This will return true (1) on a valid request and an exception on a bad request. You can test this out to open up your Firebug/Chrome debug console and change the value of the textbox to something else. Basicly what this does is set a session variable with a name (getTokenName) and gives it a hashed value. If a request has a token in it it has to have the same value or it is not send from the correct form. Well I hope I helped someone. 33 Quote Share this post Link to post Share on other sites
arjen 1,368 Posted June 6, 2013 Thanks for sharing, harmster! Quote Share this post Link to post Share on other sites
Valery 87 Posted September 5, 2013 Hi harmster, Thanks for the nice tip. However, I get "Error: Exception: This request was aborted because it appears to be forged. (in /home/.../wire/core/SessionCSRF.php line 80)" in errors.log when I try to spoof the token. Is there any way to catch this exception? Thanks. Quote Share this post Link to post Share on other sites
ryan 16,710 Posted September 7, 2013 You can always use a try { ... } catch(Exception $e) { ... } but I think it's probably better to leave that as a fatal condition and stop processing. 1 Quote Share this post Link to post Share on other sites
Valery 87 Posted September 8, 2013 Thank you, Ryan. My intent was to implement some sort of security auditing such that I would receive a message whenever a CSRF was attempted. So I needed to catch the exception. The following code works for me. try { $form->processInput($input->post); $session->CSRF->validate(); } catch (WireCSRFException $e) { echo "Processing aborted. Suspected attempt to forge the submission. IP logged." . $e->getMessage(); /* * Some code to execute whenever the token gets spoofed * Like sending a notification mail with the spoofer's IP address */ die(); // live a great life and die() gracefully. } 5 Quote Share this post Link to post Share on other sites
valan 89 Posted September 24, 2013 Thanks for hint! Is it possible to handle several forms at one page using these CSRF protection methods? I've used code above for two forms and got identical token names and values... Thanks! Quote Share this post Link to post Share on other sites
Harmster 98 Posted September 24, 2013 Yeah, that shouldn't matter, the token is there to prevent others from sending their forms to your backend. Having 2 the same tokens is OK. The tokens aren't there to indentify your form. Source: http://stackoverflow.com/questions/14715250/generating-csrf-tokens-for-multiple-forms-on-a-single-page Quote Share this post Link to post Share on other sites
valan 89 Posted September 25, 2013 Yeah, got it. Thanks! Quote Share this post Link to post Share on other sites
Hari KT 83 Posted June 20, 2014 Awesome, and thanks for sharing. Quote Share this post Link to post Share on other sites
Can 275 Posted July 25, 2014 (edited) That's great, thank you! Just changed the value via Chrome Dev-Tools as you mentioned to see what happens I get a white screen with this one "Unable to complete this request due to an error. Error has been logged." <?php public function validate() { if(!$this->config->protectCSRF) return true; if($this->hasValidToken()) return true; $this->resetToken(); throw new WireException($this->_('This request was aborted because it appears to be forged.')); } Aaaaha, just got it working. used the hasValidToken() function instead. In my login form it looks like this now <?php if($input->post->username && $input->post->pass && $session->CSRF->hasValidToken() == true) So for anyone struggling like me ;-) You can then of course check easily for invalid tokens ( hasValidToken() == false ) and throw custom errors in this case if you like. cheers Update: Looks like I'm doing something wrong. Even after I noticed $session->CSRF->resetToken(); But I'm sure I'll find it out Update 2: Thanks to Valery I think I got it now. got rid of the hasValidToken() == true thing an am using try and catch now <?php try { $session->CSRF->validate(); //SWIFTMailer stuff to send the mail } catch (WireCSRFException $e) { $error = "Seems to be a resubmission."; } Edited July 26, 2014 by Can 1 Quote Share this post Link to post Share on other sites
Valery 87 Posted August 21, 2014 Hi fellow ProcessWire devs, Recently I had a revelation which was: CSRF session IDs do expire in ProcessWire with time (though I do not yet know how soon it is). So, for instance, if you leave a form unsubmitted for some time and then try to post it, ProcessWire will through a CSRF validation error. I gave this a little bit of thought and came up with some code like this: public function processForm ($form) { try { $form->processInput(wire('input')->post); wire('session')->CSRF->validate(); } catch (WireCSRFException $e) { die($this->displayForm($form)); // $this->displayForm($form) is a method which returns $form->render() } } I run a few public forms for comments and orders, and I do not want to piss off my guests and clients when they have to re-enter form data again. If their CSRF session runs out, I will send them their form back with their data filled in. What do you think of this approach? How likely is that a CSRF session expiry occurs? Should I be harsh when a CSRF validation fails, or should I give them a second try? Quote Share this post Link to post Share on other sites
adrian 11,177 Posted August 21, 2014 Are you running 2.4 stable? Does this dev commit fix the issue for you: https://github.com/ryancramerdesign/ProcessWire/commit/c18cf1e62eaafb3df1071a8c0936e5346721f754 Quote Share this post Link to post Share on other sites
Valery 87 Posted August 21, 2014 Thanks, adrian. Cannot really say for sure if this commit addresses my issue but I found that the CSRF challenge cookie lasts a whopping 30 days. // set challenge cookie to last 30 days (should be longer than any session would feasibly last) setcookie(session_name() . '_challenge', $challenge, time()+60*60*24*30, '/', null, false, true); Now that's really more than enough time to think a form submission over and over a few hundred times Quote Share this post Link to post Share on other sites
Juergen 472 Posted September 30, 2015 Hello @ all, today I have tried to implement the CSRF validation for the first time on a contact form. I have implemented the tokens to the form in a hidden field. So far so good, but the validation shows always "1" after sucessfull or failed submission. You can now go to the part where you are receiving the post. Then use: $session->CSRF->validate();This will return true (1) on a valid request and an exception on a bad request. You can test this out to open up your Firebug/Chrome debug console and change the value of the textbox to something else. I have a custom form with Bootstrap 3 Markup and this is the code that runs after there are no errors in the form: if($session->CSRF->validate() == "1"){ //send the data and create the success message $thankyou = __("danke nachricht"); $mailsenttext = __("Nachricht erhalten"); $out = "<div class='alert alert-success' role='alert'><span class='fa fa-check fa-3x pull-left fa-border'></span><h4>$thankyou</h4><p>$mailsenttext</p><div class='clearfix'></div></div>"; //create the email $fullname = $firstname . ' ' . $lastname; $userip = $_SERVER['REMOTE_ADDR']; $mail = new PHPMailer(); $mail->IsHTML(true); $registertext = __("Mailheader Contactform"); $senderinfo = __("Absender Label"); $mail->From = $email; $mail->FromName = $fullname; $mail->AddAddress($emailTo); $mail->AddReplyTo($email, $fullname); $mail->Subject = $subject; $mail->Body .= "<p><b>$subjectlabel:</b> $subject</p><p><b>$messagelabel</b><br />$comments</p><p><b>$senderinfo</b><br />$gendername $firstname $lastname<br />$email<br />IP: $userip</p>"; $mail->send(); $form = ""; } else { //dont send data and create an error message $out = "<div class='alert alert-danger' role='alert'><span class='fa fa-warning fa-3x pull-left fa-border'></span><h4>$warning</h4><p>$doublesubmissiontext</p><div class='clearfix'></div></div>"; } Unfortunately the value of the $session->CSRF->validate() is always "1", even after successfull submission, so the form could be sent multiple times. What I am doing wrong? Best regards Jürgen Quote Share this post Link to post Share on other sites
LostKobrakai 5,002 Posted September 30, 2015 CSRF is not there to protect your users from multiple submissions. It's there to ensure that the data send are from the exact user you send the form to, nothing more. If you additionally want to prevent multiple submissions, than use $session->CSRF->resetToken(); on successful submissions. But this would prevent other forms, that the users may already have requested on e.g. another tab, from working. There are other ways to prevent duplicate form submissions, like using redirects. E.g. one way with redirects would be: /my-page-with-form/ -> submits to "submit/" /my-page-with-form/submit/ -> on success redirect back, on error save submitted data, too, to repopulate form …, but you could also simply redirect to the same page in your current code just after $mail->send(); 2 Quote Share this post Link to post Share on other sites
Juergen 472 Posted September 30, 2015 Thanks, I have misunterstood something completely, so I will redirect after $mail->send(); Best regards Quote Share this post Link to post Share on other sites
LostKobrakai 5,002 Posted September 30, 2015 But keep in mind that this will only solve the issue of people hitting refresh after an successful submission (and redirect). I doesn't help if someone is hitting refresh while submitting the form, which will still send two requests. 1 Quote Share this post Link to post Share on other sites
Juergen 472 Posted October 1, 2015 Thanks LostKobrakai, now I solved it with a session id in an hidden input field that will be compared with the post value of the hidden field after submission. If session value and post value are same then send the form data and remove the session id. If someone hits the F5 button after submission, the valid session id is no more longer available (because it was removed after submission) and so the values dont match any longer. As a result a hint for "double submission" appear on the screen instead of submitting the form. The reason why I missunderstood the CSRF was a post by Soma in another topic where he uses CSRF to prevent double submissions. https://processwire.com/talk/topic/3633-prevent-form-resubmission/?p=35567 Best regards Quote Share this post Link to post Share on other sites
adrianmak 62 Posted January 22, 2016 I added form CSRF in my search form <form method='post' action='<?php echo $config->urls->root; ?>search/'> <input type='hidden' id='_post_token' name='<?php $this->session->CSRF->getTokenName(); ?>' value='<?php $this->session->CSRF->getTokenValue(); ?>' /> <input class='search__input' type='text' name='search_keywords' id='search_keywords' value='' /> <select class='search__select' id='type' name='type'> <option value=''>All</option> <?php foreach($pages->get(1028)->children() as $category) { if($t==$category) $selected = "selected"; else $selected = ""; echo "<option value='{$category->id}' {$selected}>{$category->title}</option>"; } ?> </select> <!--<input type='text' name='typeoptions' id='typeoptions'>--> <span class='search_options'></span> <input class='search__button' type='submit' value='Search' /> </form> But, the outputted two values of hidden field are empty Quote Share this post Link to post Share on other sites
kongondo 6,722 Posted January 22, 2016 (edited) Looks like you are in a template file rather than a module. In that case, PHP does not know what $this is. Turn debug mode on to catch such errors...Change code as follows: $session->CSRF->getTokenName(); $session->CSRF->getTokenValue(); Edit: ..and you also need to echo out your token name and values Edited January 28, 2016 by kongondo Quote Share this post Link to post Share on other sites
adrianmak 62 Posted January 22, 2016 Looks like you are in a template file rather than a module. In that case, PHP does not know what $this is. Turn debug mode on to catch such errors...Change code as follows: $session->CSRF->getTokenName(); $session->CSRF->getTokenValue(); now works! Another problem I found immediately, I opened firbug to change textfield value or a select option value, it still success to process Did I test CSRF the right way? search.php try { $q = $sanitizer->text($input->post->search_keywords); $t = $sanitizer->text($input->post->type); $o = $sanitizer->text($input->post->typeoptions); $session->CSRF->validate(); } catch(WireCSRFException $e) { echo "Processing aborted."; die(); } // if success, continue to process form data Quote Share this post Link to post Share on other sites
Manol 117 Posted February 19, 2016 How would I use CSRF with ajax in a page that's located in a different server. I would like to use it with phonegap/cordova, make and ajax(angular) call from the phone to get some data back. Quote Share this post Link to post Share on other sites
Manol 117 Posted February 19, 2016 I've in my php file <?php header('Access-Control-Allow-Origin: *'); /* Allowed request method */ header("Access-Control-Allow-Methods: PUT"); /* Allowed custom header */ header("Access-Control-Allow-Headers: Content-Type"); when I make calls with ajax(angular), it works this.login = function (creds) { var deferred = $q.defer(); $http({ method: 'POST', url: 'http://miwebpage.com/api/login/', data: {'user': creds.username, 'pass': creds.password} }) .success(function (result) { deferred.resolve(result); }) .error(function(data){ deferred.reject; }); return deferred.promise; } I would like to use a token in every call (csrf?), how can I achieve that in ajax ( angular ), any tutorials? I've searched but not sure yet how to do it with pw and csrf. Quote Share this post Link to post Share on other sites
Chris 29 Posted February 22, 2016 Thanks a lot for this hint! Quote Share this post Link to post Share on other sites
adrianmak 62 Posted May 18, 2017 How could i use different token name and value on different form on the same page ? like Quote Share this post Link to post Share on other sites