151

This question is about protecting against Cross Site Request Forgery attacks only.

It is specifically about: Is protection via the Origin header (CORS) as good as the protection via a CSRF token?

Example:

  • Alice is logged in (using a cookie) with her browser to https://example.com. I assume, that she uses a modern browser.
  • Alice visits https://evil.example, and evil.example's client side code performs some kind of request to https://example.com (classic CSRF scenario).

So:

  • If we don't check the Origin header (server-side), and no CSRF token, we have a CSRF security hole.
  • If we check a CSRF token, we're safe (but it's a bit tedious).
  • If we do check the Origin header, the request from evil.example's client side code should be blocked just as well as it would when using a CSRF token - except, if it is possible somehow for evil.example's code to set the Origin header.

I know, that this should not be possible with XHR (see e.g. Security for cross-origin resource sharing), at least not, if we trust the W3C spec to be implemented correctly in all modern browsers (can we?)

But what about other kinds of requests - e.g. form submit? Loading a script/img/... tag? Or any other way a page can use to (legally) create a request? Or maybe some known JS hack?

Note: I am not talking about

  • native applications,
  • manipulated browsers,
  • cross site scripting bugs in example.com's page,
  • ...
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Chris Lercher
  • 37,264
  • 20
  • 99
  • 131

3 Answers3

54

know, that this should not be possible with XHR (see e.g. Security for cross-origin resource sharing), at least not, if we trust the W3C spec to be implemented correctly in all modern browsers (can we?)

At the end of the day you have to "trust" the client browser to safely store user's data and protect the client-side of the session. If you don't trust the client browser, then you should stop using the web at all for anything other than static content. Even with using CSRF tokens, you are trusting the client browser to correctly obey the Same Origin Policy.

While there have been previous browser vulnerabilities such as those in IE 5.5/6.0 where it has been possible for attackers to bypass the Same Origin Policy and execute attacks, you can typically expect these to be patched as soon as discovered and with most browsers automatically updating, this risk will be mostly mitigated.

But what about other kinds of requests - e.g. form submit? Loading a script/img/... tag? Or any other way a page can use to (legally) create a request? Or maybe some known JS hack?

The Origin header is normally only sent for XHR cross-domain requests. Image requests do not contain the header.

Note: I am not talking about

  • native applications,

  • manipulated browsers,

  • cross site scripting bugs in example.com's page,

I'm not sure whether this falls under manipulated browsers or not, but old versions of Flash allowed arbitrary headers to be set which would enable an attacker to send a request with a spoofed referer header from the victim's machine in order to execute an attack.

Community
  • 1
  • 1
SilverlightFox
  • 32,436
  • 11
  • 76
  • 145
  • The Flash example is a good one - and maybe other plugins may have a similar vulnerability. I can (unfortunately) only protect Alice from CSRF, if she uses a modern browser etc, that's clear. But it is not unreasonable, that even as a security-aware user, she might have installed 3rd party plugins - especially when they are from large (trustworthy) companies. Even though they may write safe plugins, I'm not 100% convinced, if they also think about CSRF! So unless browser sandboxes restrict the plugins from violating SOP (do they maybe?), I'd rather recommend to stick with the CSRF token. – Chris Lercher Jul 11 '14 at 12:14
  • @ChrisLercher: Yes modern day plugins appear to be a bit more robust. However, at any moment a new version could be released that allows an attacker to leverage it in such a way to bypass browser protections. The best way to handle it (e.g. token/header) will depend on the sensitivity of your data and whether such a risk is acceptable. Flash does obey a SOP, but the origin of a Flash plugin is the site it was loaded from (rather than the calling site like JavaScript). There is a `crossdomain.xml` that can enable cross-domain communication. – SilverlightFox Jul 11 '14 at 12:59
  • If the origin header is missing, a server can always reject the request, which mitigates the img src attribute type of attack. But agree, the trustless approach would encourage a csrf token. – GViz Feb 11 '22 at 21:05
32

Web content can't tamper with the Origin header. Furthermore, under the same origin policy, one origin can't even send custom headers to other origins. [1]

Thus, checking the Origin header is just as good at blocking attacks as using a CSRF token.

The main concern with relying on this is whether it it lets all legitimate requests work. The asker knows about this issue, and has set up the question to rule out the major cases (no old browsers, HTTPS only).

Browser vendors follow these rules, but what about plugins? They might not, but the question disregards "manipulated browsers." What about bugs in the browser that let an attacker forge the Origin header? There can be bugs that allow the CSRF token to leak across origins too, so it would take more work to argue that one is better than the other.

guest
  • 6,450
  • 30
  • 44
  • 5
    I've just tested Firefox 47 and it does not send an origin header for a cross-origin form post (a common way of attacking REST services that don't enable CORS for XHR), so I don't think an origin header check would be effective if the user is using Firefox. – Andy Jul 13 '16 at 13:41
  • 4
    For reference, the issue of Firefox not sending an "Origin" header is tracked at Bugzilla: https://bugzilla.mozilla.org/show_bug.cgi?id=446344 You could fallback to checking the "Referer" header in this case but in my experience some Firefox users block the "Referer" header because of privacy concerns (although IMHO it would be enough to strip the path and query). – Steffen Jan 05 '17 at 18:44
27

Explaining the terms

CORS (Cross-Origin Resource Sharing) is a protocol to allow two different domains to talk to each other.

A CORS (preflight) request needs to include Origin attribute in header. A response of a CORS preflight request contains Access-Control-Allow-Origin attribute in header to allow/restrict access from a different domain. From MDN web docs: enter image description here

The attribute Origin is in every request header except for GET and HEAD request. A CORS (preflight) request must contain Origin attribute, but not every request with Origin attribute is a CORS (preflight) request

The same-origin policy is a mechanism to restrict domains to talk to each other.

The CSRF token (Cross-Site-Request-Forgery) is stored in the session of the user and has to be send along with a POST/DELETE/PUT request. On the server side, the CSRF token will be compared to the value in the session and only allow to continue if they match.

Same origin policy prevents communication across domains

All browsers implement the same-origin policy. This policy avoids in general that a web application on domain A can make a HTTP request to an application on domain B. However, it does not restrict all requests. For example same-origin policy does not restrict embed tags like this:

<img src="https://dashboard.example.com/post-message/hello">

It’s irrelevant whether the response is a valid image — the request is still executed. This is why it’s important that state-changing endpoints on your web application cannot be invoked with the GET method.

CORS Protocol: Preflight check

You may use CORS to avoid same-origin policy and let the domain A make a request to domain B that would otherwise be forbidden. Before the actually request is send, a preflight request will be send to check if the server allows domain A to send this request type. If it does, domain B will have Access-Control-Allow-Origin field in the response header and domain A will sent the actual request.

For example, if no Access-Control-Allow-Origin is set, then a Javascript XMLHttpRequests would be restricted for domain A by a preflight, without executing the request on domain B.

Why you need CSRF token despite same-origin policy

If same-origin policy would work for all types of request there would be no need to use CSRF token, because one would have full protection by the same-origin policy and could use CORS to explicitly state which domain can communicate. However, this is not the case. There are a couple of HTTP requests that do not send a CORS preflight request!

GET, HEAD and POST requests with specific headers and specific content-type do not send a preflight request. Such requests are called simple requests. This means the request will be executed and if the request was not allowed, then a not-allowed error response will be returned. But the problem is, that the simple request was executed on the server.

As forms existed before CORS and where already allowed to send stuff to any origin, one didn't want to break the existing internet with pre-flight requests for POST requests. See also here:

The motivation is that the element from HTML 4.0 (which predates cross-site XMLHttpRequest and fetch) can submit simple requests to any origin, so anyone writing a server must already be protecting against cross-site request forgery (CSRF). Under this assumption, the server doesn't have to opt-in (by responding to a preflight request) to receive any request that looks like a form submission, since the threat of CSRF is no worse than that of form submission. However, the server still must opt-in using Access-Control-Allow-Origin to share the response with the script.

Unfortunately, a plain <form action="POST"> creates a simple requests!

And because of these simple requests, we have to protect the POST routes with CSRF tokens (GET routes don't need CSRF because they can be read anyway by embedded tags as shown above. Just make sure you don't have a state-changing get method).

Is checking origin of request enough?

Since the POST request is sending the origin along in the header and browser disallow modifying it, one could wonder if checking origin alone is enough, without the pre-flight check. However, scripts that are executed on CLI can set origin to any arbitrary value. Also, it still may be possible to change the origin due to a combination of plugins that the user may have available. Another thing is that browsers may fail to support it (Edge was released in April 2015, but supported origin header in June 2015, in Firefox was a bug that was fixed in 2008 https://bugzilla.mozilla.org/show_bug.cgi?id=446344).

From wikipedia:

Various other techniques have been used or proposed for CSRF prevention historically. Verifying that the request's headers contain [...] HTTP Origin header. However, this is insecure – a combination of browser plugins and redirects can allow an attacker to provide custom HTTP headers on a request to any website, hence allowing a forged request

Thus its recommended to use a CSRF token instead of relying on origin header. But there are cases, where it may be better to not have a CSRF token, because of session expiration.

Problem with CSRF tokens

I would avoid to use CSRF tokens for contact forms. As it may lead to a situation, that a user wrote for a long time and when he submits, the session is outdated, CSRF token is invalid, and the whole message of the user is removed. That would be a terrible user experience. For a contact form, you want to avoid getting messages from SPAM, so recaptcha3 may be reasonable choice, as it does not require user interaction.

You could also increase the life-time of sessions on your browser, but this may end up in a large storage size. Alternatively, you could use cookie hashed session that are stored at a client. Those session should also not expire, even if the client has to make a 12 hour break before submitting the form.

My summary

  • Use CRSF token for critical endpoints, like login or shop checkout
  • For practial reasons, I would suggest not to use CSRF token for application forms or contact forms.
  • Never have a state-changing GET endpoint
Adam
  • 25,960
  • 22
  • 158
  • 247
  • Quote "...simple requests,... the request will be executed and if the request was not allowed, then a not-allowed error response will be returned." So you are saying the server will reject the request? Then why is CSRF token still necessary here? – Morris Mar 13 '22 at 20:51
  • 1
    @Morris server will execute and return an error message. So it will upda profile or w/e and respond with no access – Adam Mar 13 '22 at 22:17
  • What's preventing a evil domain from first scraping the HTML form string, obtaining token, then submitting evil requests with token? I've seen in above discussions, some said "Form HTML must require authentication", but we also said "When myApp is logged in, Evil domain can make a request to myApp domain, and browser will automatically send myApp cookies" - doesn't it mean Evil domain's request would still be considered "authenticated" ? – Morris Mar 14 '22 at 18:10
  • @Morris CSRF token is matched vs a key stored in a session. If you obtain token but wihtout the cookie of the user, meaning without session, then request is not executed and you get error message. – Adam Mar 14 '22 at 19:40
  • 1
    I don't get it: if the server checks the value of the `Origin` header and rejects requests with invalid origin, why do I still need CSRF token? – izogfif Apr 26 '22 at 15:36
  • @izogfif same question as from Morris. If you send a POST request to update your profile from invalid origin, then you receive an error message saying not allowed origin, but the action (the update of your profile) was still executed. – Adam Apr 26 '22 at 18:25
  • 1
    @Adam What kind of action are you talking about? The server checked the origin, it was invalid (did not match the allowed ones), and returned 403 (HTTP "forbidden") response. What action was executed? The profile was not updated because the request was rejected by the server. – izogfif Apr 26 '22 at 20:00
  • @izogfif no, same origin policy will not prevent the request from being executed on the server. The response returns forbidden but the request itself was executed. Thatis because there are no preflight requests for GET and POST. Maybe try to read https://stackoverflow.com/a/33324803/2311074 – Adam Apr 27 '22 at 06:11
  • 1
    @Adam Preflight requests have nothing to do with it. Let's say that the browser sends the request from attacker's domain without preflight request but with header `Origin` set to `https://attacker.org`. The server checks the value of the `Origin` (because there is a server-side check like `httpRequest.headers['Origin'] == 'https://validsite.net'`) and immediately sends `403: forbidden` reply, without even reading the body of the request and doesn't execute the command to update the profile of the user. So I still don't understand why you think that checking the `Origin` header is not enough. – izogfif Apr 27 '22 at 11:36
  • @izogfif did you read the reference? Especially *So, a POST with Content Type application/x-www-form-urlencoded will hit to the server (this means a CSRF vulnerability) but the browser will not make accessible the results from that request.* – Adam Apr 27 '22 at 17:26
  • @izogfif oh you mean if you prevent manually on the server to execute the event if Origin does not match? Of course you could do that. But thats not default behavior. See also https://security.stackexchange.com/a/197269/108639 – Adam Apr 27 '22 at 17:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/244262/discussion-between-izogfif-and-adam). – izogfif Apr 27 '22 at 17:52
  • @izogfif actually you can spoof the origin header using curl on terminal/script, thats why ist not secure to rely on origin https://stackoverflow.com/a/21058346/2311074 – Adam Oct 16 '22 at 17:06
  • @Adam it's never secure to rely on anything that comes from outside. Regardless, this is outside of the scope of issue presented in this StackOverflow question. – izogfif Oct 16 '22 at 20:53
  • @izogfif not sure what you mean, I wanted to answer your very original question "why do I still need CSRF token?" – Adam Oct 17 '22 at 18:44
  • @Adam OK, let's continue that conversation. Why do I still need a CSRF token? In particular, when the sender is not restricted by built-in security measures of an ordinary browser and can construct and transmit any HTTP requests to the server? – izogfif Oct 19 '22 at 06:35
  • @izogfif I think if there are ways to modify the header origin, you should not rely on it. A CSRF session based token is not known to the attacker, so its secure. No more to say to it, I also don't want to go in a discussion here, as I am not a security expert. – Adam Oct 21 '22 at 19:14
  • Also from https://en.wikipedia.org/wiki/Cross-site_request_forgery#SameSite_cookie_attribute :`Various other techniques have been used or proposed for CSRF prevention historically. Verifying that the request's headers contain [...] HTTP Origin header. However, this is insecure – a combination of browser plugins and redirects can allow an attacker to provide custom HTTP headers on a request to any website, hence allowing a forged request` – Adam Oct 21 '22 at 19:14
  • @Adam If the goal is to protect an average user of a modern browser without plugins designed to overcome built-in security measures, then both CSRF token and checking HTTP "Origin" header provide the same level of security. If the user can gain access and compromise one of them, he can compromise the other one, too. CSRF token was invented long before HTTP "Origin" header was introduced and supported by all modern browsers. Nowadays there is no need to use CSRF token anymore, since popular browsers and server-side frameworks have built-in support for HTTP "Origin" header. – izogfif Oct 24 '22 at 17:35
  • I don't think you should rely that people don't have plugins installed. Also `If the user can gain access and compromise one of them, he can compromise the other one, too.` does not seem to be correct. Faking HTTP Origin Header and session_id are different things. They have nothing in commen, if you can compromise one you do not get the other too. – Adam Oct 25 '22 at 10:45
  • Or to be more precise, you can always fake the HTTP origin header, but its always a secret what the session_id is. – Adam Nov 15 '22 at 05:01