Content Security Policy (CSP)
Written by Ulises Gascón
Apr 08, 2020 — 5 min readThe attack
There isn’t one specific attack the CSP module prevents. The main thing is this: you don’t want anyone putting anything on your webpages that you don’t expect.
The nastiest attack is probably cross-site scripting (XSS), which is when a hacker puts malicious JavaScript onto your page. If I can run JavaScript on your page, I can do a lot of bad things, from stealing authentication cookies to logging every user action.
There are other things attackers can do, even if they can’t execute JavaScript. For example, if I could put a tiny, transparent 1x1 image on your site, I could get a pretty good idea of how much traffic your site gets. If I could get a vulnerable browser plugin like Flash to run, I could exploit its flaws and do things you don’t want!
One of the tricky things about these injection attacks is the browser doesn’t know what’s good and what’s bad. How can it tell the difference between a legitimate JavaScript file and a malicious one? In many cases, it can’t…unless you’ve defined a Content Security Policy.
Most modern browsers support a header called Content-Security-Policy
, which is effectively a whitelist of things allowed to be on your page. You can whitelist JavaScript, CSS, images, plugins, and much more. Things are opt-in, so you’re saying “this stuff is allowed” instead of “this stuff is not allowed”.
The header
Let’s say you’ve got a website that links to no external resources at all, just your stuff. You could set a header that looks like this:
Content-Security-Policy: default-src 'self'
This effectively tells the browser “only load things from my own domain”. If you’re running example.com and a user tries to load https://example.com/my-javascript.js
, it’ll work just fine. But if a user tries to load http://evil.com/evil.js
, it won’t load at all!
Now, let’s say you want to also allow CSS from Bootstrap’s CDN. You could set a CSP that looks like this:
Content-Security-Policy: default-src 'self'; style-src 'self' maxcdn.bootstrapcdn.com
Now we’ve whitelisted 'self'
and maxcdn.bootstrapcdn.com
. The user will be able to load CSS from there, but nothing else. The user won’t even be able to load JavaScript or images from that URL, either—only stylesheets.
As an example here is the CSP used by Github:
default-src 'none';
base-uri 'self';
block-all-mixed-content;
connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com;
font-src github.githubassets.com;
form-action 'self' github.com gist.github.com;
frame-ancestors 'none';
frame-src render.githubusercontent.com;
img-src 'self' data: github.githubassets.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com customer-stories-feed.github.com;
manifest-src 'self';
media-src 'none'; script-src github.githubassets.com;
style-src 'unsafe-inline' github.githubassets.com
The code
Helmet’s csp module helps set Content Security Policies.
Directives can be kebab-cased (like script-src
) or camel-cased (like scriptSrc
); they are equivalent.
Directives
There are a lot of inconsistencies in how browsers implement CSP. Helmet looks at the user-agent of the browser and sets the appropriate header and value for that browser. If no user-agent is matched, it will set all the headers with the 2.0 spec.
Check out the supported directives
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'", 'default.com'],
scriptSrc: ["'self'", "'unsafe-inline'"],
sandbox: ['allow-forms', 'allow-scripts'],
reportUri: '/report-violation',
objectSrc: ["'none'"],
//...
upgradeInsecureRequests: true,
workerSrc: false
}
}))
CSP violations
If you’ve specified a reportUri, browsers will POST any CSP violations to your server.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy({
directives: {
reportUri: '/report-violation',
//...
}
}))
Report Only mode
This modules reportOnly option will switch the header to Content-Security-Policy-Report-Only. This instructs browsers to report violations to the reportUri (if specified) but it will not block any resources from loading.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy(csp({
directives: {
// ...
},
reportOnly: true
})
set all headers
To set all headers, including legacy ones, set the setAllHeaders
option to true. Note that this will change the value of the headers based on User-Agent
. You can disable this by using the browserSniff: false
option above.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy(csp({
directives: {
// ...
},
setAllHeaders: true
})
loose mode
This module will detect common mistakes in your directives and throw errors if it finds any. To disable this, enable "loose mode".
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy(csp({
directives: {
// ...
},
loose: false
})
Disable for Android
Old Android browsers can be very buggy. This is false by default.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy(csp({
directives: {
// ...
},
disableAndroid: true
})
Browser sniffing
By default, this module will look at the incoming User-Agent
header and send different headers depending on the detected browser. For example, Chrome prior to version 25 uses an alternate header called X-WebKit-CSP
, and this module handles that. If no browser is detected, this module will set all the headers with the 2.0 spec.
To disable this browser sniffing and assume a modern browser, set the browserSniff
option to false
.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy(csp({
directives: {
// ...
},
browserSniff: false
})
Advance CSP
Generating Nounces
const uuidv4 = require('uuid/v4')
app.use(function (req, res, next) {
res.locals.nonce = uuidv4()
next()
})
app.use(csp({
directives: {
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'` // 'nonce-614d9122-d5b0-4760-aecf-3a5d17cf0ac9'
]
}
}))
app.use(function (req, res) {
res.end(`<script nonce="${res.locals.nonce}">alert(1 + 1);</script>`)
})
Debugging mode
app.use(csp({
directives: {
// ...
},
reportOnly: (req, res) => req.query.cspmode === 'debug'
})
Refs:
- Helmet | Content Security Policy
- Google developers | Content Security Policy
- Wikipedia | Cross-site scripting
- Can I use | Content Security Policy 1.0
- Can I use | Content Security Policy 2.0
- Content Security Policy Reference
- Content Security Policy Browser Test
- 📺 dotSecurity 2016 | Scott Helme | Content Security Policy: The application security Swiss Army Knife
- 📺 GOTO 2018 | Matt Brunt | Content Security Policies: Let's Break Stuff