Rolling Out a Content Security Policy with KrakenJS Middleware Without Breaking Your UI
An architecture note on deploying a Content-Security-Policy header via KrakenJS's lusca middleware: minimal config, report-only rollout, nonce handling, trust boundaries, and the failure modes that break UIs.
11 Nov 2025, 03:57 UTC

The problem: XSS mitigation that fails silently or breaks the page
A Content Security Policy (CSP) header tells the browser which sources of scripts, styles, and other resources it may load for a page. Done well, it sharply limits the damage from cross-site scripting (XSS), because injected markup can no longer execute attacker-controlled code. Done carelessly, it either blocks legitimate third-party resources (fonts, analytics, payment widgets) or sits in a half-configured state nobody notices.
KrakenJS — PayPal's configuration-driven layer on top of Express — ships a CSP middleware that builds the header from a declarative policy object. This note covers the smallest design that works, where the trust boundaries sit, how to verify the rollout, and the failure modes that should change your design. One caveat up front: KrakenJS is in maintenance mode and receives little active development. The middleware described here is stable and stateless, but treat the framework itself as frozen and pin your dependency versions.
Requirements
- Set a syntactically valid
Content-Security-Policyheader on every HTML response. - Fail fast at startup if the policy is malformed, rather than deploying a broken header.
- Support a phased rollout: monitor violations before enforcing, so a too-strict policy never takes the UI down.
- Allow inline scripts that the application intentionally renders, without resorting to
'unsafe-inline'. - Add negligible latency — this runs on every request.
The smallest suitable design
KrakenJS middleware is declared in config/config.json and wired into the Express pipeline by the framework. The CSP module takes a policy object and serializes it into the header. A minimal enforced policy looks like this:
{
"middleware": {
"csp": {
"enabled": true,
"module": {
"name": "lusca",
"arguments": [{
"csp": {
"policy": {
"default-src": "'self'",
"script-src": "'self' https://cdn.example.com",
"style-src": "'self'",
"img-src": "'self' data:"
},
"reportOnly": false
}
}]
}
}
}
}In KrakenJS 1.x/2.x the CSP implementation comes from the lusca security module, which Kraken wires in by default. The exact argument shape varies slightly between lusca versions, so check the README of the lusca version your lockfile resolves to before copying this verbatim. The key properties are policy (a map of directive names to source lists) and reportOnly.
The design stays minimal because the middleware is stateless: it computes the header string once at startup and sets it on each response. There is no per-request policy assembly unless you add nonces (below), and no external calls.
Phased rollout with report-only
Set reportOnly: true first. The middleware then emits Content-Security-Policy-Report-Only instead of the enforcing header. Browsers evaluate the policy and send violation reports but block nothing. Run this in production for at least one full traffic cycle, collect the reports, fix or whitelist the legitimate sources that appear, and only then flip to enforcing. Skipping this phase is how teams end up hotfixing a policy that blocked their payment provider's JavaScript.
Nonces for intentional inline scripts
If your templates render inline <script> blocks you control, prefer a nonce over 'unsafe-inline'. A nonce is a random per-request token; the browser only executes inline scripts whose nonce attribute matches the header value. This means a small piece of per-request middleware that generates the value, stores it on res.locals for the template engine, and appends 'nonce-<value>' to script-src. That addition makes the middleware no longer fully stateless, but the cost is one random-value generation per request — still negligible. The hard requirement: every rendered script tag must receive the nonce. A template partial that misses it will be blocked, and the failure appears only in the browser console, not in server logs.
Trust and data boundaries
Three boundaries matter here:
- Server to browser: the header is advisory; enforcement happens client-side. CSP does not sanitize anything on the server — you still need output encoding in templates. CSP is the second line of defense for when encoding fails.
- Policy config to runtime: the policy object is trusted configuration. Keep it in version-controlled config, not in a database or admin UI, so a policy change goes through code review. lusca validates the policy at startup and throws on malformed directives, which is the behavior you want — a boot failure is loud; a silently dropped directive is not.
- Browser to report endpoint: violation reports are attacker-influenced input. If you add a
report-uri(orreport-toin newer browsers) directive, the receiving endpoint must not require authentication cookies (reports arrive without them), must rate-limit, and must treat report bodies as untrusted JSON. Otherwise you have built an unauthenticated log-flooding vector.
Operational checks
After enabling the middleware, verify from outside the application:
curl -sI https://your-app.example/ | grep -i content-security-policyRun this from any shell against a deployed environment; no special permissions needed. Confirm the header name matches the mode you expect (Content-Security-Policy when enforcing, ...-Report-Only when monitoring) and that the directives match your config. Then open a page in a browser, open the developer console, and confirm there are no unexpected CSP violation messages. For a negative test, temporarily add a script tag pointing at a non-whitelisted origin in a staging template and confirm the browser blocks it and logs a violation. Finally, paste the header value into a CSP evaluator tool to catch syntax issues (a missing semicolon or a typo'd scheme source) that browsers may parse leniently.
Failure modes
- Overly strict policy: third-party widgets, fonts, or analytics stop loading. Symptom is console errors and a visibly broken UI; fix by whitelisting the specific origin, not by adding
'unsafe-inline'or*. - Missing nonces: with a nonce-based policy, any template that forgets the attribute produces blocked scripts with no server-side error. Mitigate by rendering scripts through a single helper that always injects the nonce.
- Report endpoint abuse: spoofed or flooded reports. Mitigate with rate limiting and payload size caps.
- Header set twice: if a reverse proxy or another middleware also sets CSP, browsers enforce the intersection of both headers, which is almost never what you intend. Check for duplicates with the
curl -sIcommand above — two CSP lines in the output is a bug.
What would change this design
If you need per-route policies (for example, a sandboxed admin section), a single global middleware is wrong — mount separate policy instances per router. If you adopt nonce-based policies everywhere, consider moving CSP generation into explicit middleware you control rather than static config, since the config-driven approach assumes a fixed header. And if you are starting a new service today, weigh whether to build on KrakenJS at all: plain Express with helmet's CSP middleware gives you the same behavior on an actively maintained base.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.