Strict Content Security Policy

How the injected guard interacts with Content Security Policy: the four modes, styles, rollout and violation reading.

Do I need this guide?

Gate One does not add a Content Security Policy unless a route declares a csp block. If you only proxy a page through Gate One, with no csp block and no custom injection, there is nothing to do: the page’s own policy (if any) passes through unchanged.

your situationwhat to do

page proxied by Gate One, no csp block, no custom injection, and the page sends no CSP

nothing: Gate One adds no CSP.

page proxied, but the page already sends a CSP

the browser enforces it. The guard’s inline script/style must be allowed by that policy, or the guard is blocked. Configure Gate One csp.mode: "merge" (script hashes) or "nonce", and make the app’s style-src compatible with the guard style. See “An application that already has a CSP”.

you enable csp on a route

this guide applies.

you inject custom HTML/CSS/JS (inject, userscripts, plugins)

this guide applies: your injected content must satisfy the policy.

you proxy an external site that sends a CSP

both policies are enforced; the origin’s policy can block the guard. See “External sites and upstream”.

You need this guide in the last four cases.

What Gate One injects

For a frontend route with the guard active, Gate One adds:

  • an inline <script> (the guard: session probe, login recovery, UI gating, user badge);

  • a <style> element created at runtime by the guard, carrying the CSP nonce of the response;

  • optionally PWA wiring (manifest link, service worker registration) and custom injections from the route bundle.

Since the guard’s UI is entirely class-based, it does not set style="…​" attributes and does not need 'unsafe-inline'. The one runtime <style> is nonced from document.currentScript.nonce.

The four modes

modebehaviour

off

Nothing is emitted or amended. The origin’s policy (if any) is the only one.

merge

The origin policy is preserved; the gateway only appends the hashes of its own injected scripts and of the guard stylesheet to script-src-elem / script-src / default-src and style-src-elem / style-src / default-src. No policy of its own. This is the default for a proxified external site that already sends a CSP.

hash

Gate One emits its own policy; the guard script and the guard stylesheet are allow-listed by SHA-256 hashes, and explicit script-src / style-src sources apply. The body is not rewritten.

nonce

Gate One emits its own policy with a per-response nonce and stamps inline <script> tags. The guard’s runtime <style> carries the same nonce. This is the recommended mode for applications that accept a per-response nonce.

own: true removes the origin’s Content-Security-Policy (and -Report-Only) headers and lets the gateway’s policy be the only one. It is opt-in, logged, and only valid with mode: "nonce" or "hash" and reportOnly: false. Use it only for a site you control/accept and for which the configured policy is complete. Without own, the origin policy remains enforced in addition to the gateway’s.

reportOnly: true emits Content-Security-Policy-Report-Only: the policy is evaluated and violations are reported, but nothing is blocked. Use it to roll out a policy without breaking the site.

Nonce semantics — read this before enforcing

In nonce mode, Gate One stamps every inline <script> in the HTML response (unless it already carries a nonce). This includes the application’s own inline scripts and any custom injection. As a consequence:

  • it removes the need for 'unsafe-inline' in script-src;

  • it does not protect against a <script> injected into the HTML by a server-side vulnerability, because that script is stamped too.

If you need protection against server-side HTML injection, use the hash or merge mode with a strict origin policy, and keep application scripts in external files.

stampScripts:

  • absent / inline (default): only <script> tags without src are stamped; external scripts stay governed by the host allow-list;

  • all: every <script> tag is stamped (only meaningful with a host allow-list or 'strict-dynamic').

Styles: the real constraint

CSS has two different mechanisms, and only one accepts a nonce:

mechanismcontrolled bynonce?

<style>…</style>

style-src-elem (falls back to style-src)

yes

style="…" attribute

style-src-attr (falls back to style-src)

no

A nonce (or hash) never applies to a style attribute. To obtain a strict policy without 'unsafe-inline':

  • use external CSS files (style-src 'self');

  • use classes instead of element.style / style="…​";

  • let Gate One nonce or hash its own runtime <style> (it already does: nonce in nonce mode, guard-stylesheet hash in hash/merge mode);

  • for third-party widgets that insist on style attributes, isolate the exception with style-src-attr 'unsafe-inline' rather than relaxing the whole style-src.

Gate One’s guard follows this rule: badge, banners, chips and gating states are classes; hidden gated elements use [data-gtw-gated-off]{display:none}. A static test fails the build if an inline style reappears in the guard.

Application checklist for styles

  • no style="…​" attributes (including generated by JS);

  • no element.style = …​ / .style.cssText / .style.display;

  • use classList and classes, or an external stylesheet;

  • no <style> without a nonce (the gateway does not stamp application <style> tags; move them to a CSS file);

  • SVG style attributes are style attributes too;

  • third-party embeds: check whether they need style-src-attr 'unsafe-inline'.

Scripts and event handlers

A strict script-src without 'unsafe-inline' also blocks:

  • onclick="…​" and other inline event handlers (script-src-attr);

  • javascript: URLs;

  • eval(), new Function(), setTimeout("code") ('unsafe-eval');

  • document.write of scripts without a nonce/hash.

Use addEventListener, external scripts and data attributes.

With nonce mode, inline <script> tags are stamped by the gateway, so existing inline scripts work — but see the server-side injection caveat above. For the strictest setup, move scripts to external files and rely on script-src 'self' only.

A strict configuration

Start report-only:

"csp": {
  "mode": "nonce",
  "reportOnly": true,
  "directives": {
    "default-src": "'none'",
    "script-src": "'self'",
    "style-src": "'self'",
    "img-src": ["'self'", "data:"],
    "font-src": "'self'",
    "connect-src": ["'self'", "https://auth.cladone.com"],
    "worker-src": "'self'",
    "frame-ancestors": "'none'",
    "object-src": "'none'",
    "base-uri": "'none'",
    "form-action": "'self'"
  }
}

Why these directives:

directiverole

default-src 'none'

deny by default; every resource type must be explicitly allowed.

script-src 'self'

external same-origin scripts; the gateway adds its nonce and hashes.

style-src 'self'

external same-origin styles; the gateway adds its nonce.

img-src

add data: only if the application uses data-URI icons.

font-src

self-hosted fonts; add the font CDN if used.

connect-src

fetch, XHR, EventSource and WebSocket endpoints. Add the IdP origin if the application calls it directly.

worker-src

the PWA service worker ('self').

frame-ancestors 'none'

anti-clickjacking.

object-src 'none', base-uri 'none'

remove plugin/base-tag injection vectors.

form-action 'self'

restrict form destinations.

Keep reportOnly: true until the reports are clean, then switch to false. A policy that emits violations is not protecting the page in report-only mode.

For a site that you control and whose origin CSP is structurally incompatible (e.g. hosts pinned to the origin), Gate One can own the policy:

"csp": {
  "mode": "nonce",
  "own": true,
  "directives": { "...": "a COMPLETE policy for this site" }
}

own removes the origin’s Content-Security-Policy and -Report-Only headers, logs a warning, and cannot be combined with reportOnly: true. Do not use it for a site you do not control.

Reading violations

The gateway serves a collector (default /__gate-one_csp/report) and logs one line per violation:

CSP VIOLATION host="app.example.com" directive="style-src-elem"
blocked="inline" document="https://app.example.com/" source="..." line=572

Typical signatures:

directiveblockedusual cause / fix

style-src-elem

inline

a <style> without nonce: move to an external CSS file (Gate One’s own guard style is nonced).

style-src-attr

inline

style="…​" or element.style: use classes; or isolate with style-src-attr 'unsafe-inline'.

script-src-elem / script-src

inline

an inline script; in nonce mode the gateway stamps it, so check that the policy is not the origin’s (the origin policy can still block it).

script-src-attr

inline

onclick= and friends: use addEventListener.

connect-src

https://…​;

missing API/SSE/WebSocket origin.

img-src

data: / CDN

missing source.

frame-src

https://…​;

embedded iframe origin.

worker-src

URL

service worker origin.

Browser DevTools also shows the violating element and the computed policy; the gateway log is the aggregate view across users.

An application that already has a CSP

If the application (or the proxified origin) sends its own Content-Security-Policy, the browser enforces both policies: the application’s and Gate One’s, whichever is stricter.

For scripts, csp.mode: "merge" appends the SHA-256 hashes of the guard and other gateway-injected scripts to the origin’s script-src-elem / script-src / default-src. The origin policy does not need 'unsafe-inline' for the guard.

For styles, merge appends the SHA-256 hash of the guard’s stylesheet to the origin’s style-src-elem / style-src / default-src. The guard’s runtime <style> is then allowed by hash, without 'unsafe-inline', and the origin policy stays in place. The guard stylesheet is deterministic per route (static classes + the route’s uiGate selectors), so the hash is stable and cache-friendly.

This covers the guard, not the application’s own inline styles: style="…​" attributes and application <style> tags still need to be refactored to classes/external CSS (or covered by the application’s own nonce/hash). If the origin policy says style-src 'none', the gateway appends the guard hash, so the guard style is allowed while the origin’s other restrictions remain.

nonce mode still adds the gateway’s nonce for the guard style; the origin policy is not amended with that nonce (a per-response nonce cannot be known by the origin), but the guard stylesheet hash is appended by merge/hash/nonce modes via the origin amendment.

own: true is the explicit alternative when the origin policy is fundamentally incompatible: the origin headers are removed and only the gateway policy applies. Use it only for a site you control/accept and with a complete policy.

External sites and upstream

For a proxified external site that already sends a CSP, the browser enforces both policies: the origin’s and the gateway’s, whichever is stricter. Gate One never removes or weakens the origin policy.

Consequences:

  • merge appends the hashes of the guard script and of the guard stylesheet to the origin’s script-src-elem and style-src-elem (fallbacks script-src/style-src/default-src). A strict origin policy no longer blocks the guard, without 'unsafe-inline'.

  • if the origin policy is incompatible for another reason (hosts pinned to the origin, script-src 'none' for the site’s own code, etc.), use own: true or upstream.responseHeaders.remove only for a site you control/accept, and provide a complete policy;

  • upstream.responseHeaders.remove is still a blunt tool: it is not logged by the CSP layer and removes the origin’s protections. Prefer merge or own.

  • external sites that use inline styles themselves will need style-src-attr 'unsafe-inline' or a refactor you do not control.

The guard is handled by hash; the remaining difficulty is the site’s own content, not the guard. Document the exception per route.

PWA, MCP, SSE and WebSocket

  • PWA: worker-src 'self' for the service worker; manifest-src 'self' if your policy declares it; the Workbox runtime is self-hosted. A CSP nonce is per response: a cached HTML page keeps its nonce, so the cached response must keep the matching CSP header. Workbox caches the response headers with the body; verify this after a cache version change. For long-lived cached HTML, hash mode is more cache-friendly than a per-response nonce.

  • MCP: the MCP endpoint is same-origin (/mcp); connect-src 'self' is enough for HTTP. If an MCP client uses a WebSocket, add its origin.

  • SSE (EventSource) and WebSocket are governed by connect-src, not script-src.

  • IdP redirects: login is a top-level navigation, not connect-src; form-action applies only to forms, not to location.href.

Rollout procedure

  1. Enable reportOnly: true with the strict directives.

  2. Load the main pages; watch the browser console and the gateway CSP VIOLATION logs.

  3. Fix one directive family at a time (styles first: they are the most common).

  4. Re-run the UI smoke tests and a real navigation with a session.

  5. Switch reportOnly: false only after a clean period.

  6. Keep a rollback: reportOnly: true (or mode: "off") can be applied by SIGHUP without a restart.

Minimal test commands

# the header actually emitted (nonce changes per response)
curl -sD - -o /dev/null https://app.example.com/ \
  | grep -i '^content-security-policy'

# aggregate violations
docker logs <gate-one-container> 2>&1 | grep 'CSP VIOLATION'

# route-level validation
gate-one -config api-routes.json -check

Known limitations

  • Gate One does not stamp application <style> tags; use external CSS.

  • Style attributes cannot be nonced: classes or style-src-attr 'unsafe-inline'.

  • In nonce mode, all inline scripts are stamped; it is not a defense against server-side HTML injection. Use hash/merge plus a strict origin policy.

  • A proxified external site’s CSP is enforced in addition to the gateway’s; an incompatible origin policy can block the guard.

  • Third-party widgets may require a documented style-src-attr or script-src exception.

Gate One’s own UI (guard, badge, banners) is strict by construction and does not require 'unsafe-inline'.