How to prevent Cross-Site Request Forgery
HTTP header Referer validation
Web browser automatically sends with each HTTP request a
Referer header whose value is the address
of the page from which the request was made. For example, when user navigated from en.wikipedia.org to
twitter.com, web browser will automatically send the header
Checking that the value of the Referer field is the same as the current site is not valid. Sending the Referer header can be disabled in the browser, for example, at the Incognito mode, which will block the loyal users.
Same-Site attribute Cookie
At the moment, Google are working on at specification "Same-Site" Cookie attribute. The attribute makes it possible to explicitly indicate that the Cookie does not need to be transmitted if the request comes from an origin different from that which Cookie has installed. Setting this attribute gives automatic protection against CSRF attacks. The standard has not yet been accepted and most web browsers do not support this attribute.
CSRF token validation
In process of sending a page to a user, the server generates a string, called a CSRF token, that satisfies the following properties:
- Unique value for each user session
- Time to live after which it becomes inactive
- An attacker does not know the token generation algorithm
The token must be sent along with every HTML form and HTTP request (GET, POST). When the request is received, the server checks the token and if it is correct, the request is processed.
HTML forms support only two HTTP methods. There are GET and POST. There is no way to execute a request with another method (PUT, DELETE) without using Ajax, which requires CORS of the header when accessing another source.
The original form of transferring money to bank.kom is converted to the next
<form method="POST" action="/transferMoney"> <input type="number" name="accountNumber"/> <input type="number" name="amount"/> <input type="hidden" name="csrf_token" value="e00e1b12789e4e8d1e3582051d23a1ec"/> <!-- CSRF-token --> <input type="button" value="Transfer money"/> </form>
Generation methods of CSRF tokens
Stateful: using an additional data storage
This is the most obvious, but not the most effective method. The server
generates a random sequence of symbols, and it is stored in the database (file, noSQL) for the current user
and sent to the client at the time of authorization. Then, when the request is received, backend checks
csrf_token is present in the data storage and its lifetime has not expired. The disadvantages
of this approach:
- Additional data storage load
- Additional place to store tokens
- You need to delete the old tokens after the expiration of the lifetime
Stateless: without using additional storage
In order not to store CSRF tokens in data storage, you need to create an algorithm for generating a token from data that an attacker can not get. For example, using a user ID and a string (Salt), which only the server knows:
Salt = getConfig('attacks/csrf.salt'); csrfToken = md5(Salt + UserId);
- Salt is a random string for the web application specified in the configuration file
- UserId is user ID of website. It can be a session identifier, derived from a user session (Cookie, JWT).
It remains to add the lifetime for a token. Let a token have a lifetime of 30 minutes. Let's change the process of generating a token as follows:
expirationTime = now() + 30 minutes; csrfToken = md5(Salt + UserId + expirationTime);
You might think that now you need to store expirationTime on the server. But additional data is simply passed by parameters on the frontend and added to the form by hidden fields.
<form method="POST" action="/transferMoney"> <input type="number" name="accountNumber"/> <input type="number" name="amount"/> <input type="hidden" name="csrf_token" value="e00e1b12789e4e8d1e3582051d23a1ec"/> <!-- CSRF-token --> <input type="hidden" name="csrf_expiration_time" value="2017-02-22 12:30"/> <!-- expirationTime --> <input type="button" value="Transfer money"/> </form>
Then on the server a token is calculated from the available data, and checked
csrf_expiration_time >= now().