Cross-Site Request Forgery (CSRF) is a way for an evil web-page loaded into a user’s browser to perform actions using credentials the user previously provided for use with other websites. The Same Origin Policy is a design principle that modern web browsers implement to partially protect their users against such attacks. However, the same-origin policy is too strict for some sites; Cross-Origin Request Sharing (CORS) provides a way for a web-site to inform a browser about what kinds of cross-origin requests are safe for that site.
This article is the result of notes I made from multiple sources and merged together into a hopefully correct and helpful whole. The primary resources I used are listed at the end of this article.
And a warning: some articles (particularly some postings on Stack Overflow) are simply wrong; use care when reading any resource on the topic of security (including this one).
The Problem With Cookies
A user who has logged in to web-banking and then (in same or different tab) visits some other evil site may later find that bank transactions have been made on their behalf. Or someone logged into Facebook may find that posts have been made on their behalf.
Such authentication cookies may be cached for long periods of time, making this attack feasable long after the user has logged into a site (potentially weeks). Explicitly logging out of a site avoids the problem (and is highly recommended after visiting sites such as online banking), but can be annoying for frequently-visited sites.
Traditionally, any website can serve up an HTML page which contains references to other websites, including:
- Links which navigate to any target site when clicked (hyperlinks)
- Elements such as images which make a GET request to any target site immediately
- Iframe fragments which make a GET request to any target site immediately
- HTML forms which make a POST request to any target site when the submit button is clicked
Attempts to use any of the above to perform undesired requests that take advantage of cached credentials in the user browser are named cross-site request forgery aka CSRF.
- the protections are not complete and the server needs to take some precautions too in order to completely close the related security problems, and
- the same origin policy is sometimes too strict; site A might have some non-security-sensitive urls that other sites are allowed to access, or might have a trusted partnership with specific other sites.
The first problem is addressed below under the title “CSRF Protection”. The second problem is addressed under the title “CORS Support”.
One variant of problem 2 which caused me particular pain is when developing a front-end for a site providing rest services. In production the site and the rest endpoints are at the same location; however during development, it is convenient to serve the front-end pages from a local server while still calling rest-endpoints in a remote development environment. Unfortunately, a web-browser sees those requests as cross site requests and will block them by default! Solutions include adding CORS support to the server, and specify that localhost” is a trusted origin (for development server only), or disabling CORS in the browser (currently only Chrome supports this).
The Same Origin Policy
A browser treats two URLs as being for the same site (origin) if:
- scheme (protocol) is same, eg both HTTPS
- host is same
- port is same
The protections offered by the same origin policy are particularly relevant when a browser supports tabs, as there may be multiple pages loaded from different origins in memory at the same time. However it is still relevant when only one tab is open if the browser still holds cookies for other sites (user has not logged out to invalidate a cookie, and cookie lifetime has not expired).
Some cross-origin HTTP requests are not blocked by the same origin policy. These include:
- GET or HEAD requests with standard headers only
- POST requests with standard headers only and content-type of form-encoded-data or plaintext
I suppose in some circumstances a cross-origin HEAD request could be a minor problem (eg indicate if a specific URL is valid), but it doesn’t seem to be a major concern.
POST requests with body holding form-encoded-data or text are allowed simply for historical reasons; blocking these would break too many sites. Security-sensitive sites which are concerned about cross-origin requests should either avoid traditional HTML forms or take additional precautions (see later).
It is important to note that problems with cross-origin requests are not really a server-side problem; they are caused by insufficient cookie isolation in the client (the web browser). What the server sees in all cases is a properly-formed HTTP request with appropriate authentication information. However in practice, the browser just cannot properly isolate websites to protect its user while providing the user-experience that users now expect. It is therefore necessary for the browser and server to co-operate in order to block such attacks. This can be achieved by combining same-origin-policy, CORS, and additional checks of http requests on the server side.
Unfortunately, simply stripping out all cookies when a cross-origin request is made is not possible; there are non-authentication cookies which are useful for such requests, eg those indicating user preferences for a particular site.
Same origin policy constraints:
- reading cookies associated with other origins,
As noted above, the “same origin policy” does still allow the following (but requires the browser to set an “origin” header which the server can optionally check):
- GET or HEAD requests with “standard” headers
- POST requests with “standard” headers and “standard” content-type (HTML form)
Cross-Site Scripting (XSS) Attacks
This article is primarily about cross-site forgery attacks (CSRF), and how cross-origin policy, CORS and server-side checks can deal with them.
There is a related attack called cross-site scripting (XSS) which avoids the cross-origin policy protection completely by having attacking code come from the site under attack. When the origin is the same, the cross-origin policy checks do not apply.
See cross-site scripting aka XSS for more information.
A cookie is a block of data that an HTTP response can “associate” with the request URL. The browser should remember this data and resend it on later requests to the same URL.
There are a few special flags that the webserver can associate with a cookie. Unfortunately while it is a good idea to set these flags when possible, they do not solve the CSRF problem. Just for reference, the relevant flags are:
Secure: cookie is only sent on HTTPS requests, never HTTP requests, which prevents mistakes/attacks which force a non-HTTPS request to be made which can then be intercepted by a man-in-the-middle to obtain the cookie contents.
This section describes the steps that server-side code needs to take to prevent CSRF attacks against the website.
It is assumed that every endpoint (URL handler) in the server-side code checks the authentication/authorization information in the incoming request correctly; this section addresses the additional steps needed to ensure that an evil site running code within an authenticated user’s browser cannot fake additional requests the user did not intend to make.
The problem is that the cross-origin policy does not block GETs or POSTs with traditional content; an evil web-page can still trigger such requests to arbitrary URLs and cookies associated with that target URL are automatically sent. When such requests can be security-sensitive then the server needs to deal with them.
The nearest that there is to an official guide to securing websites against CSRF is the OWASP CSRF Page. It is written by experts, ie is technically correct, but I found it somewhat confusing at first - maybe the description below is easier to follow. If anything below contradicts the OWASP site though, trust OWASP (and please let me know)!
This solution does make it impossible to have “long-term” credentials cached in the browser for that site. However possibly “single signon” can help here, allowing site-specific credentials to be reallocated when needed without having to prompt the user to log in again. See later for a quick note on single signon.
An attacker can potentially trigger a CSRF request to the site using GET or POST with traditional headers to avoid cross-origin-policy constraints, but cannot set the custom HTTP header. Not only should it be impossible to set the http header on the request, but setting the header also means that request does not fall into the “exempted” cross-origin requests (due to the custom header), and so the cross-origin-policy checks then apply (including preflight check).
Sync Tokens (Embed Additional Authentication Tokens) aka Synchronizer Token Protection
In traditional websites, the server can allocate a “next transaction id” (aka Sync Token) for each user, and embed it in the HTML links and forms within the HTML page it returns to a user after a request. On the next request from the user, the id will be included in the request; this should be checked against the expected value for that user, and then a new id allocated and embedded in all links and forms in the returned page.
Because this ID changes on each request, and is not stored in any cookie, it is very difficult for a CSRF attack to predict the correct value that it should include in its faked request. Note that CSRF attacks do not have access to the “current page” in any way.
The per-request ID should be checked in addition to checking authentication information stored in a cookie.
Of course the first time a user visits a site, they have no “sync token” to include in their request. The “entry pages” for the site should not require a sync-token - and should not perform any security-sensitive operations or return security-sensitive content (eg bank balance). The HTML returned from a visit to such an “entry page” may include buttons/forms/etc associated with security-sensitive URLS - together with the appropriate sync token. This makes it relatively safe to cache credentials in cookies; an attacker can trigger a CSRF request to an “entry page” but those are harmless. The attacker can also potentially trigger a CSRF request to a sensitive URL which is of a “simple” type (get or post with traditional headers), but as they cannot guess the right sync-token, those requests are rejected.
Checking Referer and Origin
In this approach, the authentication information is stored in a cookie as usual. The server then relies on the browser to indicate whether a cross-origin request is being performed, and all such requests are rejected on the server side even when a valid authentication cookie is present.
The point of the sync-token checks is that “simple” requests can arrive with credentials, but maybe not origin. However 99.9% of “simple” requests will have (origin or referer), and for some use-cases the app can simply reject requests that don’t have these. This allows a much simpler solution to the CSRF issue.
As noted earlier, there are three kinds of requests that need to be dealt with separately:
- gets or posts with traditional headers and content-type
- all others (aka “complex requests”; see CORS “pre-flight” later)
For the “all others”, the browser cross-origin policy should protect users from harm; the browser will detect cross-origin requests and first make an OPTIONS request to the target site to see if access is allowed. To verify the server is set up correctly (which is usually the case by default), the developer should make an OPTIONS request to the root url of the site and ensure that the response is either:
- an HTTP error (4xx);
- an HTTP success with no CORS headers; or
- an HTTP success with CORS headers which only grant access to appropriate trusted sites.
These results will ensure that any web-browser which supports CORS (which is all sane ones) will block such “other” requests when triggered via a CSRF attack. See CORS later for more info.
To handle POST requests, the easiest case is for the server to simply reject POST with traditional content. This is common with “pure REST” webservers, where any POST request will be expected to have content-type of “
application/json”. A CSRF attack therefore can submit POST-with-traditional-content and have it rejected by the server as “invalid content type” or submit POST-with-other-content-type and have it blocked by the browser due to falling into the “other” category above.
Handling GET requests is the trickiest; it is quite likely that server-side code supports GET operations which return sensitive data for the calling user. There are two HTTP headers which can be used here:
Referer(yes, it is spelled wrong)
It can also sometimes be tricky for code in a webserver to know exactly which URL its front-end is served at (as seen by the browser), due to HTTP proxies and DNS mappings. Often the simplest solution is to provide the expected base URL to the webserver as a configuration-parameter. Header “host” is usually set by http proxies to help with this issue.
Referer has been part of the HTTP specification for a long time, and is set on almost all requests. It was originally intended for a different purpose, but works well for CSRF protection.
Origin was introduced as part of the same-origin and CORS specifications; it is always set by a browser which has detected a “complex” cross-origin request. Unfortunately some web-browsers set the header only for complex requests (ie it is not present on normal GET requests) while other web-browsers set the Origin header for any cross-origin request.
The Origin header is easier to deal with than Referer as
origin includes only the “base url” component (which is all that is relevant) while
referer contains a complete URL which then needs to be truncated to just (scheme, host, port) before being checked - but that is not particularly difficult. However as Origin is not always present, it is best to use the Origin header if present, else the Referer header. There are (rarely) http proxies which strip out the Referer header to “protect user privacy”; the best way to deal with this is probably to just reject requests where neither Origin nor Referer is present, and require such users to use a browser that supports Origin for simple cross-origin requests.
When navigating to a website via a bookmark or similar, neither Origin nor Referer will be set. However such a request will usually be to an “entry page” for the site, rather than being a security-sensitive request, and can be dealt with as a special case.
Some online advice states that the Referer header is not trustworthy and should not be used. There were in the past cases where bugs or hacks could be used to insert a fake value into this header, but in my opinion these are very unlikely in modern browsers. In particular, the Adobe flash plugin could be abused at some time in the past to do this; fortunately this plugin is (a) fixed, and (b) now very rarely used. There is a “Referrer-Policy” header in the HTTP standard to control how the “Referer” field is populated by browsers; however IE and Edge do not support it, making it rather useless.
It is of course possible to fake headers such as Referer and Origin from custom code - or even just a tool like Curl. However that is not relevant; such requests will still need valid authentication information in the headers. CSRF is a problem specific to web browsers, due to the fact that logic from multiple sites is being executed in the same environment together with the fact that cookies are sent with a request to site A even when that request is triggered from site B. This whole complexity regarding CSRF protection is about cooperation between the browser and server to block that specific scenario; the problem does not occur with dedicated client applications.
This article talks a lot about “credentials in cookies”. It is probably now a good time to add just a little more detail on that topic..
Server Verification of User Authentication
In any client/server system, the server should validate client requests. Parameters provided should be valid, and most importantly each request which is related to a specific user should include some kind of authentication token - in the case of http, usually an http header containing either a “session id” or a cryptographically signed block of data representing a login session (userid, expiry-time, etc). And of course the token should only be transferred over an encrypted network connection (eg https).
That’s really the limit of a server’s responsibility. Of course if any attacker manages to get hold of the authentication token then they can make requests as if they were the authenticated user, which is bad. Keeping the authentication token private on the client side is not really the server’s problem. Nevertheless, when the client is a web-browser then there are a few things that the server can do to help that web-browser.
With a dedicated client application, keeping the authentication token secret is not particularly complex. Truly paranoid client apps will for example encrypt the token in memory to make it difficult for another app which manages to scan the memory-space of the client.
When the client is a web-browser, however, keeping authentication tokens secret is trickier - and thus this whole article.
Single Signon and CSRF
The above descriptions assume that some kind of site-credentials (typically a “session id”) are stored in a long-term cookie within the browser. This leaves a very large time window for CSRF attacks; as long as the cookie is valid, a CSRF attack is possible.
An obvious solution is to reduce the lifetime for such cookies. Setting a lifetime to something like 15 minutes does not completely bypass CSRF attacks, but is definitely robuster. However that means that when the user has not interacted with the site for 15 minutes, they need to relogin - rather annoying.
A solution to that problem is single signon. Here, a separate site X is responsible for authentication of the user and issuing of a session-id for some other site A. The general logic flow is:
- user visits site A
- site A detects that no credentials cookie for A exists, and redirects to site X
- site X detects that no credentials cookie for X exists, and prompts the user to log in
- site X validates credentials, returns a long-duration cookie for site X plus a short-duration cookie for site A, and redirects back to A
- user continues using A
- eventually, user sends a request to A with expired credentials; A then redirects to X
- user browser follows the redirect to X, and includes the long-duration cookie associated with X
- site X verifies that the long-duration cookie is valid, returns a new short-duration cookie for site A and redirects back to A
- user continues using A without (probably) having noticed that their browser did a quick two-redirect step to refresh the short-term cookie for A
The short lifetime of the credentials info for A (“session id”) makes access to A somewhat safer with regards to CSRF attacks. Site X is of course now in danger of CSRF attacks, but such single-signon sites have (a) a very small set of rest-endpoints, and (b) are very carefully coded to be safe against CSRF attacks.
Single signon also provides the benefit that multiple sites can trust the same central authentication server, allowing a user to log in just once to access them all.
As noted, the same origin policy can sometimes be too strict. The CORS specification allows a server to inform a browser about what kinds of requests are safe to be performed when:
- Site A stores authentication information in the browser as cookies, and
- HTML or code from site B tries to send a request to site A (a cross-site request).
The Wikipedia article on CORS gives a pretty good overview.
My CORS Use-Case (Google Cloud IAP)
Unfortunately, in this case the CORS support solved the issue only briefly. A few months after fixing this problem for developers, Google made some changes in IAP (in Feb 2018) which broke it again (and with no workaround possible); pre-flight requests are now getting redirected to the authentication server rather than being passed to the web-server which makes all CORS code in the web-server irrelevant. After enquiries with Google, they stated that CORS compatibility in IAP was never an explicit feature. They agreed that it would in fact be a good idea to support this - but no timeline was given. Ah well, at least I got to learn about CORS.
If you also wish to support web-developers with local dev-environment making calls to resources running in GCP, then the best solution is probably to use a local HTTP proxy running on developer machines which forwards requests for specific urls to GCP. The web browser thus sees all requests as being to localhost. I haven’t set this up personally though, and don’t know how complex such a proxy would need to be.
CORS Pre-flight requests
As noted earlier, browsers divide cross-origin requests into two categories:
- simple (requests can be made immediately)
- complex (preflight authentication required)
The following are considered simple:
- GET and HEAD with standard headers
- POST with traditional content-types (eg form-encoded, but not JSON)
All other operations are considered security-sensitive, and trigger a “pre-flight request”, eg:
- PUT and DELETE operations
- POST operations with content other than form-encoded (in particular, JSON content)
The “same origin policy” causes all non-simple cross-origin requests to be checked against the known rules for the requesting origin to the target site; if no rules are currently cached for that (origin, method, headers, dest) combination then the request is suspended while the browser makes a “CORS pre-flight” request to the server. The target site response provides the rules that apply to a specified origin and settings; servers which do not have CORS configured will return a default response which causes the browser to block all complex operations. Thus with a standard non-cors-aware server, GET/HEAD/POST with standard headers and datatypes are allowed while other requests are blocked. CORS settings are not per-url, only per-site.
Even when a CORS response indicates that specific complex operations are allowed, the http header
origin is always set by the browser on such requests before sending them to the server.
A CORS request is simply an OPTIONS request to the root url of the server first (ie “OPTIONS /”), with the origin header set. The request includes the “method” (http verb) the origin is trying to apply, and a list of headers the origin is trying to send. If a 404 (not-found) or similar error code is returned (which is the default for a non-cors-aware server) then the request is blocked. Otherwise, the server returns an empty response with several headers set:
- Access-Control-Allow-Origin (which is either missing, identical with the request origin field, or value “
- Access-Control-Allow-Credentials (missing or “true”) - indicates whether browser should send cookies (and other stuff: http-auth, client-side-certs)
- Access-Control-Allow-Methods - tells browser what “http verbs” this origin is allowed to send (others should be blocked)
- Access-Control-Allow-Headers - tells browser what “http headers” this origin is allowed to send (others should be blocked)
- Access-Control-Expose-Headers - tells browser what http response headers should be hidden from code running in the browser
The Access-Control-Allow-Origin specifies whether the origin in the request is permitted to access this server (note that this is not url-specific, ie applies to all URLs in that domain). The Access-Control-Allow-Origin response does not “leak” the list of all allowed origins; it just confirms that the requested origin is allowed or “leaks” the fact that all origins are allowed. Allowing all origins for a site where login credentials or session ids are stored as cookies is obviously a bad idea, as it bypasses cross-origin protections.
The preflight request indicates what the following “complex” request will be doing, via request headers Access-Control-Request-Method (eg “POST”) and Access-Control-Request-Headers (eg “content-type”). The response will echo all allowed methods and headers, not just the ones matching the OPTIONS request. Note that the “standard” headers do not need to be listed here. However “content-type” should be listed, as that is permitted by default only for some content types; listing it as an allowed header permits it for all content types. A response to a preflight request may be cached by the browser, up to the value specified by header Access-Control-Max-Age.
In general, CSRF attacks via “complex” requests are secured by the cross-origin-policy and CORS; no CSRF tokens are needed. This includes all JSON-based requests. CSRF attacks via “simple” requests need to be protected by CSRF tokens; this is possible because forms can include a field with the magic token and GET requests are generally not considered dangerous.
Note that the “origin” header is not set on “302” cross-site redirects: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
CORS as “site-specific permissions” mechanism
CORS headers returned from the server can be used to grant some sites specific rights. Per-URL control is not possible, but it is possible to allow specific HTTP verbs or HTTP headers based on the requesting site. The official CORS spec does not recommend using CORS in this way, and suggests that it would be better to have each request pass an OAuth token explicitly, either in header or in form field. The back-end can then ignore origins completely, and just check the token associated with the request.
Cross-origin Policy and Cookies
- OWASP: CSRF Prevention Cheat Sheet - the most trustworthy reference on CSRF
- Wikipedia: Same Origin Policy - Same origin policz overview
- HTML5Rocks: CORS Tutorial – great tutorial, I just found the “big picture” missing.
- w3.org: CORS - CORS overview from w3c
- w3.org: CORS Specification - the official CORS spec
- Mozilla: CORS CORS overview from Mozilla
- CGISecurity: CSRF FAQ
- Microsoft: Origin header not set for POST
- Firefox: Origin header not set for POST
- Firefox: Implementing the Origin header - from 2009
See here for further reading:
- Smerity: Http referer - why the referer header is sometimes missing
- Barth et. al. at Stanford: Robust Defenses for CSRF - academic paper on CSRF from Stanford researchers
- van Waveren at JDriven: Stateless Spring Security part 1: Stateless CSRF protection
Appendix: CORS and Spring Framework
The Spring framework for Java is popular for building server applications that provide HTTP endpoints (traditional and REST). This section includes some spring-specific advice with regards to implementing CSRF protection server-side and defining CORS headers. This appendix is somewhat less organised than the main part of the article, being notes and code snippets from a project I was working on - they might be useful to you or not.
spring-security provides many APIs for configuring security-related features for HTTP request and response handling, including functionality for defining the CORS headers to be returned for browser requests for “OPTIONS /”.
CSRF protection is built in to Spring core code which is not part of the
spring-security module. In particular, it is present in class
AbstractMappingHandler which is the base for various “request handler mapping” classes, eg
Module spring-security has an optional “global”
CorsFilter (enabled by calling method
HttpSecurity.cors()). When enabled, every request which contains an “origin” header is validated:
- origin must match a list of allowed origins
- http method must be one of the permitted options
- headers must all be in the permitted set
and then appropriate cors-related headers are added to the response.
The CorsFilter is not suitable for applications which have different CORS rules for different resources (urls) - but that is a fairly rare case; normally CORS rules are the same for all resources in an app.
CorsFilter uses class
DefaultCorsProcessor to do the actual CORS checks. It does not seem to be a good idea to try to customise CORS processing by subclassing
DefaultCorsProcessor and using
DefaultCorsProcessor is also instantiated from class
AbstractHandlerMapping which is subclassed many times, and instances of these many subclasses are created at runtime, ie there are more than half-a-dozen distinct instances of
DefaultCorsProcessor at runtime; ensuring all are consistently overridden would be difficult. These subclasses include
SimpleUrlHandlerMapping (two instances),
WebMvcAutoConfiguration$WelcomePageHandlerMapping. Fortunately all calls to these classes pass the same
CorsConfigurationSource instance as parameter.
Implementing CSRF protection via Referer/Origin checks
As described in the CSRF defenses section above, some types of server which do not need to support old browsers and ugly http proxies which strip referer and origin headers. In this case, it is sufficient to simply add code to the app to verify that one of headers
referer is present, and that the value is one of the supported origins. The code below implements this for a spring-based application.
The code below also supports having a “development front-end server” serving front-end code which then makes REST requests to a separate server. It does this by configuring appropriate CORS headers, and enhancing the origin/referer checks above to allow multiple origins (including the front-end server in this case).
TODO: the code snippet associated with this comment appears to have been lost..
/** * Ensure that either http header "origin" or "referer" is valid; reject the request otherwise. * <p> * This test is not possible for some webapps due to users with weird browsers or behind weird proxies; such webapps therefore need more * complex solutions such as "sync token" or "double submit cookie". However for most projects there are no such problems and so * this test provides full CSRF protection with little complexity. * </p> */