A pen tester’s guide to Content Security Policy
In this article, we’ll look at Content Security Policy (CSP) through the eyes of a penetration tester. We will outline the advantages of CSP, explain why you should have it on your site, and share some common misconfigurations that can be exploited, along with the relevant bypass scenarios.
What is Content Security Policy?
CSP is a defense-in-depth mechanism that can help prevent client-side data/content injection attacks, such as cross-site scripting (XSS), by specifying the origins from which resources can be loaded from. CSP also offers assorted hardening options, like the rewrite of URLs with the HTTP scheme to HTTPS, and the hindering of an application from being displayed in a frame by untrusted origins.
Implementation can be done via the http-equiv attribute in the HTML <meta> tag, or the Content-Security-Policy response header. For the policy to be effective as a preventative control, it must be implemented in a strict manner. This can be a time-consuming process depending on the size of the application, which is why so many real-world implementations can be circumvented.
Keep in mind that a properly implemented CSP is still a supplementary security control, and not a replacement for secure coding practices. XSS vulnerabilities, for instance, must still be remediated by sanitizing user-supplied data with context-sensitive encoding, such as the encoding of “, ‘, > and < in an HTML context.
CSP browser compatibility
All major modern browsers have supported CSP for some time, including directives introduced in the latest version (CSP Level 3), but there are a few exceptions. For a more detailed list, please see the browser compatibility section on MDN Web Docs and the W3C working draft.
All examples demonstrated in this article have been tested on a modern browser (Chromium) with support for CSP Level 3.
Formatting a policy
A CSP consists of directives, which governs a certain area of the application’s behavior, each of which can be coupled with a set of source expressions (instructions for how the area should be governed).
Header
Content-Security-Policy: <directive> <source expressions>; <another directive> <source expressions> etc. |
<meta> element
<meta http-equiv=”Content-Security-Policy” content=”<directive> <source expressions>; <another directive> <source expressions> etc.”> |
Implementation via the response header is the preferred and most common alternative. Not all directives are supported in the <meta> element. A few examples of these directives include frame-ancestors, sandbox and report-to.
In terms of security, we’ve come across situations during penetration tests where user-supplied input has been reflected both above the <meta> element in the HTML source, and directly into the HTTP response headers. This allows the CSP implementation to either be altered, in a way that would benefit an attacker, or overwritten completely. More on this later when we go through a few bypass scenarios.
CSP directives
A directive governs a certain area of the application’s behavior. This section briefly lists some of these directives. A more detailed approach will be taken in the “CSP examples” section, where we’ll combine individual directives with a few source expressions (their respective instructions) into complete policies.
Fetch directives
The fetch directives, by far the biggest type, control what origins a resource can be loaded from. The table below showcases some of the most common ones:
default-src | Provides fallback values for other fetch directives |
connect-src | What origins can be loaded using various script interfaces (e.g., fetch() and XMLHttpRequest) |
font-src | What origins fonts can be loaded from |
frame-src | What origins <frame> and <iframe>’s can be loaded from |
img-src | What origins images can be loaded from |
media-src | What origins media (e.g., <audio>, <video>) can be loaded from |
object-src | What origins plugin content (e.g., <object> and <embed>) can be loaded from |
script-src | What origins scripts can be loaded from |
style-src | What origins stylesheets can be loaded from |
Document directives
This type features, as of now, only two directives, both governing the properties of a document/worker environment:
base-uri | What document base URLs (<base> element) can be used |
sandbox | Creates a restricted environment for requested resources that, for example, treat these as if they were from a unique origin, as well as assorted hardening options |
Navigation directives
Instead of controlling the origins which a resource can be loaded from, like the fetch directives, the navigation directives control what origins we can issue requests to, or what origins are allowed to embed our pages:
form-action | What origins can be used in form submissions |
frame-ancestors | What origins are allowed to embed the page, e.g., the <iframe>, <object> and <embed> tags |
Reporting directives
Controls the reporting of CSP violations:
report-to | What URL to report CSP violations to |
Other
The only directive here that is not in its experimental stage is “upgrade-insecure-requests”:
upgrade-insecure-requests | Rewrite of URLs with the HTTP scheme to HTTPS |
CSP source expressions
A source expression is the value a directive can have. And just like with the directives, let’s briefly list some source expressions now, and then take a more detailed approach in the “CSP examples” section where we’ll construct complete policies.
Keywords
‘none’ | Nothing is allowed |
‘self’ | Only the same origin is allowed |
‘unsafe-inline’ | Inline script execution is allowed |
‘unsafe-eval’ | Functions that evaluate code from strings, such as eval(), are allowed |
‘strict-dynamic’ | Allowed scripts can dynamically load scripts that are not “parser-inserted” |
Absolute URLs
https://outpost24.com/assets/specific.js | This specific file from this URL |
https://sub.outpost24.com/ | Anything from this URL |
Schemes
https: | Any origin if it has this scheme |
http: | Any origin if it has this scheme |
data: | Any origin if it has this scheme |
Hosts
outpost24.com | Any resource on this host (no matter what scheme) |
*.outpost24.com | Any resource on this host and its subdomains (no matter what scheme) |
* | Any host |
Nonces and Digests
‘nonce-r4nd0m’ | If an element has a nonce attribute value matching one set in the policy it is permitted; otherwise not |
‘sha256-golcz7QFYRhblO…’ | The contents of every inline script to be permitted is passed through a hash function; the resulting digest is then included in the policy. Any script with a non-matching digest (e.g., one injected by an attacker) will be prevented |
CSP examples
One of the keys to a strict and secure CSP implementation is a thorough understanding of its inner workings. With the help of some examples, we’ll explore the technical behaviors of various policy implementations in greater detail. Beginning with the basics, we’ll gradually increase the policy complexity where multiple directives and source expressions are intertwined.
Origin clarification
Normally when we’re talking about the origin of, let’s say, an absolute URL, then it would be defined by its scheme, host and port. In CSP, however, when browsing on an http website, resources that are loading from https will also be matched with the ‘self’ source expression, even though the scheme does not match the current origins. The same also applies to the WebSocket protocol.
The following table showcases a few comparisons with the URL http://outpost24.com in the scenario where it would attempt to load a resource via the “src” attribute of an element:
Value of a “src” attribute | Is ‘self’? |
media/image.png | Yes, given that no base URL has been specified or injected for this relative URL via a <base> tag |
//outpost24.com/media/image.png | Yes |
http://outpost24.com/resources/special.js | Yes – path does not matter for the ‘self’ source expression (it’s not like we’ve specified “media/image.png” somewhere either) |
http://outpost24.com:80/media/image.png | Yes – 80 is the default port for HTTP |
https://outpost24.com/media/image.png | Yes – corresponding insecure scheme (http) is used |
sftp://outpost24.com/media/image.png | No – the scheme does not match |
http://www.outpost24.com/media/image.png | No – the host does not match (www subdomain) |
http://outpost24.com:8080/media/image.png | No – the port does not match |
Example 1 – The img-src directive
Content-Security-Policy: img-src ‘self’ |
As a first example, we’ll look at the interplay between a directive and a source expression. In this case, img-src and ‘self’, respectively. The img-src directive controls which origins an image resource can be loaded from. The ‘self’ source expression is CSP’s process of matching an origin against the application’s own. If the origin is the same, the resource will be permitted. If combined and implemented, the only benefit this policy would offer is the prevention of loading any image resources given that they are not located on the application itself.
The following figure demonstrates that we’re able to successfully load an image located in the media directory using a relative URL, as the origin does not violate CSP’s ‘self’ source expression:
But, if we try to load something from another origin, we’ll get an error:
Similarly, if we were to use an absolute URL pointing to the same resource located on the application’s own origin, instead of a relative path, we can see in the figure below that it also got loaded without any errors:
Whereas, if this resource was located on a subdomain (in this case, www), we can see that it was prevented from loading, as it no longer satisfies CSP’s ‘self’ criteria:
If we, on the other hand, would like to add this subdomain while keeping the ‘self’ source expression active for everything else, we can simply add it as a whitelisted value to the img-src directive:
Based on how strict we want our whitelist to be, the above could also be accomplished with:
http://*.localhost (any subdomain would then be allowed)
*.localhost (any subdomain and scheme would then be allowed)
http: (any origin with this scheme would then be allowed)
Example 2 – The script-src directive and the ‘unsafe-inline’ oversight in policies based on whitelists
Following this, let’s apply what we’ve learned in the first example to the script-src directive to prevent the evaluation of unwanted scripts injected by an attacker – most likely our primary focus.
Content-Security-Policy: img-src ‘self’; script-src ‘self’ |
If this keyword works just as it did with the img-src directive, only script resources from the same origin would be allowed, right?
In the above figure, we can see that when attempting to load the “write24.js” script located in the “js” directory we get no error messages in the console and a successful evaluation, as expected.
And, if we try with a script from another origin…
…we get an error, as we should.
So, preventing script evaluation seems rather easy; unfortunately, real-world applications are rarely this straightforward. For example, what would happen with any inline scripts and event handlers if the very same policy would be active (considering no input from an attacker whatsoever)?
The above figure may be somewhat exaggerated but given a similar wall of errors, a quick fix is generally sought after. As a result, the ‘unsafe-inline’ keyword is often set, effectively removing the errors. But what is frequently overlooked with this keyword is that it now permits any inline script to be evaluated, including the ones injected by an attacker:
Seeing that that the ‘unsafe-inline’ keyword is… unsafe, one could assume that it wouldn’t be used in real-world deployments that often. Based on our pen testing experience, this is not the case. We often see this keyword being featured in more implementations than not (and yes, I’m talking about policies where the nonce, hash or strict-dynamic source expressions aren’t used, which otherwise would yield the ‘unsafe-inline’ keyword ineffective in modern browsers – but more on this later).
How can you prevent external scripts from untrusted origins, then allow legitimate inline scripts, and block scripts injected by an attacker? We’ll explore this later, but first we need to get acquainted with a cornerstone of CSP.
Example 3 – The default-src fallback
Now that we have some understanding of the img-src and script-src directives, what about all the other ones? If a similar type of protection is required for multiple directives, do we need to specify each separately? Not really, here’s where the default-src directive comes into play.
Content-Security-Policy: default-src ‘self’ |
The default-src directive acts as a fallback for all other fetch directives given that they are not explicitly set. This means that we don’t have to specify, for instance, the ‘self’ source expression for every single directive if we would like fonts, stylesheets, images, scripts, etc., to be loaded solely from the application’s own origin. Let’s test this by attempting to load an external image resource with only the above policy set:
And indeed, we can see that the external image got prevented with a clear explanation in the console window.
To clarify, why not try to inject an inline script and stylesheet as well:
Observe the successful prevention. If we now want that, for instance, stylesheets should be treated differently while everything else follows the ‘self’ source expression, we explicitly set that directive (in the case of stylesheets, style-src) with our desired value:
Example 4 – Hashes
Back to the question we ended our second example with: how can you prevent external scripts from untrusted origins, then allow legitimate inline scripts, and block scripts injected by an attacker?
The answer to this is hashes and nonces. In this example, let’s go through how this can be done with the help of hashes.
To put it briefly, in a hash-based policy, the contents of every inline script to be permitted are passed through a hash function; the resulting digest is then included in the policy. Any script with a non-matching digest (e.g., one injected by an attacker) will be prevented.
As an example, consider that we have the following inline script that we want to permit:
<script>document.write(24);</script> |
We now need to hash the contents within the script tags. The allowed algorithms are sha256, sha384 and sha512. If sha256 is to be used, the following terminal commands can be used to generate the final digest (pay attention to any newline characters in your scripts):
And lastly, we must append it to our policy:
Observe the successful evaluation of the legitimate inline script (simulated via the XSS input field this time) and a console window free from errors. Now, if we append an inline script not matching the digest…
…we can see that it got prevented, great!
Consider that we now happen to have some event handlers that we can’t get rid of, is it possible to simply calculate the digest of those as well for them to also be permitted?
As seen in the image above, it is not. However, if we take a closer look at the very end of the error message in the console, we are informed that if the ‘unsafe-hashes’ keyword is present, hashes can also be applied to event handlers (CSP Level 3); so, if appended to the very same policy…
…we get our alert box. But what’s the catch (it’s prepended with “unsafe-“ after all)? Well, if we permit an even handler script this way, we must be mindful that it will also be available to an attacker, who might use its functionalities in an unintended manner (this is more likely to be overlooked in large and complex functions).
As a final point, the hash-based approach is often used if a strict CSP is strived for on a static application. The reason is that the other strict method (using nonces), which we’ll cover in the next example, requires server-side processing, such as a templating system.
Example 5 – Nonces
Now that we are aware of the hash-based approach, let’s see how our needs can also be met with the help of nonces.
Instead of comparing the hash value of every single script with ones set in the policy to determine what should and shouldn’t be permitted, a policy based on nonces simply checks if a randomly generated value (nonce) set in the policy matches the value of a “nonce” attribute appended to the script tags. Since the nonces are generated for each response, it becomes impossible for an attacker to predict their value, effectively preventing them from injecting their own scripts.
As an example, consider “r4nd0m” to be the randomly generated nonce:
Observe the successful evaluation of the inline script, as its nonce attribute value matches the one set in the policy, and a console window free from errors. If we now append an inline script without a nonce at all (or one not matching the policy):
…we can see that it indeed got prevented.
Example 6: ‘strict-dynamic’
Consider that we now have a permitted script, let’s say from a third-party vendor using a nonce-based approach, that dynamically loads a script. Would this dynamically loaded script also be permitted considering its parent script is trusted? Let’s try to simulate this behavior:
Seeing that the dynamically loaded “file.js” script got prevented, we can conclude that trust is not inherited. In order permit it, we would also have to set a correct nonce attribute for it. Now that this is just an example with the help of an inline script under our control, we can obviously do that, but if this is not the case (e.g., if the script would’ve been located on a CDN) then we can make use of yet another keyword: ‘strict-dynamic’. This keyword offers two primary benefits, one being the acceptance of dynamically loaded scripts (exactly what we’re after). The other is the ignoring of any whitelisted source expressions. This will enforce a strict policy in modern browsers while keeping the ignored whitelist as a fallback for old browsers with no support for CSP3 to ensure availability.
So, if appended to the policy…
…we can see that the dynamically loaded “file.js” script in fact got loaded – an effective way to ease the deployment of a strict CSP on large and complex applications.
And lastly, to demonstrate ‘strict-dynamic’s ignoring of whitelisted source expressions, let’s try to include several of these while having ‘strict-dynamic’ set:
As seen in the image above, both the attempt of executing the injected inline script and the loading of an external one got prevented despite having the otherwise unsafe source expressions (in this case, http:, http:, data: * and ‘unsafe-inline’) included in the policy.
CSP bypasses
With a more thorough understanding of CSP’s inner workings, we can now explore the ways adversaries can bypass certain misconfigurations.
‘unsafe-inline’
If the script-src directive (or default-src given that script-src is not explicitly set) contains the ‘unsafe-inline’ source expression and the nonce, hash or ‘strict-dynamic’ source expressions are not set in the same directive, we’re able to simply inject our own inline scripts:
When we talk about bypasses for CSP, XSS attacks are often so prioritized that other forms of data/content injections get overlooked. For example, if the ‘unsafe-inline’ keyword would be set on a directive that is not script-src or default-src but instead, style-src, then the injection of stylesheets could be a viable attack vector an adversary decides to proceed with:
Similarly, this vector may also be of interest in the case of a sandbox policy:
http:, https:, data:
Using any of these schemes in a policy where the ‘strict-dynamic’ keyword isn’t set, simply permits resources from any origins to be loaded as long they are using these schemes:
Overly permissive whitelists
It is common to find public hosts, such as content delivery networks (CDNs), or other services where anyone can upload resources, to be overly trusted in CSP whitelists. While the application itself uses these to load certain up-to-date frameworks, it does not mean that the same policy will be effective in hindering an adversary from doing the opposite. Besides potential unwanted scripts that can be directly loaded and evaluated, we must also be mindful about the loading of frameworks that would enable an attacker to get their JavaScript evaluated with the help of, for example, client-side template injection vulnerabilities:
Another thing we must consider is the possibility of hosted JSON with Padding (JSONP) endpoints on these hosts. While used in the past to bypass the same-origin policy (SOP), they can also be used to bypass certain CSP configurations. The following figure demonstrates a local example of this:
If public hosts are required, make sure to restrict the paths in the whitelist. Alternatively, consider using a strict policy such as one based on nonces and the ‘strict-dynamic’ source expression.
‘self’
Do not assume that resources on the application’s own origin, such as AngularJS libraries, JSONP endpoints and self-uploaded files, cannot be misused by an adversary. The following figure demonstrates a scenario where an attacker simply included one of their own malicious scripts uploaded to the application, via, let’s say, an unrestricted file upload functionality:
‘unsafe-eval’
If an application uses functions that evaluate code from strings, such as eval(), then the CSP may start to throw some errors, as these are not allowed by default. This is a good safeguard against a variety of DOM-based XSS vulnerabilities considering that user-supplied input would’ve been passed to them. If the associated risk of allowing these functions is accepted, we need to set the ‘unsafe-eval’ source expression in our policy to prevent the blocking.
Path tricks using whitelisted open redirects
As covered in W3C Working Draft – CSP3 § 7.6. Paths and Redirects, if an origin containing an open redirect is whitelisted, then it can be used to bypass a set path of another whitelisted host. The following figure demonstrates an attempt to load a script resource located on a whitelisted host but on a non-matching path; since this is a violation of the policy, the resource is not loaded:
However, with the help of an open redirect located on a whitelisted origin (in this case, the application itself), it may be pointed to the same prohibited resource used above but with a perhaps unintended outcome:
User-supplied input reflected directly into (or above) a policy implementation
Ensure that user-supplied input isn’t reflected above the <meta> elements in the HTML source (given that the CSP is implemented here) or directly into the HTTP response headers to avoid any bypassing or altering of the CSP implementation altogether – no matter how strict it would be. During a recent penetration test where a relatively strict CSP was implemented via the http-equiv attribute in the <meta> tag, we managed to find a reflection of our self-supplied input on the single line of markup present above it (in this case, the lang attribute of the <html> start tag), allowing us to entirely bypass the policy:
<base> Injection
Without an explicitly set base-uri directive, an attacker might be able to inject a <base> tag in order to rewrite the base URL of relative paths to a host under their control. This is especially effective in nonce-based policies. The following figure demonstrates an intended loading of the “write24.js” script, located in the “js” directory using a nonce-based approach:
If an attacker now injects a <base> tag with an “href” attribute value pointing to their own host, we can see that their malicious script gets loaded and evaluated instead:
How Outpost24 can help
CSP is a great defense-in-depth mechanism, if properly implemented. Unfortunately, misconfigurations are far too common, and create a false sense of security. At Outpost24, we can evaluate your application’s CSPs with our application security services, which combines manual tests with automated scanning for the most accurate view of vulnerabilities. Get direct access to security experts who can help with your remediations efforts, and provide continuous best practice guidance.
References
https://research.google/pubs/pub45542/