CSP3 builder
Content Security Policy Level 3 (W3C CSP3 / candidate recommendation 2024-09) directive builder. The framework's b.middleware.securityHeaders module ships a strict default CSP; this module exposes the per-directive surface so operators can build out a policy by composition without hand-concatenating strings (which is the failure mode behind most CSP-bypass incidents — a missing 'self', an accidental 'unsafe-inline', or quoting that the UA silently ignores).
Posture: - Refuses 'unsafe-inline' / 'unsafe-eval' / 'unsafe-hashes' in any script-* directive unless explicitly acknowledged via acknowledgeUnsafe: true with a documented reason. The CSP3 spec defines these as no-ops when 'strict-dynamic' is present, but UAs that haven't shipped strict-dynamic full support still honor the unsafe keywords — refusing at builder time prevents shipping an unintentional bypass. - Defaults require-trusted-types-for 'script' + the named Trusted Types policy "default" when operators wire any script-* source (Trusted Types is the strongest defense against DOM-XSS available in browsers today). - Refuses data: in img-src / media-src / font-src unless the operator explicitly opts in (data: URLs sidestep most CSP defenses and are a common XSS pivot). - Refuses https: / * as a source in any directive (catch-all sources defeat the principle of least privilege).
v0.10.16 light-up: builder + nonce helper + hash helper. Trusted Types policy declaration helper. CSP-report-uri / report-to wiring composes with b.middleware.cspReport (existing).
Spec citations: - W3C CSP Level 3 (CR 2024-09) - W3C Trusted Types (CR 2023-05) - Reporting API Level 1 (W3C 2024)
b.csp.build(directives, opts?) #
{
{
acknowledgeUnsafe?: boolean, // default false — refuses 'unsafe-*' otherwise
allowDataImages?: boolean, // default false — refuses data: in img-src/media-src/font-src
trustedTypesPolicies?: string[], // policy names allowed by trusted-types directive
requireTrustedTypes?: boolean, // default true when any script-* is set
}
}
Build a CSP3 header value from a per-directive object. Each key is a CSP directive name; each value is an array of sources (strings). Returns a single string ready for Content-Security-Policy: or Content-Security-Policy-Report-Only:.
var policy = b.csp.build({
"default-src": ["'self'"],
"script-src": ["'self'", "'nonce-" + req.cspNonce + "'"],
"style-src": ["'self'"],
"img-src": ["'self'"],
"connect-src": ["'self'"],
"frame-ancestors": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
"object-src": ["'none'"],
"report-to": ["default"],
}, { trustedTypesPolicies: ["default", "app-sanitizer"] });
res.setHeader("Content-Security-Policy", policy);
b.csp.nonce(byteLen?) #
Generate a CSP3 nonce — base64url-encoded random bytes for use as 'nonce- in script-src / style-src. The CSP3 spec recommends at least 128 bits of entropy (16 bytes); this primitive uses 32 bytes by default for a generous margin.
req.cspNonce = b.csp.nonce();
res.setHeader("Content-Security-Policy",
b.csp.build({ "script-src": ["'self'", "'nonce-" + req.cspNonce + "'"] }));
b.csp.hash(scriptBody, alg?) #
{
alg?: "sha256" | "sha384" | "sha512" // default sha384 (matches
// b.crypto.sri default)
}
Compute a CSP3 hash source for an inline script/style. Returns the ' token suitable for direct use as a script-src source.
var src = b.csp.hash("console.log('boot');");
// → "'sha384-abcd...'"
b.csp.mergeDirectives(base, additions, opts?) #
{
acknowledgeUnsafe: boolean, // allow an added 'unsafe-*' in a script directive
allowDataImages: boolean, // allow an added data: in img-src/media-src/font-src
}
Derive a per-route CSP from a strict base by ADDING hosts to named directives, leaving every other directive exactly as the base. The fix for the "load a third-party SDK on one route" case: take the framework's strict default (pass base omitted / undefined) and add https://js.stripe.com to script-src + frame-src for the checkout route only, without re-typing the whole policy or relaxing frame-ancestors / object-src / base-uri.
Additive only: each additions[directive] array is APPENDED (de-duped) to that directive's existing sources; a directive absent from the base is seeded from default-src (or 'self') first so it never lands wide-open. Only the ADDED sources are validated (CR/LF/NUL, catch-all */https:, data: in img/media/font, unsafe-* in script directives) — the trusted base round-trips untouched. Returns a policy string for the middleware csp: opt. Throws CspError on an unknown directive, a hostile directive name, or a rejected added source.
// Admit Stripe on the checkout route's script + frame directives only.
var routeCsp = b.csp.mergeDirectives(undefined, {
"script-src": ["https://js.stripe.com"],
"frame-src": ["https://js.stripe.com"],
});
// -> the strict default with those two hosts appended; everything else unchanged
b.csp.mergePermissionsPolicy(base, overrides, opts?) #
{
(none)
}
Derive a per-route Permissions-Policy from a strict base by replacing the allowlist of NAMED features only, leaving every other feature at its () deny default. The companion to mergeDirectives for the Permissions-Policy header: re-enable payment to (self "https://js.stripe.com") on the checkout route while camera / microphone / geolocation stay denied.
Each override value is validated as an RFC-9651 feature value-list (*, (), self, or a parenthesised origin list) — a value carrying a comma or CR/LF is refused so it can't inject a second feature or a header break. A feature not present in the base is added (opting it in); one present is replaced. Returns a header string for the middleware permissionsPolicy: opt. Throws CspError on a hostile feature name or a malformed value.
var routePp = b.csp.mergePermissionsPolicy(undefined, {
payment: '(self "https://js.stripe.com")',
});
// -> the strict default with payment re-enabled to that allowlist; all else denied
Last updated 2026-08-08T16:39:49.652Z by seeder.