Link to home
Start Free TrialLog in
Avatar of Crazy Horse
Crazy HorseFlag for South Africa

asked on

CSRF form validation fires even when form token matches session variable token - MVC

I have a function which generates a value for the form token. That is in a hidden field in the view:

<form action="<?php echo URLROOT; ?>/users/" method="post">
	<input class="form-email" type="email" placeholder="Email Address" name="email" autocomplete="off" value="<?php echo $data['email']; ?>">
	<input class="form-password" type="password" placeholder="Password" name="password">
	<input class="login-btn btn-filled" id="login" type="submit" value="Login">
	<input type="hidden" name="form_token" id="form_token" value="<?php echo make_form_token(); ?>">
</form>

Open in new window



In my controller I have:

public function index() 
		
	{
	
		if($_SERVER['REQUEST_METHOD'] == 'POST') {
			
			$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
			$password = trim($_POST['password']);
		
				$data = [
				
					'email' => $email,
					'password' => $password,
					'message' => ''
					
				];
			
				if(!isset($_SESSION['token']) || $_POST['form_token'] !== $_SESSION['token']) {
			
					$data['message'] .= "CSRF token invalid <br />";
				}

                              // other form validation here
                                
                           if(!empty($data['message'])) {
			
					$this->view('users/index', $data);
				
				} else {

// success

Open in new window


if I echo out the session variable and view the page source, I can see that the hidden variable value for token is exactly the same as the session variable value. Why then do I keep getting my error message that says the CSRF token is invalid?

The strange thing is that I do exactly the same thing in another area of my website and it works just fine. Very confused...

I also know that the session is set because if I change the validation to just:

if(!isset($_SESSION['token'])) {

Open in new window


then the error goes away. So, it is something to do with the hidden field form token.
Avatar of gr8gonzo
gr8gonzo
Flag of United States of America image

Well, !== checks for both the value AND the type. So if your form token is numeric (e.g. 12345) when it's generated and saved to your session, you might have an INTEGER in your session and a STRING in your post:

POST = "12345"
SESSION = 12345

If that's the case, then !== would return true because of the different data types. A quick way to test this is to just change !== to != (remove one of the equals signs) and see if it works at that point.

I'd also suggest doing a var_export() in your invalid message:

                        if(!isset($_SESSION['token']) || $_POST['form_token'] !== $_SESSION['token']) {
                  
                              $data['message'] .= "CSRF token invalid: " . var_export($_POST["form_token"],true) . " !== " . var_export($_SESSION["token"],true) . "<br />";
                        }

Then re-test and see what the error message says.

On a side note, be mindful of what would happen if your users want to open up multiple tabs or use the back button on their browser. If you're only storing one valid token in your session and if you're overwriting it each time a new token is generated, then you're likely going to end up with quite a few "invalid token" messages that will frustrate your users. So make sure that you're first thinking through your workflow as if you were an average user and attempting to use your application the way you would normally use any other web application. What happens when you hit the Back button on different pages? What happens if you refresh on a screen that processes a form POST? What happens if someone right-clicks on a link and chooses to open it in a new tab so they have two tabs open at the same time for your application?
Avatar of Crazy Horse

ASKER

Hi there. I changed from !== to != and still get the same error. After running your code, I get:

CSRF token invalid: 'ozzYHKIBsClRase0yQ+Fi9JxnRAQdAk8L07M62Hy7ng=' != '/GAwFb6QwNgAZuyHAV08E0KoV5+0W7VJDAQlnNCg3SA='

What would you suggest to remedy the scenarios you put to me? This is pretty much how I have been doing it for a while now and thought it was the "right" way to do it. Clearly not! Haha.
The function which creates the session token is:

function make_form_token() {
	
		$token = base64_encode(openssl_random_pseudo_bytes(32));
		$_SESSION['token'] = $token;
		return $token;
	
} 

Open in new window

Another thing I noticed. Underneath the form I have a link to another page (forgot password). On the forgot password page I have a link back to login. If I start on the login page and then click on the link to go to the forgot password page, and then click on the link again to go back to login and submit the form. The tokens match. If I click submit again then they don't match. That is odd behavior.
I figured it out but I don't know why it broke it.

<form action="<?php echo URLROOT; ?>/users/" id="loginForm" method="post"> 

Open in new window


had a slash after /users/ but if I remove it so it is /users without the trailing slash then it works just fine.
Well, that was short lived. The reset password page also gives me the CSRF error now.
ASKER CERTIFIED SOLUTION
Avatar of gr8gonzo
gr8gonzo
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
My guess is that your make_form_token() function is being called more times than you expect.

Indeed. I am still not 100% sure but in my index method, I put die() right at the beginning of it so that the index method never actually runs. This issue goes away after doing this. If I let the index method run as normal, the error appears again. So for some reason, the token is being generated twice. I didn't find the exact solution but all I did was rename the index method to login.

So, my url went from mysite.com/users to mysite.com/users/login and everything seems to work properly now.

I am keen to use your 'quick fix' in production. Would that be okay?
Sure - it will simply have a little bit more overhead but if you can identify the root cause then you can eliminate most of that overhead.
Thanks!