One token vs. multiple tokens to prevent CSRF atta

2019-03-28 11:02发布

问题:

I'm using Codeigniter and I want to prevent CSRF attacks attempts that may happen. And to achieve this I add a hidden input tag with a random token to each form I want to protect, and in the same time I keep this token in a session to compare against when begin handling this form data.

  // set a token to prevent CSRF attacks
  $csrf_token = md5(uniqid(rand(), true));
  $this->session->set_userdata("csrf_token", $csrf_token);

And the form will look like this:

<form action="path/to/handler/page" method="post">
    <input type="text" name="title">
    <input type="text" name="date">
    <textarea name="content"></textarea>
    <input type="hidden" name="csrf_token" value="<?php echo $this->session->userdata("csrf_token") ?>"> 
    <input type="submit" name="submit" value="Save"> 
</form>

And in the page where I handle submitted data I check CSRF attacks something like this:

  // make sure there is no CSRF attack attempt
  $csrf_token = $this->session->userdata("csrf_token");
  if (empty($csrf_token) || $csrf_token !== $this->input->post("csrf_token")) {
    die("Some message here!!");
  }

And that works pretty good. But as you see I generate a random token for each page containing a form and in some cases this causes a problem if for example I opened another tab in the browser to perform some another action. Consider this scenario:

  1. I opened add.php page to add a new item.
  2. I filled in all required data, but I didn't submit the form.
  3. Now I decided to open edit.php page in another tab in the browser to edit an existing item.
  4. Then I went back to the add.php page that was filled in and tried to submit the data.

At this point I will get an error because value of the token that has been stored in the session when I opened add.php page has been changed and replaced with another token when I open the edit.php page. So how can I fix this problem? Should I generate just one token for each user when he successfully login and then use this token in all pages that he may deal with? Does this approach has any risks or any cons?

回答1:

To solve this problem, you could create a token string with a unique key and store the pairs of keys/tokens in the session (as userdata in CodeIgniter).

Considering this scenario, You'll need these steps:

  1. Creating a unique token key.
  2. Creating a token string.
  3. Storing the key/token pair in the session (userdata).
  4. Creating 2 hidden <input> elements for CSRF key and token.
  5. Validating the posted key/token by checking the existence of key/token in the session.

Getting to work:

$csrf_key   = "TOKEN_" . mt_rand(0, mt_getrandmax());
$csrf_token = hash("sha512", mt_rand(0, mt_getrandmax()));
// Store the key/token pair in session
$this->session->set_userdata($csrf_key, $csrf_token);

Adding hidden inputs:

<form action="path/to/handler/page" method="post">
    <!-- form input elements -->
    <input type="hidden" name="csrf_key" value="<?php echo $csrf_key; ?>">
    <input type="hidden" name="csrf_token" value="<?php echo $this->session->userdata($csrf_key); ?>">
</form>

Validating the posted key/token:

if (count($_POST)) {
    if (! isset($_POST['csrf_key']) or ! isset($_POST['csrf_token'])) {
        die('No CSRF token found, invalid request.');     
    }

    $key   = $this->input->post('csrf_key');
    $token = $this->input->post('csrf_token');

    if ($token !== $this->session->userdata($key)) {                
        die('Invalid CSRF token, access denied.');
    }
}


回答2:

I scanned your post and don't see a reason why not to use the base codeigniter CSRF protection? It seems you are reinventing the wheel and creating problems that don't exist within its standard implementation.

Not to mention you are violating DRY principles by trying to print your tokens to every form. Any reason why not to keep it simple?

Codeigniter has build in CSRF protection that can be enabled in /application/config/config.php

$config['csrf_protection'] = TRUE;
$config['csrf_token_name'] = 'csrf_token_name';
$config['csrf_cookie_name'] = 'csrf_cookie_name';
$config['csrf_expire'] = 7200;