NOTE: This article was synced via RSS from my blog. In the process, some of the image formatting looks to have been messed up. If you have any trouble reading this, please view it on my blog while I work out the image formatting issues.
In my experience, many developers are not aware of the Same Origin Policy nor of the Content Security Policy, or at least were not aware of more than one or two of the directives CSP supports. Let’s lay out what these terms are and how they drastically improve the security of the web.
Mozilla’s MDN docs describe the following:
Restricts how a document or script loaded from one origin can interact with a resource from another origin. It helps to isolate potentially malicious documents, reducing possible attack vectors.
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.
Great! Mystery solved, we can go home. You are now enlightened.
No? Ok, let’s break these terms down.
Same Origin Policy
The Same Origin Policy (SOP) is really the base of the web security model. Under this model, each website is given its own origin sandboxed from the rest of the internet. These isolated origins cannot read or modify data from another origin. This means that resources on one origin, such as reddit.com, cannot modify or read data from another origin, account.yourbank.com. SOP is enforced by the browser. Its restrictions can be relaxed via a server response header, which we will discuss in a moment.
SOP will group resources under the same origin if they have the same three properties:
The following interactions are prevented due to SOP:
Since mysite.com and malicious.com are clearly different hosts, SOP blocks data sharing between these separate origins.
Even though we are now under the same host, HTTPS and HTTP are different protocols, so SOP prevents data interaction and separates these two origins.
Since 443 and 4443 are different ports, SOP blocks data interaction and separates these two origins.
Similar to the first example, subdomains are considered separate hosts. SOP will block data interaction between two subdomains.
Understanding the Same Origin Policy
Of course, this might beg the question, ‘but I need to access files from another origin! I have a CDN I pull jQuery from, I have analytics I need from Google, NewRelic is monitoring my site, and I have a subdomain I need to modify!’
Enabling Subdomains Through SOP
In order for two subdomains of the same superdomain to share data, you must redefine the root domain for those subdomains. For example, if a.mysite.com and b.mysite.com wanted to read or modify data from each other, SOP would prevent that behavior. To change this, a.mysite.com and b.mysite.com must both run a script that sets:
document.domain = "mysite.com"
Since mysite.com is a superdomain of a.mysite.com , this setting is allowed (same for b.mysite.com ). You are forbidden from changing your document’s domain to one that you do not control. Running
document.domain = "google.com" on the site a.mysite.com will fail.
If both a.mysite.com and b.mysite.com change their document’s domain to the same superdomain, suddenly both share the same host, port, and protocol, and SOP will allow data sharing.
Note that the site’s port is held separately by the browser. Any time a call is made to set the value of
document.domain, the domain’s port value for SOP is set to
null. This is done to prevent a situation where a.mysite.com wants to modify data on mysite.com , so a.mysite.com changes it’s
mysite.com. The host value now matches, but mysite.com may not want to give a subdomain permission to read or modify its content. Since a call to set
document.domain was made on a.mysite.com , that subdomain’s port value was set to
null. If mysite.com does not similarly make a call to set
document.domain = "mysite.com" (yes, even though it’s already
mysite.com), then the port values for a.mysite.com (
null) and mysite.com (80 or 443, presumably) will not match, ensuring SOP continues to prevent data sharing.
So what does SOP allow?
SOP allows pretty much all resources executed on your site to run cross-origin writes. This means that:
<link rel="stylesheet" href="...">
can write data to your page. These resources cannot typically read data from your page due to the SOP, only write to it. A Content Security Policy will protect your site from the actions of these resources, which we will discuss momentarily.
Note that scripts executed from pages with an
Let's be clear here. CORS is an anti-security mechanism.
Cross-Origin Resource Sharing
Let's be clear here. CORS is an anti-security mechanism. The security control is Same Origin Policy, implemented by default by your browser. By setting a CORS header, you are disabling security controls on your site to serve certain content. This is not necessarily a bad thing and there are legitimate reasons to relax SOP in order for your website to function, but that context is important. I frequently see developers asking how to set up "the CORS security control." You don't. You only disable it in increments.
A CORS policy is set on the
Access-Control-Allow-Origin header. In this header, you list a series of space-separated URLs that are allowed to bypass SOP restrictions (remember, this is for those whitelisted sites to read or modify data on your site, not the other way around). I often see the insecure
Access-Control-Allow-Origin: * set as the value for the header. What are we saying with this CORS policy? We are saying that any site on the internet has permission to read or modify data on our site. That is certainly easier than defining and maintaining a whitelist of sites that need access, but let’s please stop doing this, ok? An overly-permissive CORS policy, such as
*, leads to plenty of valid attacks. The article mentions that
Access-Control-Allow-Origin support in browsers is minimal. I want to point out that the article was written in 2016. By now, this header is supported in every browser.
Some request methods require an additional preflight request to be sent before making the cross-origin request. A preflight request is an OPTIONS request automatically issued by a browser. Generally, this is when making a cross-origin request with a request method other than GET or POST, or when using certain non-whitelisted headers. The details are outlined here but, again, your browser will issue this automatically as needed.
Follow-up on CORS
For more information on CORS, I strongly recommend this video from Derbycon 2019: "To CORS, The Cause and Solution to Your SPA Problems."
The presenters explain CORS in the most understandable format I've yet seen and show how nearly every language's CORS libraries set insecure CORS defaults.
Content Security Policy
Ok, so now we have a pretty good understanding of SOP and how CORS is used to relax SOP restrictions. Let’s talk about where Content Security Policy (CSP) fits in.
CSP is a policy defined on the
Content-Security-Policy HTTP header. A legacy version of the header was
X-Content-Security-Policy. Use the current version. CSP’s primary purpose is to prevent Cross-Site Scripting (XSS) attacks. XSS works by tricking a browser into running script under your site’s origin, giving the malicious code access to read or modify the site content. We should not trust what we cannot verify!1 The CSP allows us to define a whitelist of sources of trusted content. The browser will not execute or render any resource outside of that list. If an attacker is able to inject script on your site, the script will not run as it will not match the whitelist. The CSP header can be made up of a number of different directives.
An example CSP might look like:
Content-Security-Policy: script-src 'self' www.google-analytics.com
self, our current page’s origin, and any resources requested from
www.google-analytics.com. Any script that tries to execute on the webpage that does not come from these two sources will be blocked by the browser.
The error will look like this:
Refused to load the script ‘script-uri’ because it violates the following Content Security Policy directive: “your CSP directive”.
Content Security Policy: A violation occurred for a report-only CSP policy (“An attempt to execute inline scripts has been blocked”). The behavior was allowed, and a CSP report was sent.
CSP in Reporting Mode
As the Firefox error suggests, CSP can be set to a “blocking” mode or a “reporting” mode. Under reporting, CSP will not block any content, just echo the alert onto the browser console. You must set the
report-uri directive under reporting mode to a web endpoint set up to collect CSP error messages. Upon a CSP violation, the user’s browser will POST the violation error in JSON to the configured endpoint. One such service to monitor those violations for your site is Report URI. The purpose of the reporting mode is to ensure you understand where all the resources on your site are coming from before fully enabling CSP and potentially breaking your site. You should keep a
report-uri directive on your “blocking” policy to continue to be alerted to the types of attacks being made against your site and to warn about any potential CSP misconfigurations.
Content-Security-Policy-Report-Only as the header to set CSP in reporting mode. Use
Content-Security-Policy as the header when you are ready for CSP to begin blocking content.
Content-Security-Policy: script-src 'unsafe-inline' 'unsafe-eval'
style tags as well as
There are a number of other CSP directives and I strongly encourage you to read through them via the linked page. Some notable directives:
default-src: Apply a default CSP against all resources, overriden by the specific CSP directives such as
form-action: Whitelist valid endpoints for
frame-ancestors: Define what sources are allowed to embed your webpage, such as render inside an
iframe on their site. Enabling this directive blocks clickjacking, so enable it!
sandbox: Enable an iframe-like sandbox for the requested content and apply a CSP to it. An empty value for the
sandbox directive applies all restrictions to the content, which can be selectively enabled via values such as
allow-forms. This directive is a little different, as it prevents actions the page can take rather than what resources the page can load. If specified, a page is treated as though it is loaded via an iframe, creating a wide range of effects. More details on the effects of sandboxing can be found here. This feature opens up a lot of possibilites for securely locking down areas of your site.
require-sri-for: Require the use of Subresource Integrity for scripts and styles on the page. The value options for this directive are
upgrade-insecure-requests: Instructs browsers to upgrade any HTTP links on the webpage to HTTPS. This is a simple way to upgrade a legacy page with many HTTP links to HTTPS.
You should note that including a directive without defining a whitelist defaults that directive to
*, which means allow everything. Specifying a
default-src directive overrides this behavior, naturally.
Crafting a CSP Policy
Google’s CSP page has excellent guidance, with examples, on crafting a CSP from scratch. The goal is to identify what resources your site is actually loading and setting up a policy based on that information.
That’s a Wrap
I hope this article is a useful resource toward understanding SOP, CORS, and CSP and starts you on the path to enabling CORS and CSP correctly on your sites, if they are not already implemented.
1: Requests for static resources, e.g. vendored scripts that you know will not change, should use subresource integrity as the verification mechanism for those resources. ↩
Top comments (1)
This is the best explanation on these topics! Thank you