Web application security with CORS and CSP

Cross Origin Resource Sharing (CORS) and Content Security Policy (CSP) are HTTP response headers which when implemented help to improve the security of a web application. Both security headers allow application owners to whitelist the origin of resources in their web application. Both seems to work the same but there are differences.

To understand Cross Origin Resource Sharing (CORS) and Content Security Policy (CSP) headers we need to understand SameOriginPolicy first.

Same-origin policy

The same-origin policy is a browser security feature that restricts how documents and scripts on one origin can interact with resources on another origin.

An origin is defined by the scheme (also known as the protocol, for example HTTP or HTTPS), port (if it is specified), and host. When all three are the same for two URLs, they are considered same-origin.

For example, http://example.com:5000/foo is the same origin as http://example.com:5000/bar but not https://example.com:5000/status because the scheme is different.

What is permitted and blocked by the same-origin policy

A browser can load and display resources from multiple sites at once. You might have multiple tabs open at the same time, or a site could embed multiple iframes from different sites. If there is no restriction on interactions between these resources, and a script is compromised by an attacker, the script could expose everything in a user's browser.

The same-origin policy controls interactions between two different origins. These interactions are typically placed into three categories:

  1. Cross-origin writes are typically allowed. Examples are links, redirects, and form submissions. Some HTTP requests require preflight.
  2. Cross-origin embedding is typically allowed.
  3. Cross-origin reads are typically disallowed, but read access is often leaked by embedding. For example, you can read the dimensions of an embedded image, the actions of an embedded script, or the availability of an embedded resource.

The same-origin policy has some historical exceptions as you can see and if the web could be designed from scratch, these exceptions wouldn't exist. Unfortunately, by the time the web community realized the key benefits of a strict same-origin policy, the web was already relying on these exceptions.

Cross Origin Resource Sharing (CORS)

Cross-Origin Resource Sharing (CORS) is a protocol that enables scripts running on a browser client to interact with resources from a different origin. This is useful because, thanks to the same-origin policy followed by XMLHttpRequest and Fetch API, JavaScript can only make calls to URLs that live on the same origin as the location where the script is running.

Most of the time, a script running in the user's browser would only ever need to access resources on the same origin (think about API calls to the same backend that served the JavaScript code in the first place). So the fact that JavaScript can't normally access resources on other origins is a good thing for security.

However, there are legitimate scenarios where cross-origin access is desirable or even necessary. For example, if you're running a React SPA that makes requests to an API backend running on a different origins.

On the server side, when a server has been configured correctly to allow cross-origin resource sharing, some special headers will be included. Their presence can be used to determine that a request supports CORS. Web browsers can use these headers to determine whether a request call should continue or fail.

There are a few headers that can be set, but the primary one that determines who can access a resource is Access-Control-Allow-Origin. This header specifies which origins can access the resource. For example, to allow access from any origin, you can set the response header as follows:

Access-Control-Allow-Origin: *

Or it can be narrowed down to a specific origin:

Access-Control-Allow-Origin: https://example.com

CORS is normally used for "anonymous requests" where the request does not identify the requester. If you want to send cookies (which could identify the sender) using for example the Fetch API, you need to add additional headers to the request and response.

fetch('https://example.com', {
  mode: 'cors',
  credentials: 'include'
})

The response headers from the server should now look like:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

Note: The Access-Control-Allow-Origin should have an origin set in the response header, and the Access-Control-Allow-Credentials should be set to true.

Understanding CORS request types

There are two types of CORS request: "simple" requests and "preflight" requests. It's the browser that determines which is used. As the developer, you don't normally need to care about this when you are constructing requests to be sent to a server. However, you may see the different types of requests appear in your network log, and since it may have a performance impact on your application it may benefit you to know why and when these requests are sent.

The browser deems the request to be a "simple" request when the request itself meets a certain set of requirements:

  • One of these methods is used: GET, POST, or HEAD.
  • A CORS safe-listed header is used.
  • When using the Content-Type header, only the following values are allowed: application/x-www-form-urlencoded, multipart/form-data, or text/plain.
  • No event listeners are registered on any XMLHttpRequestUpload object.
  • No ReadableStream object is used in the request.

The request is allowed to continue as normal if it meets these criteria, and the Access-Control-Allow-Origin header is checked when the response is returned.

If a request does not meet the criteria for a "simple" request, the browser will instead make an automatic preflight request using the OPTIONS method. This call is used to determine the exact CORS capabilities of the server, which is in turn used to determine whether the intended CORS protocol is understood. If the result of the OPTIONS call dictates that the request cannot be made, the actual request to the server will not be executed.

The preflight request sets the mode as OPTIONS and sets a couple of headers to describe the actual request that is to follow:

  • Access-Control-Request-Method: The intended method of the request (e.g., GET or POST).
  • Access-Control-Request-Headers: An indication of the custom headers that will be sent with the request.
  • Origin: The usual origin header that contains the script's current origin.

An example of such a request might look like this:

OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Accept

This request basically says, I would like to make a DELETE request with the Content-Type and Accept headers on https://example.com - is that possible?

The server will include some Access-Control-* headers within the response to indicate whether the request that follows will be allowed or not. These include:

  • Access-Control-Allow-Origin: The origin that is allowed to make the request, or * if a request can be made from any origin.
  • Access-Control-Allow-Methods: A comma-separated list of HTTP methods that are allowed.
  • Access-Control-Allow-Headers: A comma-separated list of the custom headers that are allowed to be sent.
  • Access-Control-Max-Age: The maximum duration that the response to the preflight request can be cached before another call is made.

A response to the earlier example might look like this:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: Content-Type, Accept
Access-Control-Max-Age: 120
Vary: Access-Control-Request-Headers
Content-Length: 0
Connection: keep-alive

The response would then be examined by the browser to decide whether to continue with the request or to abandon it.

The Access-Control-Allow-Origin header, in this case, allows the request to be made from any origin, while the Access-Control-Allow-Methods header describes only the accepted HTTP methods. If a given HTTP method is not accepted, it will not appear in this list.

In this example, Access-Control-Allow-Headers echos back the headers that were asked for in the OPTIONS request. This indicates that all the requested headers are allowed to be sent. If for example, the server doesn't allow the Accept header, then that header would be omitted from the response, and the browser would reject the call.

Content Security Policy (CSP)

Content Security Policy (CSP) header is used to define what content can run on its own domain. It is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement to distribution of malware.

To enable CSP, you need to configure your web server to return the Content-Security-Policy HTTP header.

Alternatively, the element can be used to configure a policy, for example:

By injecting the Content-Security-Policy (CSP) headers from the server, the browser is aware and capable of protecting the user from dynamic calls that will load content into the page currently being visited.

Mitigating cross site scripting

A primary goal of CSP is to mitigate and report XSS attacks. XSS attacks exploit the browser's trust of the content received from the server. Malicious scripts are executed by the victim's browser because the browser trusts the source of the content, even when it's not coming from where it seems to be coming from.

CSP makes it possible for server administrators to reduce or eliminate the vectors by which XSS can occur by specifying the domains that the browser should consider to be valid sources of executable scripts. A CSP compatible browser will then only execute scripts loaded in source files received from those allowlisted domains, ignoring all other script (including inline scripts and event-handling HTML attributes).

As an ultimate form of protection, sites that want to never allow scripts to be executed can opt to globally disallow script execution.

Mitigating packet sniffing attacks

In addition to restricting the domains from which content can be loaded, the server can specify which protocols are allowed to be used; for example (and ideally, from a security standpoint), a server can specify that all content must be loaded using HTTPS. A complete data transmission security strategy includes not only enforcing HTTPS for data transfer, but also marking all cookies with the secure attribute and providing automatic redirects from HTTP pages to their HTTPS counterparts. Sites may also use the Strict-Transport-Security HTTP header to ensure that browsers connect to them only over an encrypted channel.

Configure your CSP policy

Configuring Content Security Policy involves adding the Content-Security-Policy HTTP header to a web page and giving it values to control what resources the user agent is allowed to load for that page. For example, a page that uploads and displays images could allow images from anywhere, but restrict a form action to a specific endpoint. A properly designed Content Security Policy helps protect a page against a cross site scripting attack.

You can use the Content-Security-Policy HTTP header to specify your policy, like this:

Content-Security-Policy: policy

A policy is described using a series of policy directives, each of which describes the policy for a certain resource type or policy area. Your policy should include a default-src policy directive, which is a fallback for other resource types when they don't have policies of their own (for a complete list, see the description of the default-src directive).

A policy needs to include a default-src or script-src directive to prevent inline scripts from running, as well as blocking the use of eval().

A policy needs to include a default-src or style-src directive to restrict inline styles from being applied from a <style> element, or a style attribute.

There are specific directives for a wide variety of types of items, so that each type can have its own policy, including fonts, frames, images, audio and video media, scripts, and workers.

Recommended measures for CSP

The XSS Cheat Sheet is an old but representative cross-section of the methods an attacker might use to violate this trust by injecting malicious code. If an attacker successfully injects any code at all, it's pretty much game over: user session data is compromised and information that should be kept secret is exfiltrated.

The following overview highlights a defense that can significantly reduce the risk and impact of XSS attacks in modern browsers.

Source allow list

The issue exploited by XSS attacks is the browser's inability to distinguish between script that's part of your application and script that's been maliciously injected by a third party.

For example, if you trust some API to deliver valid code, you should define a policy that only allows script to execute when it comes from the trusted sources:

Content-Security-Policy: script-src 'self' https://apis.example.com

We used script-src which is a directive that controls a set of script-related privileges for a specific page. We've specified 'self' as one valid source of script and https://apis.example.com as another. The browser dutifully downloads and executes JavaScript from apis.example.com over HTTPS as well as from the current page's origin.

While script resources are the most obvious security risks, CSP provides a rich set of policy directives that enable fairly granular control over the resources that a page is allowed to load.

Policies that applies to a wide variety of resources

Let's walk through the rest of the resource directives. The list below represents the state of the directives as of level 2 specification. A level 3 spec has been published, but is largely unimplemented in the major browsers.

  • base-uri, restricts the URLs that can appear in a page's <base> element.
  • child-src, lists the URLs for workers and embedded frame contents. For example: child-src https://youtube.com would enable embedding videos from YouTube but not from other origins.
  • connect-src, limits the origins that you can connect to (via XHR, WebSockets, and EventSource).
  • font-src, specifies the origins that can serve web fonts.
  • form-action, lists valid endpoints for submission from <form> tags.
  • frame-ancestors, specifies the sources that can embed the current page. This directive applies to <frame>, <iframe>, <embed>, and <applet> tags. This directive can't be used in <meta> tags and applies only to non-HTML resources.
  • frame-src, was deprecated in level 2, but is restored in level 3. If not present it still falls back to child-src as before.
  • img-src, defines the origins from which images can be loaded.
  • media-src, restricts the origins allowed to deliver video and audio.
  • object-src, allows control over Flash and other plugins.
  • plugin-types, limits the kinds of plugins a page may invoke.
  • report-uri, specifies a URL where a browser will send reports when a content security policy is violated. This directive can't be used in <meta> tags.
  • style-src, is script-src's counterpart for stylesheets.
  • upgrade-insecure-requests, instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large numbers of old URL's that need to be rewritten.
  • worker-src, is a CSP Level 3 directive that restricts the URLs that may be loaded as a worker, shared worker, or service worker.

By default, directives are wide open. If you don't set a specific policy for a directive, let's say font-src, then that directive behaves by default as though you'd specified * as the valid source (for example, you could load fonts from anywhere, without restriction).

You can override this default behavior by specifying a default-src directive. This directive defines the defaults for most directives that you leave unspecified. Generally, this applies to any directive that ends with -src. If default-src is set to https://example.com, and you fail to specify a font-src directive, then you can load fonts from https://example.com, and nowhere else. We specified only script-src in our earlier examples, which means that images, fonts, and so on can be loaded from any origin.

The following directives don't use default-src as a fallback. Remember that failing to set them is the same as allowing anything.

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

You can use as many or as few of these directives as makes sense for your specific application, simply listing each in the HTTP header, separating directives with semicolons. Make sure that you list all required resources of a specific type in a single directive. If you wrote something like script-src https://host1.com; script-src https://host2.com the second directive would simply be ignored. Something like the following would correctly specify both origins as valid:

script-src https://host1.com https://host2.com

If, for example, you have an application that loads all of its resources from a content delivery network (say, https://cdn.example.net), and know that you don't need any framed content or plugins, then your policy might look something like the following:

Content-Security-Policy: default-src https://cdn.example.net; child-src 'none'; object-src 'none'

Implementation details of CSP

You will see X-WebKit-CSP and X-Content-Security-Policy headers in various tutorials on the web. You should ignore these prefixed headers. Modern browsers, except IE, support the un-prefixed Content-Security-Policy header.

Regardless of the header you use, policy is defined on a page-by-page basis: you'll need to send the HTTP header along with every response that you'd like to ensure is protected. This provides a lot of flexibility, as you can fine-tune the policy for specific pages based on their specific needs. Perhaps one set of pages in your site has a +1 button, while others don't: you could allow the button code to be loaded only when necessary.

The source list in each directive is flexible. You can specify sources by scheme (data:, https:), or ranging in specificity from hostname-only (example.com), which matches any origin on that host: any scheme, any port to a fully qualified URI (https://example.com:443), which matches only HTTPS, only example.com, and only port 443). Wildcards are accepted, but only as a scheme, a port, or in the leftmost position of the hostname: ://.example.com:* would match all subdomains of example.com (but not example.com itself), using any scheme, on any port.

The source list also accepts four keywords:

  • 'none', as you might expect, matches nothing.
  • 'self' matches the current origin, but not its subdomains.
  • 'unsafe-inline' allows inline JavaScript and CSS.
  • 'unsafe-eval' allows text-to-JavaScript mechanisms like eval.

These keywords require single-quotes. For example, script-src 'self' (with quotes) authorizes the execution of JavaScript from the current host; script-src self (no quotes) allows JavaScript from a server named "self" (and not from the current host), which probably isn't what you meant.

Sandboxing

There's one more directive worth talking about: sandbox. It's a bit different from the others we've looked at, as it places restrictions on actions that the page can take rather than on resources that the page can load. If the sandbox directive is present, the page is treated as though it was loaded inside of an <iframe> with a sandbox attribute. This can have a wide range of effects on the page: forcing the page into a unique origin, and preventing form submission, among others. It's a bit beyond the scope of this article, but you can find full details on valid sandboxing attributes in the "Sandboxing" section of the HTML5 specification

The meta tag

CSPs preferred delivery mechanism is an HTTP header. It can be useful, however, to set a policy on a page directly in the markup. Do that using a <meta> tag with a http-equiv attribute:

<meta http-equiv="Content-Security-Policy" content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'">

This can't be used for frame-ancestors, report-uri, or sandbox.

Inline code is considered harmful

It should be clear that CSP is based on allowlist origins, as that's an unambiguous way of instructing the browser to treat specific sets of resources as acceptable and to reject the rest. Origin-based allowlists don't, however, solve the biggest threat posed by XSS attacks: inline script injection. If an attacker can inject a script tag that directly contains some malicious payload <script>sendMyDataToEvilDotCom()</script>, the browser has no mechanism by which to distinguish it from a legitimate inline script tag. CSP solves this problem by banning inline script entirely: it's the only way to be sure.

This ban includes not only scripts embedded directly in script tags, but also inline event handlers and javascript: URLs. You'll need to move the content of script tags into an external file, and replace javascript: URLs and <a ... onclick="[JAVASCRIPT]"> with appropriate addEventListener() calls. For example, you might rewrite the following from:

<script>
  function doAmazingThings() {
    alert('YOU AM AMAZING!')
  }
</script>
<button onclick='doAmazingThings();'>Am I amazing?</button>

to something more like:

<!-- amazing.html -->
<script src='amazing.js'></script>
<button id='amazing'>Am I amazing?</button>
// amazing.js
function doAmazingThings() {
alert('YOU AM AMAZING!');
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('amazing')
.addEventListener('click', doAmazingThings);
});

The rewritten code has a number of advantages above and beyond working well with CSP; it's already best practice, regardless of your use of CSP. Inline JavaScript mixes structure and behavior in exactly the way you shouldn't. External resources are easier for browsers to cache, more understandable for developers, and conducive to compilation and minification. You'll write better code if you do the work to move code into external resources.

Inline style is treated in the same way: both the style attribute and style tags should be consolidated into external stylesheets to protect against a variety of surprisingly clever data exfiltration methods that CSS enables.

If you must have inline script and style, you can enable it by adding 'unsafe-inline' as an allowed source in a script-src or style-src directive. You can also use a nonce or a hash (see below), but you really shouldn't. Banning inline script is the biggest security win CSP provides, and banning inline style likewise hardens your application. It's a little bit of effort up front to ensure that things work correctly after moving all the code out-of-line, but that's a tradeoff that's well worth making.

If you absolutely must use it, CSP Level 2 offers backward compatibility for inline scripts by allowing you to add specific inline scripts to the allowlist using either a cryptographic nonce (number used once) or a hash. Although this may be cumbersome, it is useful in a pinch.

To use a nonce, give your script tag a nonce attribute. Its value must match one in the list of trusted sources. For example:

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
  // Some inline code I can't remove yet, but need to asap.
</script>

Now, add the nonce to your script-src directive appended to the nonce- keyword.

Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'

Remember that nonces must be regenerated for every page request and they must be unguessable.

Hashes work in much the same way. Instead of adding code to the script tag, create a SHA hash of the script itself and add it to the script-src directive. For example, let's say your page contained this:

<script>alert('Hello, world.')</script>

Your policy would contain this:

Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

There are a few things to note here. The sha*- prefix specifies the algorithm that generates the hash. In the example above, sha256- is used. CSP also supports sha384- and sha512-. When generating the hash, do not include the <script> tags. Also capitalization and whitespace matter, including leading or trailing whitespace.

A Google search on generating SHA hashes will lead you to solutions in any number of languages. Using Chrome 40 or later, you can open DevTools and then reload your page. The Console tab will contain error messages with the correct sha256 hash for each of your inline scripts.

Eval too

Even when an attacker can't inject script directly, they might be able to trick your application into converting otherwise inert text into executable JavaScript and executing it on their behalf. eval(), new Function(), setTimeout([string], ...), and setInterval([string], ...) are all vectors through which injected text might end up executing something unexpectedly malicious. CSP's default response to this risk is to completely block all of these vectors.

This has more than a few impacts on the way you build applications:

  • You must parse JSON via the built-in JSON.parse, rather than relying on eval. Native JSON operations are available in every browser since IE8, and they're completely safe.
  • Rewrite any setTimeout or setInterval calls you're currently making with inline functions rather than strings. For example:
setTimeout("document.querySelector('a').style.display = 'none';", 10);

would be better written as:

setTimeout(function () {
   document.querySelector('a').style.display = 'none';
}, 10);
  • Avoid inline templating at runtime: Many templating libraries use new Function() liberally to speed up template generation at runtime. It's a nifty application of dynamic programming, but comes at the risk of evaluating malicious text. Some frameworks support CSP out of the box, falling back to a robust parser in the absence of eval.

However, a better choice would be a templating language that offers precompilation (Handlebars does, for instance). Precompiling your templates can make the user experience even faster than the fastest runtime implementation, and it's safer too. If eval and its text-to-JavaScript brethren are essential to your application, you can enable them by adding 'unsafe-eval' as an allowed source in a script-src directive, but we strongly discourage this. Banning the ability to execute strings makes it much more difficult for an attacker to execute unauthorized code on your site.

Reporting

CSP's ability to block untrusted resources client-side is a huge win for your users, but it would be quite helpful to have some sort of notification sent back to the server so that you can identify and squash any bugs that allow malicious injection in the first place. To this end, you can instruct the browser to POST JSON-formatted violation reports to a location specified in a report-uri directive.

Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

Those reports will look something like the following:

{
   "csp-report": {
   "document-uri": "http://example.org/page.html",
   "referrer": "http://evil.example.com/",
   "blocked-uri": "http://evil.example.com/evil.js",
   "violated-directive": "script-src 'self' https://apis.google.com",
   "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
   }
}

This contains a good chunk of information that will help you track down the specific cause of the violation, including the page on which the violation occurred (document-uri), that page's referrer (note that unlike the HTTP header field, the key is not misspelled), the resource that violated the page's policy (blocked-uri), the specific directive it violated (violated-directive), and the page's complete policy (original-policy).

Report only

If you're just starting out with CSP, it makes sense to evaluate the current state of your application before rolling out a draconian policy to your users. As a stepping stone to a complete deployment, you can ask the browser to monitor a policy, reporting violations but not enforcing the restrictions. Instead of sending a Content-Security-Policy header, send a Content-Security-Policy-Report-Only header.

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

The policy specified in report-only mode won't block restricted resources, but it will send violation reports to the location you specify. You can even send both headers, enforcing one policy while monitoring another. This is a great way to evaluate the effect of changes to your application's CSP: turn on reporting for a new policy, monitor the violation reports and fix any bugs that turn up; when you're satisfied with its effect, start enforcing the new policy.

Usage in the real world

CSP 1 is quite usable in Chrome, Safari, and Firefox, but has very limited support in IE 10. CSP Level 2 has been available in Chrome since version 40. You can see more at caniuse. Massive sites like Twitter and Facebook have deployed the header, and the standard is very much ready for you to start deploying on your own sites.

References


Published: 2021-04-30
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB