2

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.

Gabor Lengyel
  • 14,129
  • 4
  • 32
  • 59
storm
  • 795
  • 1
  • 5
  • 12
  • Gabor summarizes the issues pretty well, I don't have enough to add to warrant a separate answer. The HttpOnly flag is mostly irrelevant. It makes it marginally harder for someone to use the cookies. The main thrust here is 1) The API can't protect the client app against XSS. If the app has XSS, the API is impacted. This is unavoidable. 2) You shouldn't need CSRF tokens for an API, because you should architect your API to not be susceptible to CSRF. 3) Doesn't matter whether you use cookies or bearer tokens, points 1 and 2 hold true. – Mic Aug 14 '21 at 16:09
  • @Mic As for "You shouldn't need CSRF tokens for an API,...", as far as I understand CSRF is about whether you use cookies to store JWT or not. If you store access token in cookies then there is a chance to get CSRF attacked. One of the OWASP recommendations is "don't store JWT in localStorage". Then where? In cookies, right? So we must care about CSRF. – storm Aug 14 '21 at 16:38
  • @HarkFork OWASP is being obtuse and impractical there, but I think you're understanding correctly. Susceptibility to CSRF requires that the browser can be instructed to send the credentials by an untrusted party, which typically means cookies are used, but also applies to browser-managed auth schemes (HTTP Basic, Digest, and NTLM auth). And switching to cookies really doesn't materially reduce XSS risk, so you're forcing yourself to solve additional security problems with little meaningful benefit. – Mic Aug 14 '21 at 16:51
  • @Mic, Right. XSS risk still exists. But if we store or creds in cookies does malicious Javascipt code have access to creds? It is usually recommended to store in cookies because in case XSS attack, at least tokens are safe. – storm Aug 14 '21 at 17:04
  • Not read access to the creds, but still use of the creds in making authenticated API calls. – Mic Aug 14 '21 at 17:08
  • For that matter, XSS could still show the user the login form and prompt for creds from within the context of the trusted app. Stopping the attacker from reading the credential just really isn’t that valuable. – Mic Aug 14 '21 at 17:10
  • @Mic But are cookies (creds) transferred? What does the same-origin policy say? – storm Aug 14 '21 at 17:14
  • They have to be, or your app wouldn’t be able to use your API normally. So your cookie flags and CORS policy will need to allow it from your app. If you’re same-origin, it’ll be allowed implicitly, and if you’re cross-origin you will explicitly allow them to be transmitted from the same context that an attacker could read your local storage from. – Mic Aug 14 '21 at 17:18

1 Answers1

5

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).

Gabor Lengyel
  • 14,129
  • 4
  • 32
  • 59
  • 2
    Really good summary of the issues. A couple items I'd put more explicitly - httponly mitigates some risk associated with XSS, but most of the risk still remains. Even if the attacker can't read the cookie, they have execution within the context of a client application that can call the API. They will be able to make calls to the API with credentials via XHR. Also, if legit requests from the client app to the API are cross-origin, `SameSite=none` will have to be set on the cookie. – Mic Aug 14 '21 at 16:41
  • 2
    One more thing - for cross-origin requests, the CORS policy is going to be a factor. Ensuring that all requests that mutate state are preflighted will go a long way to prevent CSRF, as the `Access-Control-Allow-Origin` header can then prevent mutation from untrusted origins. – Mic Aug 14 '21 at 16:43