feat(jetstream): nonce the emitted script tags (3.3 of 5 towards strict-dynamic) - #5752
Open
nabramovitz wants to merge 2 commits into
Open
feat(jetstream): nonce the emitted script tags (3.3 of 5 towards strict-dynamic)#5752nabramovitz wants to merge 2 commits into
nabramovitz wants to merge 2 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third step of #5743, first half. Puts the CSP nonce on the console's script
tags so a later
strict-dynamicpolicy 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>:The content hash lives in
src, outside the closing run, sotype="module"></script>is a literal that can be matched without a regex oran HTML rewrite — the same technique
injectNoncealready uses for<style>and
<app-root>. Verified against six builtindex.htmlfiles from fivebranches 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 nolonger occurs and a second pass is a no-op — the guarantee
TestInjectNonceOnAlreadyInjectedHTMLKeepsFirstNoncepins.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.htmlcarries no script tags at all — thebuild adds them — so
TestInjectNonceOnRealIndexHTMLreads a file that cannotshow what has to be matched, and there is no built
index.htmlin the repo forit to read instead. The backend suite never runs the frontend build.
scriptNonceGapcloses that at startup by comparing opening<script>tagsagainst 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 shipun-nonced and get blocked under a policy without
'unsafe-inline'.Verification
(7 cases). Scripts folded into the existing re-application test.
TestInjectNonceOnRealIndexHTMLnow also asserts the source has no scripttags, so if it ever gains them the test says to assert on them there rather
than lean on the startup check.
injectNoncerun against all six real builtindex.htmlfiles: 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/isnot in the repo — and is not included here.
gap check firing on a real build.
make check gategreen.Refs #5743