Skip to content

CSP: raise the policy to Level 3 strict #5743

Description

@norman-abramovitz

With #5742 merged, the ZAP finding tracked in #5689 no longer reproduces and
the element half of #5705 is shipped. The scanner is quiet, but the policy is
not yet at the strongest level CSP offers. This issue collects what is left, so
the remaining steps are tracked somewhere their titles describe them.

Where the policy stands after #5742

default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
style-src-elem 'self' 'nonce-PLACEHOLDER' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com;
img-src 'self' data:;
connect-src 'self';
worker-src 'self' blob:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self'

Style elements are nonce-enforced, which is the CSP Level 3 mechanism for them.
Scripts are still governed by a host allowlist, which is CSP Level 1 thinking,
and several modern directives are absent.

A note on measurement

ZAP will not drive this work. Rule 10055 is satisfied and ZAP has no notion of
"how strict is this policy" beyond the specific patterns it looks for — it will
report a host-based script-src 'self' as perfectly fine. Anything below that
depends on 'strict-dynamic' should be judged with a tool that grades policies,
such as Google's CSP Evaluator, and confirmed in a browser with a violation
listener the way #5742 was. Worth saying up front so nobody chases a green
scanner as evidence of having finished.

The steps, roughly in order of value per unit of risk

1. object-src 'none'. Not declared today, so it inherits default-src 'self', which still permits same-origin plugin content. Every strict-CSP
reference profile sets this to 'none'. One token, no runtime behaviour to
break.

2. Violation reporting. No report-to endpoint exists, so an operator who
turns CSP on and hits a problem discovers it only when a user notices something
unstyled. Even report-only telemetry would have shortened the #5705
investigation considerably. This also makes every later step in this list
safely deployable, which is why it should come early rather than last.

3. script-src 'nonce-…' 'strict-dynamic'. The main event, and the step
that actually moves the policy to Level 3 strict. Under strict-dynamic the
host allowlist is ignored and trust propagates from nonced scripts to the
scripts they load, which is what makes an injected <script src="/…"> useless
to an attacker even when it is same-origin.

The server-side plumbing already exists: cspHeaderWithNonce substitutes every
occurrence of the placeholder, not just the first, so adding the token to a
second directive needs no backend change. What does need work is
injectNonce, which stamps <style> and <app-root> today and would need to
stamp the two build-emitted <script type="module"> tags as well.

The open question is Angular's lazy chunks. They arrive by dynamic import()
from an already-nonced module, which should inherit trust under
'strict-dynamic', but that needs verifying in a browser across a
lazy-route navigation rather than reasoning about — the failure mode is a route
that silently fails to load. The built-UI rig from #5742 is the right
instrument; ng serve cannot test any of this because it never runs
Jetstream's document handler.

4. Trusted Types (require-trusted-types-for 'script'). The strongest
option available and the largest lift. Angular supports it, but Monaco and
xterm are both unknowns, and #5705 is a good reminder of how that tends to go.
Worth scoping only after step 3 lands.

5. style-src-attr. Listed for completeness and explicitly not planned
here. It cannot be fixed by policy — CSP has no nonce or hash for attribute
values computed at runtime, and Monaco's per-line positioning and xterm's
per-cell colours are exactly that. It needs upstream conversion to the CSSOM,
which CSP exempts. Tracked at microsoft/vscode#288813.

Adjacent: the residual scanner warnings

The post-#5742 baseline scan leaves eight warnings, none High or Medium.
They are unrelated to CSP and unchanged by it, but they are the same class of
work and are cheap to fold in:

  • Strict-Transport-Security not set (Low, high confidence). Genuine, though
    in most deployments TLS terminates at a router in front of Jetstream, so the
    right layer needs deciding rather than assuming.
  • Permissions-Policy not set (Low). Straightforward addition.
  • Cross-Origin-Opener/Embedder/Resource-Policy not set (Low). COOP is the
    one with real value; COEP has knock-on effects on cross-origin resources and
    should not be added casually.

Two more are low-confidence pattern matches on the minified bundle and want a
human look before being treated as real: Dangerous JS Functions (one hit,
lowest confidence) and Timestamp Disclosure (five hits, lowest confidence —
ZAP flags any ten-digit number as a Unix timestamp, which in a minified bundle
is close to meaningless).

The remaining informational entries are cache observations, and one of them is
self-inflicted on purpose: Non-Storable Content on the console document is
a direct consequence of the Cache-Control: no-store the nonce mechanism
requires. It should stay exactly as it is, and is noted here so it does not get
"fixed" by someone reading the scan cold.

Scope

Steps 1 and 2 are small and independent. Step 3 is the substantial one and
should be its own PR with the same before/after browser evidence #5742 used.
Steps 4 and 5 are not proposed for now.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions