10

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

8
  • 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 Feb 20, 2019 at 3: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, 2019 at 3:57
  • The server at bar.com should check that the Origin header of the requests are foo.com or null. 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. Feb 20, 2019 at 4: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, 2019 at 4:25
  • 1
    Adding a link to @TobiasBergkvist 's great answer that is somewhat related security.stackexchange.com/a/197269/12776
    – Eran Medan
    Feb 20, 2019 at 17:50

1 Answer 1

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'))
9
  • 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, 2019 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, 2019 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, 2019 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. engineering.mixmax.com/blog/modern-csrf Sep 1, 2019 at 16:56
  • 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: cheatsheetseries.owasp.org/cheatsheets/… Jul 14, 2020 at 21:24

You must log in to answer this question.

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