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

asked on

CSRF session variables

I know I have touched on this already and have had some good answers as well as been advised not to use this method but it is bothering me because I still don't fully understand. I have read a lot today and looked at a lot of tutorials that are similar but I still have some questions.

I have 2 versions of code and not sure which one is best to continue with:

if(empty($_SESSION['csrf_token'])){
	
	$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}


if(!empty($_POST['csrf_token'])) {
	
	if($_SESSION['csrf_token'] === $_POST['csrf_token']){
		
		echo "token verified";
		
	} else {
		
		echo "token invalid";
	}
}

Open in new window


AND

if($_SERVER['REQUEST_METHOD'] === 'POST'){
	if(!isset($_POST['csrf_token']) || ($_POST['csrf_token'] !== $_SESSION['csrf_token'])){
		die("Invalid CSRF token");
		
	} else {
		
		echo "token is valid";
	}
}


$_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes(16));

Open in new window


I also have a hidden field in a form:

<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>" />

Open in new window


1. One of the versions changes the token every time you refresh the page. is that good or bad?

2. If I have 3 different pages and on each of those pages there is a different page, could I just copy and paste the code above or would that cause issues?

3. Do I need to make the session variables expire after a certain amount of time or should I unset them at a certain point of time?

Thank you
Avatar of Ray Paseur
Ray Paseur
Flag of United States of America image

I would change the token every time you refresh the page.  Prepare the token for the form, making a new one each time you display a new form.

Different "pages" is a complex question.  It goes to the nature of the HTTP client/server process.  Pages do not really exist - only requests and responses.  Read this over, and think about what a page is -- it's a response, and it can only exist in response to one request.
https://www.experts-exchange.com/articles/11271/Understanding-Client-Server-Protocols-and-Web-Applications.html

Session variables expire automatically.  Here's how the session works.
https://www.experts-exchange.com/articles/11909/PHP-Sessions-Simpler-Than-You-May-Think.html
Avatar of Crazy Horse

ASKER

Thanks, Ray. I will check it out. I know you said this isn't a good method to use but I can't seem to find any other ways of doing it with clear examples. 95% of examples out there use the session token method. You did mention CAPTCHA, but to be honest they irritate the heck out of me and I wouldn't want to irritate the heck out of users unless I really had to use it.
Avatar of Julian Hansen
Have you considered using a honeypot approach?
I use a honeypot for spam but isn't that different?

Just to check we are on the same page, my version of a honeypot is:

<input name="somename" style="display:none" type="text" />

Open in new window


if(!empty($_POST['somename']) { // some code to abort the submission attempt }

Open in new window

Yup that is a honeypot

Is your intention to check if the form submitted was one you actually sent to the browser?
I am trying to " prevent the type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site for which the user is currently authenticated." (OWASP on CSRF attacks)

https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
ASKER CERTIFIED SOLUTION
Avatar of Ray Paseur
Ray Paseur
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
@Black Sulfur,

I am familiar with OWASP and what a CSRF is but in your particular case how would this manifest itself. To answer your question it would be helpful to know what it is a malicious site could do on your site that you are trying to protect against.
Oh, sorry Julian, didn't know that is what you meant.

Well, I am just trying to implement security best practices. I only really have a login form and some forms where a user can update their personal details and password as well as add/edit/delete records in the database for event bookings. At this stage there is no online store with financial details or transactions.

So, like I mentioned above, I am just trying to implement it as a security best practice just like preventing SQL injection or XSS or spam etc.
I just found an interesting example and it is the first time I have seen the use of 2 tokens. Perhaps you guys would be kind enough to look at it and let me know if it's any better than the others which just use one token.

if(isset($_POST['submit'])){
	
	if(isset($_POST['_csrfname']) && isset($_POST['_csrfvalue']) && isset($_SESSION[$_POST['_csrfname']]) && $_SESSION[$_POST['_csrfname']] === $_POST['_csrfvalue'] && $_POST['_csrfvalue'] !== '') {
		echo "token valid";
	} else {
		
		throw new exception("CSRF token validation failed");
	}
}

$name = 'token-' . mt_rand();
$token = bin2hex(random_bytes(32));
$_SESSION[$name] = $token;

Open in new window


The form:

<input type="hidden" name="_csrfname" value="<?php echo $name; ?>">
<input type="hidden" name="_csrfvalue" value="<?php echo $token; ?>">
<input type="submit" name="submit" value="submit" />

Open in new window

... any better than the others which just use one token.
Short answer: No, it's still a clear-text form token and therefore it can be read and returned by "screen scraper" scripts.  Make no mistake, this is far better than nothing, but it's nowhere near as valuable as a CAPTCHA test.

I think of our security measures like the doors of a fire safe.  They are rated on the basis of time and temperature.  No fire safe is permanent and absolute, but stronger doors mean you have more time to suppress the fire before its contents are incinerated.  And it matters what you're trying to protect.  If you have bowling scores, or purchase histories, or medical records, or financial details, or nuclear launch codes the appropriate security measures are likely to be different.
SOLUTION
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
Most of the articles I have read and videos I have watched say that you should implement CSRF prevention on ANY form you have on your website. If this is true, would you not annoy the daylights out of your user if every time they wanted to add/edit/delete a record you were first presented with a CAPTCHA?
JSON Web Tokens appear to be conceptually the same as anti-tamper cookies.  This is something that PHP should have built into the language, in hindsight.  But like many PHP things, simplicity took precedence over security.  And the threats are always evolving.

The idea is illustrated by this teaching example.  You can install this script and run it to see the behavior.  The point of most of these authentication measures is really very simple.  Your server wants to share a secret token with your client, and wants your client to return the secret token on each request.  Most of our security measures go to the issue of sharing the token with "good" clients and keeping it secret from "bad" clients.

This can get as elaborate as you want -- you can test not only the tokens you've created, but also the time of day, the IP address, the browser identity, etc.  These things should remain much the same from one HTTP request to the next.  If any of them change, just ask for the client password again.  This is how your bank ATM does it, and they are pretty good at protecting our money.
<?php // demo/cookie_safety.php
/**
 * Demonstrate how to encode information in a cookie in a way that
 * can reduce the risk of cookie tampering.  We do this with a
 * salted message digest that contains the contents of the cookie.
 *
 * When the cookie is returned, we use the value to recreate the
 * salted message digest.  If the new message digest does not match
 * the message digest in the cookie, we can discard the cookie.
 *
 * The cookie will be url-encoded on the browser like this:
 * MARY+HAD+A+LITTLE+LAMB%7Ccf783c37f18d007d23483b11759ec181
 * It will be url-decoded before it is presented to php.
 *
 * http://php.net/manual/en/function.setcookie.php
 * http://php.net/manual/en/function.md5.php
 */
error_reporting(E_ALL);
ob_start();
echo '<pre>';


// A DATA DELIMITER
$dlm = '|';

// YOUR OWN SECRET CODE IS A SALT FOR THE MESSAGE DIGEST STRING
$secret_code = 'MY SECRET';

// A DATA STRING THAT WE WANT TO STORE (MIGHT BE A DB KEY)
$cookie_value = 'MARY HAD A LITTLE LAMB';

// MAKE A MESSAGE DIGEST FROM THE DATA STRING TOGETHER WITH OUR SECRET CODE
$cookie_code = md5($cookie_value . $secret_code);

// CONSTRUCT THE COOKIE STRING WITH THE CLEAR TEXT AND THE CODED STRING
$safe_cookie_value = $cookie_value . $dlm . $cookie_code;

// SET THE COOKIE LIKE "MARY HAD A LITTLE LAMB|cf783c37f18d007d23483b11759ec181"
setcookie
( 'safe_cookie'         // COOKIE NAME
, $safe_cookie_value    // COOKIE VALUE
, NULL                  // COOKIE EXPIRATION
, NULL                  // COOKIE PATH
, NULL                  // COOKIE DOMAIN/SUBDOMAIN http://php.net/manual/en/function.setcookie.php#76395
, NULL                  // COOKIE SECURE (HTTPS)
, NULL                  // COOKIE HTTP-ONLY
)
;


// HOW TO TEST THE COOKIE
if (isset($_COOKIE["safe_cookie"]))
{
    // BREAK THE COOKIE VALUE APART AT THE DELIMITER
    $array = explode($dlm, $_COOKIE["safe_cookie"]);

    // ENCODE THE DATA STRING TOGETHER WITH YOUR SECRET
    $cookie_test = md5($array[0] . $secret_code);

    // IF THE MD5 CODES DO NOT MATCH, THE COOKIE IS NO LONGER INTACT
    if ($cookie_test == $array[1])
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS INTACT";
    }
    else
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS <i>CORRUPT</i>";
    }
}
else
{
    die('COOKIE IS SET - REFRESH THE BROWSER WINDOW NOW');
}


// SHOW WHAT HAPPENS WITH A CORRUPT COOKIE
$_COOKIE["safe_cookie"] = str_replace('MARY', 'FRED', $_COOKIE["safe_cookie"]);


// TEST THE COOKIE AGAIN
if (isset($_COOKIE["safe_cookie"]))
{
    // BREAK THE COOKIE VALUE APART AT THE DELIMITER
    $array = explode($dlm, $_COOKIE["safe_cookie"]);

    // ENCODE THE DATA STRING TOGETHER WITH OUT SECRET
    $cookie_test = md5($array[0] . $secret_code);

    // IF THE MD5 CODES DO NOT MATCH, THE COOKIE IS NO LONGER INTACT
    if ($cookie_test == $array[1])
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS INTACT";
    }
    else
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS <i>CORRUPT</i>";
    }
}

Open in new window

annoy the daylights out of your user...
It depends on the CAPTCHA you choose.  And on what you're trying to protect.  There is some science, some art, and a lot of bunko, hokum, hooie, voodoo, and black magic in these choices.  And the things that worked a couple of years ago do not necessarily work today in the ever-evolving landscape of online security threats.  It's a full-time four year college major.  

My sense is "choose something" and don't get hung up on implementation details -- abstract the form security into a separate function or class and code to the interface.  Then you can change the concrete implementation if your requirements ever change.
JSON Web Tokens appear to be conceptually the same as anti-tamper cookies.
But are also portable across systems unlike cookies. They are encrypted and verifiable and are able to store additional payload outside of the authentication token.
Yes, we've used a similar concept in secure messaging.  It was implemented in XML, not JSON (mostly because the idea is so old) and it consisted of a message-text, along with a message-digest.  If the message-text did not match the message-digest, the entire message was deemed to be bogus. There were a few other details such as message encryption, but the core concepts seem to be about the same as JWT.
Last question about this before I close and issue the points, I have decided to put 2 forms on the user profile page as it is just easier for me to do what I am trying to do like that. Do I need to generate two hidden fields with 2 different random tokens or can I use 1 for both forms as they are on the same page?
Also, if you have time, please could you "translate" this as I am not familiar with this style:

$token = !empty($_SESSION['form_token']) ? $_SESSION['form_token'] : 'X';
 $input = !empty($_POST['form_token'])    ? $_POST['form_token']    : 'Y';

Open in new window

Well, the "two forms" is a loaded question.  If you use a single-use form token design, you will need two form tokens and a way to disambiguate the session and request variables so you can know which token goes with which form.  If you use a single reusable form token design, you sacrifice some security by allowing the token to be reused.  Not sure why you would need two forms, but if it were my work, I would look for a way to use one form, and one action script, even if I had logically disconnected information in two parts.

Now on to the other piece of this...
!empty($_SESSION['form_token']) ? $_SESSION['form_token'] : 'X';

Open in new window

See: http://php.net/manual/en/language.operators.comparison.php

This is called "ternary operator" notation, so named because it has three parts.  It's basically a True/False test.  The first part is the expression, to the left of the ?.  It is evaluated to either True or False.  If True, the second part of the statement is returned (the part between the ? and :).  If False, the third part of the statement is returned (the part after the :).

In narrative terms we are saying, look at $_SESSION['form_token'] with the not-empty() function.  The exclamation point means negation in PHP.  If not-empty() returns True, meaning that the variable is present, return the variable.  If not-empty() returns False, meaning the variable is empty or missing, return the letter X.  Assign the returned value to the $token variable

Since we do the same sort of thing with the $_POST variable, but return the letter Y into $input we are setting up a comparison that will yield a false result (no matching form token) if either or both of the values are missing.
That is called a Ternary expression. It is a basically a short hand if / else

$x = (expr) ? (if true then this) : (else this);

The first of the above statements can be expanded to
if (!empty($_SESSION['form_token'])) {
  $token = $_SESSION['form_token'];
}
else {
  $token='X';
}

Open in new window

There is a tendency sometimes to overuse Ternary operators and use them inappropriately. However, this is a classic use case - conditional assignment of a variable to a value based on the state of another variable.
Thanks guys! I had initially wanted to use 1 form but it got too confusing for me so I split it in 2.  I had a user profile edit page where they could edit their email address and password if they wanted to. But if they only wanted to edit their email address and not their password it was getting tricky.

Something like:

If you edit the email address, check the database to see if the new inputted email address exists and return an error if it does because you can't have duplicate email addresses. if the email address = the one you logged in with then don't do anything. If you don't enter a password in the "old password" field then assume you are only editing your email address. If user inputs a value into the "old password" field then you have to enter something into the new password field because it assumes you want to change your password and then also the confirm new password field needs to be filled in. Check the database to make sure that the old password inputted actually is the same as the one in the database for that email address etc.

So, I decided it would be easier to have 2 forms. One for if you want to change your email address and one for if you want to change your password. Made my life way easier.
Post a question about how to do MySQLi table maintenance for things like email addresses and passwords.  I can show you the general design patterns we use.  It's easier than you think once you know what lever to pull :-)  

Or learn about active record design patterns.  It will all make sense soon, I promise!
Okay, I am going to ask :)
By way of following up, here is the article that I wrote in response to this question.
https://www.experts-exchange.com/articles/28802/Improved-Form-Tokens-to-Guard-Against-CSRF-and-Screen-Scrapers.html

Best to all, ~Ray