CS253 Lecture Summaries: Part VII: XSS Defences
A better name for XSS is HTML injection, analogous to SQL Injection.
One way of thinking about XSS attacks is in the context shift in the document flow.
One type of attack is injecting down - create a new nested context. Eg inserting a new node into the DOM tree.
Another is injecting up - ending the current context to go to a higher one. Eg escaping out of an attribute string.
Where does untrusted data come from?
HTTP requests from the user - query params, form fields, headers, cookies, file uploads.
Data from a database - who knows how the data got into the database? Do not trust.
Third party services - who knows if that is safe? What if the service gets hacked and starts sending unsafe data?
When to escape? On the way into the database? Or out at render time?
The problem of doing it on the way in is that you don’t know the context in which it will appear. There are different contexts (html parser, js parser) and so how do you escape all of them?
At render time we know where we’re going to put the string, so we can make the right escape decisions. Better to assume at runtime that your data is unsanitized and make sure you do it for your context.
How to escape user input?
Use your framework’s built in HTML escaping functionality - given enough eyeballs all bugs are shallow.
Make sure you know the contexts where it is safe to use the output - eg don’t use an HTML escaping function and put the output into a script tag.
XSS is one of the most common vulnerabilities on the web. What if we accept that XSS will happen to our site?
How can we defend our site’s users even in the presence of XSS?
This seems a tall order - the attacker code is running on the same page as the user’s data.
Key idea - defence in depth - provide redundancy in case security controls fail, or a vulnerability is exploited. Attacker now has to find multiple exploitable vulnerabilities in order to produce a successful attack.
Some defences:
Defend their cookies, use HttpOnly cookie attribute to prevent cookie being read by JavaScript.
Content Security Policy
CSP is a way to prevent our site from making requests to other sites.
CSP is an added layer of security against XSS. Even if the attacker is running code in the user’s browser in our site’s context, we can limit the damage they can do.
We need a way to specify the policy - HTTP header.
We add the Content-Security-Policy
header to an HTTP response to control the resources the page is allowed to use.
CSP will block HTTP requests which would violate the policy.
For example:
Content-Security-Policy: default-src 'self'
- says that we only want the page to make requests to the same origin. This policy will prevent inline scripts from running too!
We can allow resources from our sitek, including our subdomains, block anywhere else, but allow images from anywhere:
Content-Security-Policy: default-src 'self' *.mailsite.com; img-src *
We can run the CSP in report-only mode to allow the request, but report it.
You can add the report uri on the CSP too: Content-Security-Policy: default-src 'self'; report-uri https://example.com/report
to get notified of XSS attacks and other policy violations.
There are a lot of more specific directives too eg connect-src
which restricts sources from ‘script interfaces’ like fetch, XHR, WebSocket, Navigator.sendBeacon, etc.
script-src
is an important one - restrict the origin of scripts. style-src
worker-src
too, frame-src
. object-src
can be used to block old applets that you don’t use.
Other directives that you can include that don’t inherit from default-src:
base-uri
Restricts the URLs that can be used in base
which sets the base url for all relative urls.
form-action
Restricts which can be used as a target of a form submission.
frame-ancestors
restircts parents which may embed the page using iframes
upgrade-insecure-requests
instruct browser to treat all http urls as their https equivalent.
navigate-to
restricts the urls to which the document can navigate.
You can add unsafe-inline
to allow inline scripts, but this is basically equivalent to having no CSP, it allows any inline script tag to execute. (Check out Santander for an example of a bank site using unsafe-inline).
The problem is that it can be hard to get the CSP right.
For example, let’s say you allow a script that you want from google analytics. That site may fetch other scripts from other domains (very common), and it might change them. This means that our CSP is going to break our site, or require constant ad-hoc revision to update it to respond to the behaviour of the site.
What if we propagate trust from the original script to any other script that it includes.
For some of the issues see the paper CSP is dead. Long live CSP! linked here
From the paper 99% of hosts with CSP use policies that don’t defend against XSS. A lot depend on ‘unsafe-inline’ for example.
There is an implementation improvement now recommended in that paper that improves this:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'
Then in the html:
<script src="https://trusted.com" nonce="abc123"></script>
<script nonce="abc123">foo()</script>
It specifies that the trust explictly given to a script present in the markup by accompanying it with a nonce, shall be propagated to all the scripts loaded by that root script.
So no more whitelist, and trust cascades.
Another useful policy is Feature-Policy
where you can selectively disable browser features. eg autoplay
geolocation
picture-in-picture
and vertical-scroll
.
DOM Based XSS
One other type of XSS is DOM-based XSS.
Let’s assume that the DOM is being modified by a valid script running in the browser.
The attacker tricks the script into adding attacker DOM nodes into the page.
Unlike reflected or stored XSS, the attacker isn’t changing the HTML rendered by the server, instead the page is attacked at “runtime”.
So for example, let’s say my user profile is fetched from the DB, and put in the page by a trusted script saying node.innerHTML = data
. This is really dangerous.
Instead we should use textContent
(which escapes for you), rather than innerHTML
.
CSP doesn’t protect against DOM-based XSS, which is common in the land of frontend frameworks fetching and injecting content dynamically, rather than server templated pages.
There is a new spec, “Trusted Types” that will eliminate Dom-based xss. It works like this:
First the server sends the header Content-Security-Policy trusted-types template
which tells the browser we’re using the Trusted Types API with a policy called ‘template’. Then we do this:
const templatePolicy = TrustedTypes.createPolicy('template'), {
createHTML: (userInput) => {
return htmlEscape(userInput)
}
});
const data = await fetch('/api/bio?user=me')
const html = templatePolicy.createHTML(data)
document.getElementById('bio').innerHTML = html