helmet-csp
Version:
Content Security Policy middleware
141 lines (136 loc) • 6.33 kB
JavaScript
Object.defineProperties(exports, {__esModule: {value: true}, [Symbol.toStringTag]: {value: "Module"}})
const dangerouslyDisableDefaultSrc = Symbol("dangerouslyDisableDefaultSrc")
const DEFAULT_DIRECTIVES = {
"default-src": ["'self'"],
"base-uri": ["'self'"],
"font-src": ["'self'", "https:", "data:"],
"form-action": ["'self'"],
"frame-ancestors": ["'self'"],
"img-src": ["'self'", "data:"],
"object-src": ["'none'"],
"script-src": ["'self'"],
"script-src-attr": ["'none'"],
"style-src": ["'self'", "https:", "'unsafe-inline'"],
"upgrade-insecure-requests": []
}
const SHOULD_BE_QUOTED = new Set(["none", "self", "strict-dynamic", "report-sample", "inline-speculation-rules", "unsafe-inline", "unsafe-eval", "unsafe-hashes", "wasm-unsafe-eval"])
const getDefaultDirectives = () => Object.assign({}, DEFAULT_DIRECTIVES)
const dashify = str => str.replace(/[A-Z]/g, capitalLetter => "-" + capitalLetter.toLowerCase())
const isDirectiveValueInvalid = directiveValue => /;|,/.test(directiveValue)
const shouldDirectiveValueEntryBeQuoted = directiveValueEntry => SHOULD_BE_QUOTED.has(directiveValueEntry) || directiveValueEntry.startsWith("nonce-") || directiveValueEntry.startsWith("sha256-") || directiveValueEntry.startsWith("sha384-") || directiveValueEntry.startsWith("sha512-")
const warnIfDirectiveValueEntryShouldBeQuoted = value => {
if (shouldDirectiveValueEntryBeQuoted(value)) {
console.warn(`Content-Security-Policy got directive value \`${value}\` which should be single-quoted and changed to \`'${value}'\`. This will be an error in future versions of Helmet.`)
}
}
const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
function normalizeDirectives(options) {
const defaultDirectives = getDefaultDirectives()
const {useDefaults = true, directives: rawDirectives = defaultDirectives} = options
const result = new Map()
const directiveNamesSeen = new Set()
const directivesExplicitlyDisabled = new Set()
for (const rawDirectiveName in rawDirectives) {
if (!has(rawDirectives, rawDirectiveName)) {
continue
}
if (rawDirectiveName.length === 0 || /[^a-zA-Z0-9-]/.test(rawDirectiveName)) {
throw new Error(`Content-Security-Policy received an invalid directive name ${JSON.stringify(rawDirectiveName)}`)
}
const directiveName = dashify(rawDirectiveName)
if (directiveNamesSeen.has(directiveName)) {
throw new Error(`Content-Security-Policy received a duplicate directive ${JSON.stringify(directiveName)}`)
}
directiveNamesSeen.add(directiveName)
const rawDirectiveValue = rawDirectives[rawDirectiveName]
let directiveValue
if (rawDirectiveValue === null) {
if (directiveName === "default-src") {
throw new Error("Content-Security-Policy needs a default-src but it was set to `null`. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.")
}
directivesExplicitlyDisabled.add(directiveName)
continue
} else if (typeof rawDirectiveValue === "string") {
directiveValue = [rawDirectiveValue]
} else if (!rawDirectiveValue) {
throw new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}`)
} else if (rawDirectiveValue === dangerouslyDisableDefaultSrc) {
if (directiveName === "default-src") {
directivesExplicitlyDisabled.add("default-src")
continue
} else {
throw new Error(`Content-Security-Policy: tried to disable ${JSON.stringify(directiveName)} as if it were default-src; simply omit the key`)
}
} else {
directiveValue = rawDirectiveValue
}
for (const element of directiveValue) {
if (typeof element === "string") {
if (isDirectiveValueInvalid(element)) {
throw new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}`)
}
warnIfDirectiveValueEntryShouldBeQuoted(element)
}
}
result.set(directiveName, directiveValue)
}
if (useDefaults) {
Object.entries(defaultDirectives).forEach(([defaultDirectiveName, defaultDirectiveValue]) => {
if (!result.has(defaultDirectiveName) && !directivesExplicitlyDisabled.has(defaultDirectiveName)) {
result.set(defaultDirectiveName, defaultDirectiveValue)
}
})
}
if (!result.size) {
throw new Error("Content-Security-Policy has no directives. Either set some or disable the header")
}
if (!result.has("default-src") && !directivesExplicitlyDisabled.has("default-src")) {
throw new Error("Content-Security-Policy needs a default-src but none was provided. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.")
}
return result
}
function getHeaderValue(req, res, normalizedDirectives) {
let err
const result = []
normalizedDirectives.forEach((rawDirectiveValue, directiveName) => {
let directiveValue = ""
for (const element of rawDirectiveValue) {
if (typeof element === "function") {
const newElement = element(req, res)
warnIfDirectiveValueEntryShouldBeQuoted(newElement)
directiveValue += " " + newElement
} else {
directiveValue += " " + element
}
}
if (!directiveValue) {
result.push(directiveName)
} else if (isDirectiveValueInvalid(directiveValue)) {
err = new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}`)
} else {
result.push(`${directiveName}${directiveValue}`)
}
})
return err ? err : result.join(";")
}
const contentSecurityPolicy = function contentSecurityPolicy(options = {}) {
const headerName = options.reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"
const normalizedDirectives = normalizeDirectives(options)
return function contentSecurityPolicyMiddleware(req, res, next) {
const result = getHeaderValue(req, res, normalizedDirectives)
if (result instanceof Error) {
next(result)
} else {
res.setHeader(headerName, result)
next()
}
}
}
contentSecurityPolicy.getDefaultDirectives = getDefaultDirectives
contentSecurityPolicy.dangerouslyDisableDefaultSrc = dangerouslyDisableDefaultSrc
exports.dangerouslyDisableDefaultSrc = dangerouslyDisableDefaultSrc
exports.default = contentSecurityPolicy
exports.getDefaultDirectives = getDefaultDirectives
module.exports = exports.default
module.exports.default = module.exports