Skip to content

feat(jetstream): nonce the emitted script tags (3.3 of 5 towards strict-dynamic) - #5752

Open
nabramovitz wants to merge 2 commits into
cloudfoundry:developfrom
nabramovitz:norm/feat/csp-script-nonce
Open

feat(jetstream): nonce the emitted script tags (3.3 of 5 towards strict-dynamic)#5752
nabramovitz wants to merge 2 commits into
cloudfoundry:developfrom
nabramovitz:norm/feat/csp-script-nonce

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

Third step of #5743, first half. Puts the CSP nonce on the console's script
tags so a later strict-dynamic policy has something to rest on.

Nothing observable changes yet. The policy still admits scripts via 'self',
so the nonce is inert until the policy tightens — that is the other half of
step 3 and is not in this PR.

What the build actually emits

Angular appends both scripts to the end of <body>:

<script src="polyfills-PJGX3BAA.js" type="module"></script><script src="main-YDIHTKZY.js" type="module"></script>

The content hash lives in src, outside the closing run, so
type="module"></script> is a literal that can be matched without a regex or
an HTML rewrite — the same technique injectNonce already uses for <style>
and <app-root>. Verified against six built index.html files from five
branches spanning three weeks: two scripts each, identical form, attribute
order never varying.

Matching that literal also preserves the non-re-appliable property for free.
After injection the tag reads type="module" nonce="X">, so the literal no
longer occurs and a second pass is a no-op — the guarantee
TestInjectNonceOnAlreadyInjectedHTMLKeepsFirstNonce pins.

Why there is a startup check

The script form cannot be pinned by a test the way the other two forms are.
src/frontend/packages/core/src/index.html carries no script tags at all — the
build adds them — so TestInjectNonceOnRealIndexHTML reads a file that cannot
show what has to be matched, and there is no built index.html in the repo for
it to read instead. The backend suite never runs the frontend build.

scriptNonceGap closes that at startup by comparing opening <script> tags
against matchable ones, warning when they differ. It counts rather than testing
for presence: presence alone would pass a template where only some scripts
match, and would miss an inline <script> entirely — both of which ship
un-nonced and get blocked under a policy without 'unsafe-inline'.

Verification

  • New unit tests for the script match, hash independence, and the gap check
    (7 cases). Scripts folded into the existing re-application test.
  • TestInjectNonceOnRealIndexHTML now also asserts the source has no script
    tags, so if it ever gains them the test says to assert on them there rather
    than lean on the startup check.
  • injectNonce run against all six real built index.html files: 2 scripts,
    2 styles, 1 app-root nonced in every one, no un-nonced module script
    surviving, second injection a no-op. That check was a throwaway — dist/ is
    not in the repo — and is not included here.
  • Reverse-tested: corrupting the literal turns four tests red, including the
    gap check firing on a real build.
  • make check gate green.

Refs #5743

Angular's build appends the console's two scripts to <body> as
<script src="NAME-HASH.js" type="module"></script>. The hash sits in
src, outside the closing run, so `type="module"></script>` is a stable
literal to match on — no regex and no HTML rewrite, the same technique
already used for <style> and <app-root>. Checked against six built
index.html files spanning five branches and three weeks: two scripts
each, identical form, attribute order never varying.

Matching that literal also keeps injection non-re-appliable for free.
Once a nonce is spliced in the tag reads `type="module" nonce="X">`,
so the literal no longer occurs and a second pass is a no-op — the
guarantee TestInjectNonceOnAlreadyInjectedHTMLKeepsFirstNonce pins.

Nothing observable changes yet: the policy still admits scripts via
'self', so the nonce is inert until strict-dynamic lands. This is the
plumbing that change will rest on.

The script form cannot be pinned by a test the way the style and
app-root forms are. The source index.html carries no script tags at
all — the build adds them — so TestInjectNonceOnRealIndexHTML reads a
file that cannot show what has to be matched, and no built index.html
exists in the repo for it to read instead. scriptNonceGap closes that
at startup: it compares opening <script> tags against matchable ones
and warns when they differ, which is when scripts would otherwise be
served un-nonced and blocked by a policy without 'unsafe-inline'.

It counts rather than testing for presence deliberately. Presence
alone would pass a template where only some scripts match, and would
miss an inline <script> entirely; both ship un-nonced.

Refs cloudfoundry#5743

Signed-off-by: Norman Abramovitz <nabramovitz@juno.com>
Signed-off-by: Norman Abramovitz <nabramovitz@juno.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant