CSRF, CORS, and HTTP Security headers Demystified
With an increasing number of breaches, intrusions, and data thefts, securing a web application is extremely important.
On the other hand, programmers often do not have a strong grasp of how attacks work and how to mitigate them. This post attempts to close that gap a little.
Cross-Site Request Forgery is an attack where a third party forces a user to execute actions against a site where they are currently logged in.
Here is an example illustrating how this works:
- You visit evil.com
- evil.com has a hidden form that submits on page load to mybank.com/transfer-funds. Since you are logged in to mybank.com, this request is made with your mybank.com cookies and will silently initiate a money transfer out of your account.
- Since 'evil.com' and 'mybank.com' are different origins, the browser refuses to provide the response to evil.com (because of CORS), but the attacker doesn't care, the money's already been transferred.
Now if mybank.com implements CSRF protection correctly:
- Each time mybank.com serves a form to a user, it generates a CSRF token and inserts it into a hidden field in the form
- If a POST request is received, it checks the CSRF token against its database - if this is present and valid then the request goes through. If the CSRF token is missing or incorrect it is rejected.
CSRF attacks target state-changing requests and not the direct theft of data because the attacker does not see the response of the forged request. The CSRF token provides security by being a stateless token that is never stored in the cookies or permanent storage - doing so would defeat the purpose of the token.
CSRF protection support needs to be added to your application code and cannot be added at the proxy server layer (eg. in Nginx). A detailed look at CSRF from OWASP
It is good practice to always use the SameSite directive with cookies as this provides protection against CSRF attacks.
Cross-Origin Request Sharing only applies in a browser context and is a security mechanism to allow one origin to make a request to another origin. All browsers follow the Single Origin Policy, meaning by default scripts cannot make requests to other origins - but if the server provides properly configured CORS headers this policy can be selectively relaxed. Thus CORS is a way of selectively loosening security and not of tightening it.
When a website makes an XHR request to another origin, the browser initiates a preflight OPTIONS request first - and the original request is only made if the server responds to this preflight with a list of allowed origins, and this list contains the origin of the current page.
Note that CORS preflight requests are not made for GET HEAD POST requests with default headers.
Some key headers sent as a response to an OPTIONS request:
- access-control-allow-credentials: If set, cookies are sent by the browser
- access-control-allow-origin: The list of origins allowed to make requests, or '*' to allow anyone to make requests. If access-control-allow-credentials is set then this cannot be set to '*' or the browser will reject the request anyway
- access-control-allow-methods: The list of HTTP methods allowed to communicate - POST, PUT, etc.
The reason access-control-allow-origin cannot be '*' when access-control-allow-credentials is set is to prevent developers taking the shortcut of adding a * and then forgetting about it altogether - this behaviour forces developers to think about how their API is going to be consumed.
CORS is often a code smell of sorts - for example, a common use of CORS is to allow multiple properties access to one API, like mybank.com being accessed by mybankpersonal.com and mybankbusiness.com - and in these cases, CORS can be avoided altogether with the use of an API gateway. If all web pages are served from the same domain (mybank.com), no CORS headers need to be set and the browser can apply the full Same Origin Policy and provide maximum security. After all, CORS only serves to selectively decrease security not increase it.
On the other hand, using CORS is unavoidable if you are providing an API for third parties to consume from a browser (eg. after going through an Oauth process).
Of course, CORS is irrelevant for requests made outside the web browser, eg. with Curl or with server-to-server communication.
Cross-Site Scripting is an attack where an attacker injects a client-side script into a web page. XSS can be used to bypass both the same-origin policy and CSRF protections.
This is done either by compromising the server (eg. by using a known exploit) or by leveraging poorly-sanitised user inputs to add a script to the website, which is triggered whenever a user visits the website.
This is the most commonly exploited vulnerability and can be fixed with careful sanitisation and encoding for display of all user input - even things like email addresses could be vectors for script injection. For instance.,
is a valid email address by RFC 5321!
XSS is best sanitised on the output side, this way raw data is stored in the database as input by the user and is then escaped to an appropriate format at the time it is sent out again - for example, if the result is to be sent as part of HTML page the escaping will be different from if the result is to be sent as JSON or included in a script tag in the HTML page. The escaping should be appropriate to the output format. Additionally, if content is escaped before insert into the database it won't be secure against future changes to business requirements - for example, if the business decides to serve data through a REST API when it was previously only used in a HTML page, the encoding would now be irrelevant.
Care must be taken to ensure the sanitisation is done safely - it may be possible to easily bypass sanitisation and several such attacks exist. For instance, it may be possible to simply add text such as:
where the sanitisation filter strips out the script tag, but in doing so causes the split script tag to come together and the attack to succeed.
OWASP has a comprehensive section on XSS sanitisation bypasses.
The OWASP cheat sheet on XSS prevention is a good starting point for securing an application against XSS.
Content Security Policy is an added layer of security to detect and mitigate XSS attacks. By specifying the domains that should be considered valid sources for scripts, a web browser will only execute scripts loaded from these white listed domains. CSP can specify allowed origins for all dynamic origins, including scripts, stylesheets, images, fonts, objects, media (audio, video), frames, and even form-actions.
CSP is set through the Content-Security-Policy HTTP header.
The difference from CORS is that CORS prevents a third party from accessing a server, while CSP prevents a website itself from loading content from a third party, as a defence against XSS.
CSP is not a silver bullet against XSS but it helps. Ultimately XSS needs to be prevented with careful application design and effective user input sanitisation, both on the frontend and the backend.
Of all the mitigations on this page, CSP is the most complicated because of the depth of its options and also likely the hardest to implement because every time a front-end dev adds a new CDN - for fonts, scripts, whatever - that CDN needs to be added to the CSP header. Without a well-designed test setup, this could work fine in dev but break when pushed to production.
CSP is complicated enough to require more than a few paragraphs - a full page would do it more justice.
The Content Security Policy web site describes how to add CSP support to your web server and Google's handy test tool can help test these policies.
HTTP Strict Transport Security protects against protocol downgrade attacks and cookie hijacking by allowing a server to declare that browsers should only interact with it using HTTPS connections and never HTTP; and the browser should always convert all attempts to access the site using HTTP to HTTPS instead, eg. by rewriting all links to use HTTPS instead of HTTP.
This guards against eavesdropping and man-in-the-middle attacks that work by transparently converting a https connection into plain http.
For example, you might connect to a free wifi service at the airport to access your bank website, but the access point is actually a hacker's laptop that performs a MITM attack and redirects you to a clone of the bank's site. HSTS provides security in this scenario as long as you have accessed your bank's web site at least once over HTTPS (and the bank uses HSTS), the browser will reject this request outright.
This page has details on configuring HSTS for Nginx.
HTTP Public Key Pinning allows HTTPS websites to resist impersonation using fraudulently obtained SSL certificates by providing the browser a set of public keys which are the only ones to be trusted for future connections to the same origin.
This provides security against a compromised certificate authority as has happened in the past with Comodo.
In the airport scenario above, if the hacker also had a fraudulently obtained certificate for the bank, HSTS alone would not protect you - but with HPKP, even this attack can be mitigated. This is a Trust-on-first-use technique.
HPKP will eventually be superseded by the Expect-CT header. More on HPKP.
This is a fairly standard header and by far the oldest on the page, yet is one of the most important (and easiest) to configure correctly.
This can be done by setting a few directives.
- The SameSite: Strict directive protects against CSRF attacks by stripping cookies from all cross-domain requests.
- The Secure directive ensures the cookies are only ever used in a https connection - thus providing additional security over what is provided by HPKP and HSTS.
You will as a rule want to set all three. More on Set-Cookie
These are generally less important to set but good to be aware of.
- X-XSS Protection: This header is outdated and possibly harmful and CSP does a better job of providing protections. The main job of this in Chrome is to trigger the XSS auditor which has been deprecated for being insecure.
- X-Frame-Options: This header indicates whether the site should be allowed to be displayed within an iFrame. This has been superseded by CSP's frame-ancestors option which is better supported in modern browsers.
- X-Content-Type-Options: This prevents the browser from attempting to guess the type of content if the Content-Type header is not set. This can prevent XSS exploits for websites that allow users to upload and share files.
- X-Download Options: Again useful in scenarios involving user file uploads, when set to 'noopen' this forces the browser to download a file instead of attempting to display it in the browser.
- Referrer-Policy: When this is set to 'no-referrer' the browser does not send the referrer when making a request and is useful for privacy. More on referrer-policy.
Overall, as the web grows in terms of features and complexity, the attack surface also grows correspondingly large. A properly configured web server and a well designed application with effective use of HTTPS (eg. with Let's Encrypt) can provide a good degree of protection.
As an added bonus, many of the mitigations on this page can be applied at the proxy server (CSP, HSTS, HPKP) or network level (better server proxying to remove the need for CORS), and only the CSRF and XSS protections really need to be added to the application. of course, the mitigations on this page are only a small subset of what is possible.
Some useful resources to implement these:
- Test your site's security headers
- Adding security headers automatically with Cloudflare workers
- CSP evaluator to test policies
That said, the application is likely the biggest source of security threats anyway, most of the OWASP top ten have to deal with application programming bugs - highlighting the importance of good design and application architecture.