Jump to content

Use CSRF in your own forms.


Harmster

Recommended Posts

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.

  • Like 34
Link to comment
Share on other sites

  • 2 months later...

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.

Link to comment
Share on other sites

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.
            }
 
  • Like 5
Link to comment
Share on other sites

  • 3 weeks later...
  • 8 months later...
  • 1 month later...

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 :biggrin:

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 by Can
  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

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?

Link to comment
Share on other sites

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 :)

Link to comment
Share on other sites

  • 1 year later...

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

Link to comment
Share on other sites

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();

  • Like 2
Link to comment
Share on other sites

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

Link to comment
Share on other sites

  • 3 months later...

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

post-2272-0-59904000-1453421460_thumb.pn

Link to comment
Share on other sites

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 by kongondo
Link to comment
Share on other sites

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

Link to comment
Share on other sites

  • 4 weeks later...

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.

Link to comment
Share on other sites

  • 1 year later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...