When you look at the details, the question comes up how often these tokens need to be changed. Again, the general recommendation is that per-session tokens are sufficient. You can go for per-request tokens if you need extra security (or if you have a scenario where leaking CSRF tokens is an issue).
While verifying how big sites implement CSRF protection, I had a look at github.com. What boggles me is that I cannot understand why they implemented their approach as they did.
It occurs to me that they use per-request tokens (even more, unique per form tokens). However, old tokens are not invalidated after they have been used. I will give an example.
Let us consider the github profiles page: https://github.com/settings/profile.
The response to a GET request contains three forms with an authenticity_token as hidden input parameter. If I remember correctly, github is built based on Rails. So this fits, as the name matches the default name of Rails CSRF protection mechanism. Further, the tokens are 64 bytes long, which matches the length of the Rails masked CSRF tokens approach.
The tokens are attached to the following forms:
- Token 1: The Sign out Button
- Token 2: The Update profile button
- Token 3: The Save jobs profile button
If you reload the page, the values of all of the three tokens change.
Let us consider a subsequent POST that updates my profile.
- If I submit the intended Token 2, the action is performed.
- If I submit an invalid token, the action is not performed (the server returns HTTP 422).
So far so good, now to the things I do not understand. The action is also performed, without an error, when I do one of the following requests:
- Submit another token from the page (say Token 1 or Token 3 from above).
- Submit Token 2 again. So the old token is not invalidated as being used. This works multiple times.
- Reload the page, but submit one of the old tokens.
So I guess it comes down to: Why does github implement per-request (even per form) tokens, but never invalidates them / verifies that they belong to the correct form element?
Or maybe I am totally wrong, and github actually implements a different approach (like Encrypted Token Pattern).