9

tl;dr the Cookie-to-header-token method can't work due to the CSRF token cookie not being readable by the client in any way. Is sending the token in a header, and having the client save it in a cookie immediately considered a valid alternative?


I am trying to find a way to mitigate CSRF in a cross domain scenario where the client stores all the static assets (including the index.html entry point) and the server is in a completely different domain (no shared parent domain).

I was looking at first at the Cookie to Header Token method, but it seems that both the client and the server must be on the same origin.

(The server issues a JavaScript readable cookie named XSRF-TOKEN, the client, being on the same origin, can read the cookie, then add a header on all subsequent calls, e.g. X-XSRF-TOKEN, this is how for example Angular handles CSRF, this all works great as long as both are on the same domain or share some parent domain)

However, it seems that for cross origin domains, this won't work.

Assumptions

  1. XHR can't access the Set-Cookie header. XHR doesn't allow looking at response cookies. Even with the most permissive CORS headers, and even if not set as httpOnly, it's blocked by the browser. (It will also hide the Set-Cookie header if looking in dev tools) (https://fetch.spec.whatwg.org/#forbidden-response-header-name)

  2. Other domain's cookies are not JavaScript accessible, obviously, even if not set as httpOnly, and even if the XHR request is withCredentials=true and will send that cookie in future requests to the cookie's domain (assuming matching header in the server) it's never JavaScript accessible, and we all are grateful for that.

  3. Setting the Cookie Domain to that of the XHR's Origin doesn't work. What if the server will set the cookie to the client domain? It is impossible to set a cookie for a different domain of course, (https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.3) and this includes XHR, e.g. if I have XHR in page ui.example.com make a request to a server in api.example.net, and the server sets a cookie with domain ui.example.com, the user agent seems to reject the cookie from what I've seen, (I assume since the server and the page itself are still in different origins), although the XHR Origin is the same as the Domain that was set in the cookie, this won't work, and I'm sure there are good reasons for it.

The only method I can think of at the moment is having the server send the CSRF token as a header that the XHR can read, the client will then store it via JavaScript in a client domain scoped cookie, then the rest will work as in the Cookie to Header Token method above.

Questions

  1. Are any of my assumptions above incorrect?

  2. Is there any authoritative source that defines the alternative approach above as secure? Does it have significant disadvantages over the same-domain cookie-to-header-token method?

  3. Are there any other ways to securely mitigate CSRF for this use case?

Related Answers I Reviewed

I went through the following questions / answers but none of them felt like an authoritative answer to my question, I may have missed to read between the lines so my apologies if this is an exact duplicate

Eran Medan
  • 811
  • 1
  • 10
  • 19
  • Assumption 3: By setting `Domain=.example.com` on the cookie, it will be used by the browser for both `ui.example.com` and `api.example.com` – Tobias Bergkvist Feb 20 '19 at 03:52
  • Yep, you are right. But my use case sadly is that the domains are more like `foo.com` and `bar.com`, without a shared parent. – Eran Medan Feb 20 '19 at 03:57
  • The server at `bar.com` should check that the `Origin` header of the requests are [`foo.com` or `null`](https://security.stackexchange.com/questions/158045/is-checking-the-referer-and-origin-headers-enough-to-prevent-csrf-provided-that/197269#197269). You will also need to set `Access-Control-Allow-Origin: foo.com` in the response. (Due to CORS, not CSRF). Otherwise, the response will be read-blocked, and the Set-Cookie header will be ignored. I think this might be the problem here. – Tobias Bergkvist Feb 20 '19 at 04:22
  • Still I don’t get how the CSRF token gets from the server to the client in a readable way. Setting a cookie and sending it back it is doable with the right CORS headers (eg the session id) but for CSRF I need a cookie I can read via JavaScript, from a different domain. No CORS setting will ever allow me that.The only way I can think of is via returning the CSRF token in a header and then the client can save it as a cookie. The question is, does it have some flaws I’m missing? Tl;dr sending and receiving a cross origin cookie is not the problem, it works, making it JavaScript readable doesn’t. – Eran Medan Feb 20 '19 at 04:25
  • Yeah, it is true that you cannot read cookies using JavaScript stored with domain `bar.com` while visiting `foo.com`. If you could, this would be a huge security problem. You don't really need the CSRF token if you check the Origin header serverside though. And is there a specific reason you need to access it with JavaScript? – Tobias Bergkvist Feb 20 '19 at 08:58
  • OWASP state that relying on Origin header is only recommended as a secondary / defense at depth measure (they have some valid reasons but perhaps not applicable to my case) – Eran Medan Feb 20 '19 at 10:26
  • Regarding why I need to read it via JavaScript, this is the only way the double cookie submit method works. But since I’m on a different domain, I have the same problem as an attacker. I can submit the cookie but can’t read it to add the header that matches. But since double submit cookie (which looks to me as just a different name to the “cookie to header token” method) is also considered as a 2nd grade solution by OWASP and has limitations: https://www.owasp.org/images/3/32/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf so maybe relying on the Origin header is better. Good point – Eran Medan Feb 20 '19 at 10:33
  • 1
    Adding a link to @TobiasBergkvist 's great answer that is somewhat related https://security.stackexchange.com/a/197269/12776 – Eran Medan Feb 20 '19 at 17:50

1 Answers1

4

Our conversation started in the comment section, but I realized some inaccuracies in what I wrote. The sharing of cookies across domains is stricter than I thought at first. This should be a more comprehensive overview, and closer to what you might be looking for.

I will make a few assumptions:

  • You have two domains: foo.com (ui) and bar.com (api)
  • You want to prevent another domain like evil.com from causing side effects/reading responses from bar.com (CSRF + CORS protection)

Approach 1: Using CSRF tokens

This can be done using cookies, or simply using custom headers and storing the values in session storage or as a hidden input in a form. This means you manually need to send the CSRF tokens as custom headers with every request. Both from server and client.

Cookies will not be a good option here, since foo.com and bar.com are separate domains.

Limitations of cookies for different domains (that are not subdomains of the same domain)

foo.com --(request)--> bar.com
  • Any cookies stored in your browser with Domain=bar.com (that does not include SameSite=lax or SameSite=strict) will be sent along with the request assuming withCredentials=true
foo.com <--(response)-- bar.com bar.com --(request)--> bar.com
  • Here, Set-Cookie will work. Hence a redirect from foo.com->bar.com and then back will be able to set a cookie with Domain=bar.com. (So this could be a somewhat ugly/slow workaround)

Approach 2: Checking the Origin header

This is probably the simplest/cleanest option. (You won't need CSRF tokens if you use this method).

NOTE: The reason OWASP recommends only using the Origin header as a secondary measure to CSRF tokens is that the Origin header didn't yet support all common browsers when the recommendation was made. All common browsers have supported this feature for quite some time now. (It is currently ~3-4 years old)

*.com --(request)--> bar.com (CSRF)

Make sure that you check the Origin header of the incoming request in the bar.com-server. If this is either missing or https://foo.com, the request should be accepted. Otherwise, the response should be something like 403 (Unauthorized).

*.com <--(response)-- bar.com (CORS)

The response needs to have the proper CORS headers (assuming it was accepted, and the Origin header is not missing):

// Something like this, depending on your server language
response.setHeader('Access-Control-Allow-Origin', request.getHeader('Origin'))
  • 1
    I like option 2 a lot obviously :) if browsers added ‘Origin’ on all form POST submits or img src / script src basically CORS would stop every bad CSRF attack vector, no? (It will break any solutions relying on JSONP, well maybe about time...) in any case, thank you! I’ll let the dust settle and see if there are other answers and will accept it soon, thanks again – Eran Medan Feb 20 '19 at 10:52
  • 1
    One clarification: Re “Trying to set a cookie with Domain=bar.com using JavaScript while at foo.com will not work either” correct, but it works if you just respond from bar.com to an XHR request from foo.com. You can’t get the cookie content in any way due to the forbidden headers in XHR, it’s hidden from JavaScript but it WILL be set and sent back to bar.com. What you can’t do is set Domain=foo.com from a response form bar.com (you can but the cookie will be ignored by foo.com as you can’t set a cookie to a different origin, even if that origin is the origin of the XHR that made the request) – Eran Medan Feb 20 '19 at 10:58
  • 1
    To clarify: yes, ‘Set-Cookie’ is illegal header in the context of the XHR API, if it was allowed it would amount to seeing other domains cookies via document.cookie. But Set-Cookie from a server with properly configured CORS headers to a client in a different domain is not blocked, the cookie is set, just not visible via JavaScript in any way (even if set without httpOnly) and even not showing in chrome dev tools network tab. But it still gets set (I wrote a little api that returns the string value of the “hidden” cookie and it seems to be set correctly, just hidden from JS and dev tools) – Eran Medan Feb 20 '19 at 11:12
  • It's my understanding that currently the Origin header is not sent by all browsers for all requests? Maybe falling back to Referer is a solution. For example, I think img tags do not set Origin. https://engineering.mixmax.com/blog/modern-csrf – Dustin Wyatt Sep 01 '19 at 16:56
  • @DustinWyatt Yes, but a request that does not cause side effects on the server does not need to be blocked by the server. A CSRF attack is one that causes side-effects. (GET-requests should not have side effects). The concern here is rather to prevent the client from reading the response (Cross Origin Read Blocking is handled by the browser). NOTE: The Referer header is less reliable than the Origin header, since it is optional for the client to send it. It should not at all be relied upon for preventing CSRF. If the Origin header is missing, you should usually accept the request. – Tobias Bergkvist Sep 18 '19 at 15:21
  • @TobiasBergkvist Yes, I agree that GET should not have side effects. I do not think we can *count* on that. – Dustin Wyatt Sep 19 '19 at 13:46
  • @EranMedan: Approach 2 above is a misconception and should *NEVER* be used to prevent CSRF attacks. Use anti-CSRF tokens as a defense mechanism. See more at https://www.nccgroup.com/us/about-us/newsroom-and-events/blog/2017/september/common-csrf-prevention-misconceptions/. Tobias please modify your post above to reflect this as to not misguide other people. Thanks and AppSec FTW! – act1vand0 Jul 14 '20 at 20:17
  • 1
    @act1vand0 Approach 2 is not listed as a misconception in the link you posted. Is there something I'm missing? OWASP themselves list approach 2 as a way to prevent CSRF here: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#verifying-origin-with-standard-headers – Tobias Bergkvist Jul 14 '20 at 21:24
  • @TobiasBergkvist it's true that my reference above focuses on CORS and not the Origin header, I got that mixed up a bit. Note though that the OWASP article only recommends Approach 2 (Origin header) as a *complement*, and never as the main/only one to protect against CSRF since it has other drawbacks besides not being supported by some older browsers. The main protection method should always be anti-CSRF tokens. Other sources also mention Approach 2 as somewhat insecure/complementary, e.g. https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention. Hope this helps clarify. Cheers – act1vand0 Jul 15 '20 at 22:35