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-srcProvides fallback values for other fetch directives
connect-srcWhat origins can be loaded using various script interfaces (e.g., fetch() and XMLHttpRequest)
font-srcWhat origins fonts can be loaded from
frame-srcWhat origins <frame> and <iframe>’s can be loaded from
img-srcWhat origins images can be loaded from
media-srcWhat origins media (e.g., <audio>, <video>) can be loaded from
object-srcWhat origins plugin content (e.g., <object> and <embed>) can be loaded from
script-srcWhat origins scripts can be loaded from
style-srcWhat 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-uriWhat document base URLs (<base> element) can be used
sandboxCreates 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-actionWhat origins can be used in form submissions
frame-ancestorsWhat origins are allowed to embed the page, e.g., the <iframe>, <object> and <embed> tags

Reporting directives

Controls the reporting of CSP violations:

report-toWhat URL to report CSP violations to

Other

The only directive here that is not in its experimental stage is “upgrade-insecure-requests”:

upgrade-insecure-requestsRewrite 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.jsThis 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.comAny resource on this host (no matter what scheme)
*.outpost24.comAny 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” attributeIs ‘self’?
media/image.pngYes, given that no base URL has been specified or injected for this relative URL via a <base> tag
//outpost24.com/media/image.pngYes
http://outpost24.com/resources/special.jsYes – 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.pngYes – 80 is the default port for HTTP
https://outpost24.com/media/image.pngYes – corresponding insecure scheme (http) is used
sftp://outpost24.com/media/image.pngNo – the scheme does not match
http://www.outpost24.com/media/image.pngNo – the host does not match (www subdomain)
http://outpost24.com:8080/media/image.pngNo – 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:

Figure 1  – permitted image loading (CSP’s ‘self’ criteria is satisfied)
Figure 1  – permitted image loading (CSP’s ‘self’ criteria is satisfied)

But, if we try to load something from another origin, we’ll get an error:

Figure 2  – prevented image loading (CSP’s ‘self’ criteria isn’t satisfied)
Figure 2  – prevented image loading (CSP’s ‘self’ criteria isn’t satisfied)

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:

Figure 3  – permitted image loading (CSP’s ‘self’ criteria is satisfied)
Figure 3  – permitted image loading (CSP’s ‘self’ criteria is satisfied)

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:

Figure 4  – prevented image loading (host differs as a result of the prepended “www” subdomain)
Figure 4  – prevented image loading (host differs as a result of the prepended “www” subdomain)

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:

Figure 5 – permitted image loading (“www” subdomain is now whitelisted in the policy)
Figure 5 – permitted image loading (“www” subdomain is now whitelisted in the policy)

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?

Figure 6 – permitted script loading (does not violate the CSP configuration)
Figure 6 – permitted script loading (does not violate the CSP configuration)

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…

Figure 7 – prevented script loading (violates the CSP configuration)
Figure 7 – prevented script loading (violates the CSP configuration)

…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)?

Figure 8 – multiple inline scripts and event handlers prevented (demonstration of a too hurried policy implementation)
Figure 8 – multiple inline scripts and event handlers prevented (demonstration of a too hurried policy implementation)

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:

Figure 9 – maliciously injected script permitted due to a common CSP misconfiguration
Figure 9 – maliciously injected script permitted due to a common CSP misconfiguration

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:

Figure 10 – prevented image loading despite not having an explicitly set img-src directive
Figure 10 – prevented image loading despite not having an explicitly set img-src directive

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:

Figure 11 - prevented script and stylesheet evaluation despite not having an explicitly set script-src and style-src directive
Figure 11 – prevented script and stylesheet evaluation despite not having an explicitly set script-src and style-src directive

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:

Figure 12 – explicitly set exception to permit the inline stylesheet
Figure 12 – explicitly set exception to permit the inline stylesheet

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):

Figure 13 – hashing document.write(24);
Figure 13 – hashing document.write(24);

And lastly, we must append it to our policy:

Figure 14 – permitted script evaluation (digest matches the one set in the CSP)
Figure 14 – permitted script evaluation (digest matches the one set in the CSP)

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…

Figure 15 – maliciously injected script got prevented (non-matching digest); intended script got evaluated (matching digest)
Figure 15 – maliciously injected script got prevented (non-matching digest); intended script got evaluated (matching 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?

Figure 16 – matching digest but prevented script evaluation
Figure 16 – matching digest but prevented script evaluation

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…

Figure 17 – permitted handler script evaluation with the ‘unsafe-hashes’ keyword set
Figure 17 – permitted handler script evaluation with the ‘unsafe-hashes’ keyword set

…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:

Figure 18 – permitted script evaluation (nonce of script matches the one set in the CSP)
Figure 18 – permitted script evaluation (nonce of script matches the one set in the CSP)

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):

Figure 19 – maliciously injected script prevented (non-matching nonce); intended script evaluated (matching nonce)
Figure 19 – maliciously injected script prevented (non-matching nonce); intended script evaluated (matching nonce)

…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:

Figure 20 – prevented loading of the dynamically loaded “file.js” script
Figure 20 – prevented loading of the dynamically loaded “file.js” script

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…

Figure 21 – permitted loading of the dynamically loaded “file.js” script with the ‘strict-dynamic’ keyword set
Figure 21 – permitted loading of the dynamically loaded “file.js” script with the ‘strict-dynamic’ keyword set

…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:

Figure 22 – whitelisted source expressions ignored by ‘strict-dynamic’
Figure 22 – whitelisted source expressions ignored by ‘strict-dynamic’

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:

Figure 23 – maliciously injected script permitted due to the common ‘unsafe-inline’ oversight
Figure 23 – maliciously injected script permitted due to the common ‘unsafe-inline’ oversight

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:

Figure 24 – CSS injection demonstration (XSS is not the only vulnerability an attacker can exploit)
Figure 24 – CSS injection demonstration (XSS is not the only vulnerability an attacker can exploit)

Similarly, this vector may also be of interest in the case of a sandbox policy:

Figure 25 – CSS injection demonstration in CSP sandbox configuration
Figure 25 – CSS injection demonstration in CSP sandbox configuration

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:

Figure 26 – maliciously injected script permitted due to https: being set
Figure 26 – maliciously injected script permitted due to https: being set
Figure 27 – maliciously injected script permitted due to data: being set
Figure 27 – maliciously injected script permitted due to data: being set

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:

Figure 28 – XSS via client-side template injection due to overly permissive whitelisting of public hosts
Figure 28 – XSS via client-side template injection due to overly permissive whitelisting of public hosts

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:

Figure 29 – evaluation of a malicious script via user-supplied input (“callback” parameter) to a JSONP endpoint (no violation of CSP configuration; the endpoint is located on the same origin)
Figure 29 – evaluation of a malicious script via user-supplied input (“callback” parameter) to a JSONP endpoint (no violation of CSP configuration; the endpoint is located on the same origin)

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:

Figure 30 – evaluation of self-uploaded malicious script (no violation of CSP configuration as the file is located on the same origin)
Figure 30 – evaluation of self-uploaded malicious script (no violation of CSP configuration as the file is located on the same origin)

‘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:

Figure 31 – prevented script loading due to non-matching path
Figure 31 – prevented script loading due to non-matching path

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:

Figure 32 – permitted script loading (with the help of an open redirect) despite a non-matching path

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:

Figure 33 – bypassed policy due to user-supplied input being reflected above the implementation
Figure 33 – bypassed policy due to user-supplied input being reflected above the implementation

<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:

Figure 34 – intended script loading without any malicious input
Figure 34 – intended script loading without any malicious input

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:

Figure 35 – evaluation of a malicious script due to no explicitly set base-uri directive
Figure 35 – evaluation of a malicious script due to no explicitly set base-uri directive

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/

https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

https://www.w3.org/TR/CSP3/

https://csp.withgoogle.com/docs/index.html

About the Author

Jimmy Bergqvist Application Security Expert, Outpost24

Jimmy is an Application Security Expert at Outpost24. With over 10 years of experience, he brings a wealth of expertise to the team. Known for his integrity and trustworthiness, Jimmy consistently delivers high-quality application security services.