UNPKG

helmet-csp

Version:

Content Security Policy middleware

164 lines (158 loc) 7.09 kB
"use strict" Object.defineProperties(exports, {__esModule: {value: true}, [Symbol.toStringTag]: {value: "Module"}}) const dashify = str => str.replace(/[A-Z]/g, capitalLetter => "-" + capitalLetter.toLowerCase()) const errify = err => (err instanceof Error ? err : new Error(String(err))) const isString = value => typeof value === "string" const throwErrorIfExists = err => { if (err) throw err } const dangerouslyDisableDefaultSrc = Symbol("dangerouslyDisableDefaultSrc") 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 = () => ({ "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 parseDirectiveName = rawDirectiveName => { if (rawDirectiveName.length === 0 || !/^[a-z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(rawDirectiveName)) { throw new Error(`Content-Security-Policy received an invalid directive name ${JSON.stringify(rawDirectiveName)}`) } return dashify(rawDirectiveName) } const getDirectiveValueValidationError = (directiveName, directiveValue) => (/;|,/.test(directiveValue) ? new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}`) : null) const getDirectiveValueEntryValidationError = (directiveName, directiveValueEntry) => (SHOULD_BE_QUOTED.has(directiveValueEntry) || directiveValueEntry.startsWith("nonce-") || directiveValueEntry.startsWith("sha256-") || directiveValueEntry.startsWith("sha384-") || directiveValueEntry.startsWith("sha512-") ? new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}. ${JSON.stringify(directiveValueEntry)} should be quoted`) : null) const stringifyDirectiveValue = directiveValue => { if (Array.isArray(directiveValue)) { return directiveValue.every(isString) ? directiveValue.join(" ") : null } if (directiveValue instanceof Set) { return stringifyDirectiveValue(Array.from(directiveValue)) } return null } const parseDirectives = ({useDefaults = true, directives: rawDirectives = {}}) => { const result = new Map(useDefaults ? Object.entries(getDefaultDirectives()) : []) let hasDisabledDefaultSrc = false const directiveNamesSeen = new Set() for (const rawDirectiveName in rawDirectives) { if (!Object.hasOwn(rawDirectives, rawDirectiveName)) { continue } const directiveName = parseDirectiveName(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`.") } result.delete(directiveName) continue } else if (typeof rawDirectiveValue === "string") { directiveValue = [rawDirectiveValue] } else if (rawDirectiveValue === dangerouslyDisableDefaultSrc) { if (directiveName === "default-src") { hasDisabledDefaultSrc = true result.delete(directiveName) 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 if (rawDirectiveValue) { directiveValue = rawDirectiveValue } else { throw new Error(`Content-Security-Policy received an invalid directive value for ${JSON.stringify(directiveName)}`) } for (const element of directiveValue) { if (typeof element !== "string") continue throwErrorIfExists(getDirectiveValueValidationError(directiveName, element) ?? getDirectiveValueEntryValidationError(directiveName, element)) } result.set(directiveName, directiveValue) } if (!result.size) { throw new Error("Content-Security-Policy has no directives. Either set some or disable the header") } if (!result.has("default-src") && !hasDisabledDefaultSrc) { 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`.") } let stringResult = "" let shouldUseStringResult = true for (const [directiveName, directiveValue] of result) { const directiveValueString = stringifyDirectiveValue(directiveValue) if (directiveValueString === null) { shouldUseStringResult = false break } else { if (stringResult) stringResult += ";" stringResult += directiveValueString ? `${directiveName} ${directiveValueString}` : directiveName } } return shouldUseStringResult ? stringResult : result } function getHeaderValue(req, res, normalizedDirectives) { const result = [] for (const [directiveName, rawDirectiveValue] of normalizedDirectives) { let directiveValue = "" for (const element of rawDirectiveValue) { if (typeof element === "function") { let newElement try { newElement = element(req, res) } catch (err) { return errify(err) } const err = getDirectiveValueEntryValidationError(directiveName, newElement) if (err) return err directiveValue += " " + newElement } else { directiveValue += " " + element } } if (directiveValue) { const err = getDirectiveValueValidationError(directiveName, directiveValue) if (err) return err result.push(`${directiveName}${directiveValue}`) } else { result.push(directiveName) } } return result.join(";") } const contentSecurityPolicy = function contentSecurityPolicy(options = {}) { const headerName = options.reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy" const parsedDirectives = parseDirectives(options) // A special case for performance. if (typeof parsedDirectives === "string") { return function contentSecurityPolicyMiddleware(_req, res, next) { res.setHeader(headerName, parsedDirectives) next() } } return function contentSecurityPolicyMiddleware(req, res, next) { const result = getHeaderValue(req, res, parsedDirectives) 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