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?) #

stable0.10.16soc2gdpr
{
  {
    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?) #

stable0.10.16

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?) #

stable0.10.16
{
  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?) #

stable0.15.13
{
  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?) #

stable0.15.13
{
  (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.