I am using Helmet to set up the content security policies of my web app in the backend using Express. The policies look like the following:
const express = require("express"); const app = express(); const helmet = require('helmet'); app.use(helmet()); app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://ajax.googleapis.com"], imgSrc: ["https://firebasestorage.googleapis.com"], objectSrc: ["'none'"], styleSrc: ["'self'", "https://maxcdn.bootstrapcdn.com/bootstrap", "https://www.w3schools.com"], upgradeInsecureRequests: [], }, }) );
When my app tries to access a link such as https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css
. It says that it violates the styleSrc policy. But I have specified as one of the policies that https://maxcdn.bootstrapcdn.com/bootstrap
is allowed, I thought with that https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css
would be accepted as well, since it is a child src. But apparently it gets blocked. How do I allow the child src to pass then? I have tried https://maxcdn.bootstrapcdn.com/bootstrap*
but it is invalid.
Advertisement
Answer
Author of Helmet here.
Try adding a trailing slash, like this:
https://maxcdn.bootstrapcdn.com/bootstrap/
This is because /bootstrap
doesn’t let you do things like /bootstrap/3.4.0/css/bootstrap.min.css
, but /bootstrap/
does. This is a Content Security Policy thing, not a Helmet thing.
For the nitty-gritty details, see step 11 of the “Matching Source Expressions” section in the CSP spec:
If the source expression contains a non-empty
path-part
, and the URL is not the result of a redirect, then:
- Let exact-match be
true
if the final character ofpath-part
is not the U+002F SOLIDUS character (/
), andfalse
otherwise.- Let source-expression-path-list be the result of splitting
path-part
on the U+002F SOLIDUS character (/
).- If source-expression-path-list’s length is greater than url-path-list’s length, return does not match.
- For each entry in source-expression-path-list:
- Percent decode entry.
- Percent decode the first item in url-path-list.
- If entry is not an ASCII case-insensitive match for the first item in url-path-list, return does not match.
- Pop the first item in url-path-list off the list.
- If exact-match is
true
, and url-path-list is not empty, return does not match.
As an aside, you might want to clean up your Helmet code to something like this:
app.use(helmet({ contentSecurityPolicy: { directives: { // ... }, }, }));
Your code uses helmet()
, which includes some default CSP middleware, and then later overrides it with helmet.contentSecurityPolicy()
. Not a huge deal, but slightly more correct to only use it once.