New CSRF token per request or NOT?
At owasp site it mentions that
Per-request tokens are more secure than per-session tokens as the time range for an attacker to exploit the stolen tokens is minimal. However, this may result in usability concerns.
And it concludes that
CSRF tokens should be:
- Unique per user session.
- Secret
- Unpredictable (large random value generated by a secure method).
The answer to your question is: it depends.
And you don't need to use session for timed tokens, you can just use the server-time and a secret key on the server.
But let's say it's better to generate a token each hour, then I would need two sessions: token, expiration,
No, you need a routine that is able to generate a token for a time-frame. Let's say you divide time per 30 minutes. The you create one token for the current 30 minutes in the form.
When then form is submitted and you verify the token against for now and against the previous 30 minute period. Therefore a token is valid for 30 minutes up to one hour.
$token = function($tick = 0) use($secret, $hash) {
$segment = ((int) ($_SERVER['REQUEST_TIME'] / 1800)) + $tick;
return $hash($secret . $segment);
};
Generally, it suffices to have one single token per user or per session. It is important that the token is bound to just one particular user/session and not globally used.
If you are afraid the token could get leaked to or get obtained by an attacking site (e. g. by XSS), you can limit the token’s validity to just a certain timeframe, a certain form/URL, or a certain amount of uses.
But limiting the validity has the drawback that it might result in false positives and thus restrictions in usability, e. g. a legitimate request might use a token that just have been invalidated as the token request is too long ago or the token has already been used too often.
So my recommendation is to just use one token per user/session. And if you want further security, use one token per form/URL per user/session so that if a token for one form/URL gets leaked the others are still safe.
If you do it per form request - then you basically remove the ability for CSRF attacks to occur & you can solve another common issue: multiple form submission
In simple terms - your application will only accept form input if the user ASKED for the form prior to the submission.
Normal scenario: User A goes to your website, and asks for Form A, is given Form A plus a unique code for Form A. When the user submits Form A, he/she must include the unique code which was only for Form A.
CSRF Attack scenario: User A goes to your website, and asks for Form A. Meanwhile they visit another "bad" site, which attempts a CSRF attack on them, getting them to submit for a fake Form B.
But your website knows that User A never asked for Form B - and therefore even though they have the unique code for Form A, Form B will be rejected, because they dont have a Form B unique Code, only a Form A code. Your user is safe, and you can sleep easy at night.
But if you do it as a generic token, lasting for an hour (like you posted above) - then the attack above might work, in which case you've not achieved much with your CSRF protection. This is because the application does not know that form B was never asked for in the first place. It is a generic token. The WHOLE POINT of CSRF prevention is to make each form token unique to that form
Edit: because you asked for more information: 1 - you dont have to do it per form request, you can do it per hour/session etc. The point is a secret value that is given to the user, and resubmiited on the return. This value is not known by another website, and thus cannot submit a false form.
So you either generate the token per request, or per session:
// Before rendering the page:
$data['my_token'] = md5(uniqid(rand(), true));
$_SESSION['my_token'] = $data['my_token'];
// During page rendering:
<input type="hidden" name="my_token" id="my_token" value="<? php echo $_SESSION['my_token']?>" />
// After they click submit, when checking form:
if ($_POST['my_token'] === $_SESSION['my_token'])
{
// was ok
}
else
{
// was bad!!!
}
and because it is "per form" - you wont get double form submissions - because you can wipe the token after the first form submission!