HardFork
HardFork

Reputation: 1073

CSRF tokens for Rails API application

In Rails API applications we don't have out-of-box CSRF protection. In particular, storing access tokens (JWT for example) in the localStorage is not recommended, it is recommended to store in cookies (with httpOnly flag, SameSite, and etc.). But this time we're vulnerable to a potential CSRF attack. In a full-stack generated Rails app, a CSRF token is generated and embedded every time we open a form. But, I don't know and couldn't find anything how we protect against CSRF using tokens in Rails API apps. Do we have best practices or could anyone suggest an approach? I use access and refresh JWTs.

Upvotes: 3

Views: 2908

Answers (1)

Gabor Lengyel
Gabor Lengyel

Reputation: 15570

This is a usual tradeoff of API design, and you can choose from several different approaches, with different risk profiles.

You can store the access token in localStorage or sessionStorage, accessible to javascript, and accept the risk. The risk obviously is mostly around cross-site scripting (XSS), because this way javascript will have access to the token, and in case of XSS, it can be accessed by the attacker leading to session compromise. If talking about an API, responses should have the content type set to application/json, which makes the API itself protected from XSS in modern browsers. However, that does not mean the client (presumably a single page javascript app) is also protected, that can easily be vulnerable and leak the token. Some frameworks are better protected by default against XSS, some are not so much, and you might have checks like static scans in your SDLC that give you a level of assurance that might allow you to accept this risk. Also if your SPA needs to send the token to multiple origins (different api endpoints), you don't really have another option. In this case the token can be sent as a request header, and CSRF is not an issue.

Or you can exchange XSS for CSRF, by storing the token in a httpOnly cookie. This is generally considered more secure, because CSRF in general is a lower risk vulnerability (but still significant ofc). In that case you will not be able to send the token to different origins, but XSS will also not have access. This does not eliminate XSS for the whole application, but at least the token will be secure. The cost is now you will have to deal with CSRF. One way to do so is the samesite attribute to cookies. Using that for the token cookie will prevent most cases of CSRF, but it is a UX tradeoff, users of some browsers will not be protected, and some cases might be missed when using the lax option for samesite (like when a GET request changes state). Only having samesite as the protection will likely also be flagged in a penetration test for the reasons above.

If based on the above you decide to have more protection, you can implement something like double submit, and still keep it stateless, which these APIs many times aim to be. In case of double submit, you generate a random value, set it as a cookie (either directly on the client, or by a response from the server), and copy the same value from the cookie in a request header. The server only has to compare the value from the cookie to the one from the request, if they match, the request is ok. The reason this works is because an attacker on their own domain (origin) cannot set or read cookies for the victim application domain, this is ensured by the same origin policy of browsers.

A somewhat different approach might be applying a message authentication code (like HMAC) computed from the whole request and a shared secret (like the API key), and checking that on the server, but this is a can of worms, it's easy to have unprotected fields not covered by the HMAC, the server needs to have access to plaintext api keys so it can compute the hmac and so on - it's not at all straightforward to get this right).

Note that if the client app is vulnerable to XSS, that negates any CSRF protection as the attacker will have a way to get any secret from the client and with that, perform any request, with any computed field (like a valid token).

Upvotes: 6

Related Questions