1

I am a hobby developer and am developing an application with a Node JS / Express backend and a React frontend.

I am currently learning about Cross Site Request Forgery (CSRF), and I want to make sure that I understand it right.

As a victim I am using a CSRF vulnerable website, and I authenticate myself on that page. The backend grants me access, sends a JSON Web Token (JWT) token for further requests and my frontend saves this token as a cookie.

Now I visit a malicious website. The malicious website forges a post request to the vulnerable website containing a request to delete my account. Because the post request is directed at the vulnerable website the browser adds my JWT token and therefore my backend executes the request.

As far as I understand Same Origin Policy (SOP) will not protect me against this attack because SOP will just prevent the malicious website from reading the response to the forged request.

To mitigate this exploit, I can create an API endpoint that sends a random token. This token will be saved to the React applications state and for every post request to the API it gets returned to the server. The server can validate each request. Due to the SOP the malicious site is not able to retrieve this token and therefore it can no longer forge requests.

Is there a hole in my logic?

I have read multiple times that I should regularly generate a new CSRF token. Why? If the token is only saved inside the React state it should not be possible for a malicious site to retrieve it.

Lastly is it possible to mitigate CSRF by putting frontend and backend on different origins (https://example.com and https://api.example.com)? My JWT token would be saved as a cookie on the frontend. Therefore, if a malicious site forges a request to https://api.example.com/delete the browser would not append the JWT cookie because it is saved on a different page.

Thank you for reading my question and please correct me if im wrong.

2
  • I think you'd be vulnerable to XSS attacks with the first approach. (which might be OK depending on the nature of the site...) So malicious user-submitted content could retrieve the token.... or an injected script. For the 2nd, making things cross-site seems a little counter-productive to security.... in the end that cookie has to get somewhere and you'd have to cross domains to get it to the right place. (though example and api.example CAN share cookies depending on how they are set... so you don't have separate domains there but domain/sub-domain)
    – pcalkins
    Jan 9 at 22:38
  • it's always more secure to rotate tokens... since the anti-csrf ties content to session you pretty much have to create a new one for each new session. It's more secure to generate a new one for each form/request. (A static phishing page with stolen token won't work for long...) Ex: asp.net generates a new encryption key and token for each new form.... as hidden field. (I think the decrypted value will match the session cookie or a hash of it...)
    – pcalkins
    Jan 9 at 23:32

2 Answers 2

1

Also taking a look at this, I did find a great video on other security concerns that I'm also trying to implement. In this video he points out, correctly, that using ID and Access Tokens on the browser has some security issues if the page is compromised on the browser side, for instance using implicit flow. His recommendation is to use the "backend for frontend" pattern and sessions to avoid this, i.e. the browser would store a session instead of the id token and access token. FWIW Google does not support this yet on browser applications, though they do for native applications. Auth0 does support the approach AFAIK.

https://www.youtube.com/watch?v=XoBtUn4XczU

I will respond to this thread with our findings.

In our case we have a graphql endpoint that we are also trying to protect correctly.

0

Is there a hole in my logic?

No, but it's arguably over-engineered. If you're using script-based requests (rather than top-level navigation via HTML form submissions) for state-changing requests, all you need to do is add some property to the request that, for a cross-origin request, would make the request no longer a "simple" request (that is, make it something that an HTML form could not generate). A simple example is just adding a custom header: X-Anti-CSRF: true. Note that there's no secret values or anything here (though the session token is still sent in the cookie, and does still need to be secret). The server does need to make sure that the header is present, but that's all. Another very common way to do this is to require the request type be something like application/json, which is not allowed in HTML forms. However, it's essential if you do this to verify the complete Content-Type header, not just that it contains the string "application/json" and/or that the body parses as valid JSON!

The thing is, only on same-origin requests can script add a header (or change the content type to JSON) and not have the browser trigger a CORS Pre-flight. Pre-flight requests do not contain the request itself, they merely ask the server "hey, is this third-party site allowed to make this kind of non-simple request?" and the server will evaluate the various headers in the preflight (usually just the Origin header) and respond appropriately. Or it will just respond with "400 Bad Request", if the server doesn't need CORS and hasn't configured it. The default state of the server is secure against CORS misconfiguration; CORS can weaken your site's security, but never strengthen it, so leaving it unconfigured is fine (arguably, ideal).

Obviously, if you modify the CORS response to allow any untrusted site to make an authenticated request and add the relevant header / content type, then your CSRF protection totally breaks. Don't do that.

I have read multiple times that I should regularly generate a new CSRF token. Why? If the token is only saved inside the React state it should not be possible for a malicious site to retrieve it.

The traditional anti-CSRF token is essentially a second session token, stored in something that is not a cookie, and tied to your session token in some way (the simplest - though arguably worst - approach is simply to store the two tokens as a pair on the server, in a DB or similar). This means you want to consider all the usual session fixation attacks. The anti-CSRF token needs to change whenever the user's session becomes authenticated, or else an attacker on e.g. a shared machine (at a school, library, or internet cafe) could note the anti-CSRF token, abandon the machine, wait for a victim to log in, and then CSRF them (because the anti-CSRF token hadn't changed). Giving each account a unique but fixed token avoids that specific problem, but has the problem that if an attacker ever sees a user+anti-CSRF-token pair, even long after the fact, they can still use that token. Much better if the token is tied to the session rather than the user, and as such is unique to each session.

Or just use an approach that doesn't require an anti-CSRF token, such as the ones above.

Lastly is it possible to mitigate CSRF by putting frontend and backend on different origins (https://example.com and https://api.example.com)? My JWT token would be saved as a cookie on the frontend. Therefore, if a malicious site forges a request to https://api.example.com/delete the browser would not append the JWT cookie because it is saved on a different page.

Yes, but not the way you describe it. The approach you describe (where the cookie is scoped to the frontend-only domain) wouldn't work at all - that is, it wouldn't even be insecure, just non-functional - because the backend would never see the session token at all and would reject every request as unauthenticated. There are ways around this. Using a common domain ("example.com", in your examples) means you can scope the cookie to the entire domain including subdomains; this is less secure (what if there's another page on the same domain with an XSS flaw, or similar?) but reasonably common. Alternatively, set the cookie for the back-end domain rather than the front-end domain. This may fail on some browsers (especially if there is no common domain name between the two) if they are configured to reject third-party cookies (which are often used for tracking), but at least it would work much of the time.

However, neither of those latter two provide any CSRF protection at all. Cookies are attached based on the destination of the request, not the origin. If a request is bound for https://api.example.com and there's a cookie for https://api.example.com, the browser will include that cookie (modulo a whole bunch of caveats). This is, after all, the core design choice behind why CSRF works at all. You'd still need another form of anti-CSRF protection.

Also, note that - if you're using script-initiated API requests, as your design calls for - you will need to configure CORS on the backend to allow authenticated requests from the front-end's origin. If you use a custom header or whatever for CSRF protection, you'll need to allow the front-end origin (but no other origins!) to send such things too.

Note that simply checking the value of the Origin header on requests is NOT adequate protection, unless you reject origins other than your front-end's origin. While there is no way for an attacker to make a browser spoof the Origin header in a request, there are ways to omit it, at least on some browsers. Chrome and other Blink-based browsers send Origin on most requests, but this is non-standard behavior and other browsers that merely follow the standard don't always do this, or perhaps do it in different contexts.


There are other options too. The cookie samesite flag is relatively new (compared to other cookie flags), but supported by all modern browsers (IE never got support but is also officially deprecated). Samesite has a LOT of flaws and limitations that were done to try and maintain compatibility and add minimal work for site owners, but it does protect against the common CSRF attacks. The idea is simple: if a cookie is created with this flag set to "lax" (now the default on modern browsers) or "strict", the browser will not consider only the destination of the request when choosing what cookies to send. Instead, it will also consider the source - the origin - of the request, and not attach the cookie if that origin is different enough that it thinks this is a different "site". If you want cross-site cookies to work, you actually need to manually specify samesite=none on the cookie at creation!

There are also other types of requests that don't include cookies. For example, CORS requests default to not including them (though it's easy to request that they be sent), and CORS pre-flight requests never contain cookies.

7
  • that does not sound correct... just having a header is no protection at all... it needs to include something that only the server could have sent. Headers and origins can be spoofed. (I could be wrong, but it just doesn't sound right... couldn't the attacker just make the same request via JS??)
    – pcalkins
    Jan 10 at 22:02
  • Nope! Attempting to add a custom header to a cross-site request will cause the browser to make a CORS preflight request. If the server does not explicitly tell the browser that the origin is allowed to make requests, with that header, (and with cookies which the attacker also must request to make a CSRF attack; requesting to send cookies doesn't force a pre-flight but if the pre-flight is performed it must allow them), then the browser won't send the actual forged request at all. Absent a very severe CORS misconfiguration, no secret anti-CSRF token is needed. (Can still use one if desired.)
    – CBHacking
    Jan 12 at 7:33
  • well an ajax call would automatically send the cookie... but I don't know anything about pre-flight requests.. How do you configure the server so that it only allows requests with pre-flight? (I guess the pre-flight is triggered by the custom header?... or IS actually the custom header?)
    – pcalkins
    Jan 12 at 18:32
  • I think I understand now in the context of custom verbs... so if nothing happens on the server for a standard POST or GET, but only with a custom verb like DELETE or PUT, then the pre-flight will happen and prevent cross-origin. The origin can't be faked because in order for the attacker to do that they'd need to send the request themselves which would be absent the client-side cookie. So a custom header would be the same as a non-standard verb?
    – pcalkins
    Jan 12 at 19:07
  • I think it's worth noting that certain operations (user deletion, changing user details/password/email) should not rely on session alone... they should require a re-entering of password.
    – pcalkins
    Jan 12 at 19:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .