Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nonce support to bootstrap scripts and external runtime #26738

Merged
merged 17 commits into from May 1, 2023

Conversation

danieltott
Copy link
Contributor

@danieltott danieltott commented Apr 28, 2023

Summary

When using a Content Security Policy with the strict-dynamic source list keyword, all other non-nonce keywords and sources are ignored, specifically 'self'. This means that scripts that are inserted via <script src=... /> into the HTML violate the content security policy.

This fix also supports CSPs that are not using strict-dynamic, but are using nonce-xxx sources

Right now the render functions (renderToReadableStream, renderToPipeableStream etc) via createResponseState add the nonce attribute to inline scripts set in bootstrapScriptContent, but not scripts via bootstrapScripts and bootstrapModules. Also missing the nonce value are external runtime scripts, as well as scripts rendered at runtime that could be rendered on the server.

Adding nonce support to those scripts will allow them to pass a content-security-policy that uses strict-dynamic.

Related issues:

Changes:

  • Add nonce to bootstrap scripts/modules in ReactFizzConfigDOM -> createResponseState()
  • Add nonce property to the return object for createResponseState()
  • Pass nonce prop to internalPreinitScript() (external runtime scripts)
  • Inject nonce prop in pushStartInstance() where type=='script' if it exists in responseState (removed based on Add nonce support to bootstrap scripts and external runtime #26738 (comment))
  • Updated FizzTestUtils.js to re-add nonce attribute after executing inline scripts

How did you test this change?

I added the following tests:

  • ReactDOMFizzServer-test - it should render scripts with nonce added
    Test that nonce values are applied correctly in rendered scripts
  • ReactDOMFizzServer-test - it should support nonce for bootstrap and runtime scripts
    Update the existing nonce test to test all bootstrap scripts, as well as the runtime script

@react-sizebot
Copy link

react-sizebot commented Apr 28, 2023

Comparing: 18282f8...d2d41c4

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 164.04 kB 164.04 kB = 51.72 kB 51.72 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 170.34 kB 170.34 kB = 53.63 kB 53.63 kB
facebook-www/ReactDOM-prod.classic.js = 567.04 kB 567.04 kB = 100.16 kB 100.16 kB
facebook-www/ReactDOM-prod.modern.js = 550.78 kB 550.78 kB = 97.36 kB 97.36 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against d2d41c4

@sebmarkbage sebmarkbage requested a review from gnoff April 28, 2023 15:34
@gaearon
Copy link
Collaborator

gaearon commented Apr 28, 2023

Seems like PROD tests fail?

@danieltott
Copy link
Contributor Author

@gaearon I see that! Looking into it now...

Test in —prod actually executes the boostrapScriptContent code
@danieltott
Copy link
Contributor Author

@gaearon all fixed 👍

@gnoff
Copy link
Collaborator

gnoff commented Apr 28, 2023

@danieltott what’s the rationale for automatically applying the nonce to all scripts? Also there are hydration implications because attributes are validated in dev. From a. Security angle it seems good that you need to opt your scripts into being nonced then again if you have a supply chain attack taking advantage of this auto nonce then they can also do a lot of other bad things

const startScriptWithNonce =
nonce === undefined
? startScriptSrc
: stringToPrecomputedChunk(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precomputed chunks are generally constructed in module scope. The idiomatic way of doing this is to break these chunks up in a static-dynamic-static sandwich

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnoff I updated the code to sandwich style 🥪

I had originally based my code on the pre-existing nonce setup for inline scripts:

const inlineScriptWithNonce =
nonce === undefined
? startInlineScript
: stringToPrecomputedChunk(
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);

If the updated code looks good, should I apply it there as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different. We use this repeatedly when we emit our fizz runtime scripts. The idea with precomputed chunks is you can do the text encoding once (if configured for the host runtime) so we only gain a benefit if the value is reused. pulling the chunk creation to module scope is our way of reusing the chunk which is why I suggested the sandwhich style for your original use case. Here we are getting reuse by referencing this precomputed chunk on each instruction flush but we can't make it in module scope because we embed the nonce within it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - if I understand correctly, this is because you return startInlineScript from createResponseState and use it other places. Since we don't have to do that with src/module scripts, it's actually more efficient to construct them the way you suggested, since we can precompile the nonce= chunk once inside the module. 👍

That was a tough one to wrap my head around but I'm pretty sure I have it - thank you for your explanations 🙌

@danieltott
Copy link
Contributor Author

what’s the rationale for automatically applying the nonce to all scripts?

@gnoff Honestly I might have gotten a little overzealous here. I was concerned with server-rendered script tags since that is the important thing in strict-dynamic scenarios, but I think you're right that users are free to add nonce=... to their own scripts in the body, and react just needs to make sure automatically-created scripts are handled properly. I'll remove those additions here shortly.

@danieltott
Copy link
Contributor Author

@gnoff Updated 👍

Copy link
Collaborator

@gnoff gnoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits but looks good

Comment on lines 287 to 299
if (nonce === undefined) {
bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src)),
);
} else {
bootstrapChunks.push(
startModuleSrcWithNonce,
stringToChunk(escapeTextForBrowser(nonce)),
middleModuleSrcWithNonce,
stringToChunk(escapeTextForBrowser(src)),
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe just do this like the integrity attribute

Suggested change
if (nonce === undefined) {
bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src)),
);
} else {
bootstrapChunks.push(
startModuleSrcWithNonce,
stringToChunk(escapeTextForBrowser(nonce)),
middleModuleSrcWithNonce,
stringToChunk(escapeTextForBrowser(src)),
);
}
bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src))
);
if (nonce) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonce)),
);
}

Comment on lines 258 to 270
if (nonce === undefined) {
bootstrapChunks.push(
startScriptSrc,
stringToChunk(escapeTextForBrowser(src)),
);
} else {
bootstrapChunks.push(
startScriptSrcWithNonce,
stringToChunk(escapeTextForBrowser(nonce)),
middleScriptSrcWithNonce,
stringToChunk(escapeTextForBrowser(src)),
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same recc as for modules

@gnoff
Copy link
Collaborator

gnoff commented Apr 28, 2023

@danieltott I'll merge once you make those last changes. Or I can make them for you if preferred

@danieltott
Copy link
Contributor Author

@gnoff makes sense! will update here in a minute

@danieltott
Copy link
Contributor Author

@gnoff this is all set

@gnoff gnoff merged commit 9545e48 into facebook:main May 1, 2023
36 checks passed
@danieltott danieltott deleted the nonce-scripts branch May 1, 2023 16:19
@gnoff
Copy link
Collaborator

gnoff commented May 1, 2023

@danieltott congrats on landing! Thanks for making the PR

github-actions bot pushed a commit that referenced this pull request May 1, 2023
Adds support for nonce on `bootstrapScripts`, `bootstrapModules` and the external fizz runtime

DiffTrain build for [9545e48](9545e48)
@danieltott
Copy link
Contributor Author

@gnoff thank you!! And thank you for your time helping me work through it a bit 🙌

Any idea on release scheduling? Not sure how that sort of thing works with the React codebase/package

@gnoff
Copy link
Collaborator

gnoff commented May 1, 2023

Any idea on release scheduling? Not sure how that sort of thing works with the React codebase/package

It's been a while since we've done a release and we're due for one soon. You'll be able to test our the changes in the next and experimental channels that get published every night and unless this gets reverted it will be included in the next stable however I cannot say when that will be

@danieltott
Copy link
Contributor Author

@gnoff awesome - thanks again!

gnoff added a commit that referenced this pull request May 1, 2023
in #26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.
github-actions bot pushed a commit that referenced this pull request May 1, 2023
in #26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.

DiffTrain build for [8ea96ef](8ea96ef)
acdlite added a commit to acdlite/next.js that referenced this pull request May 3, 2023
Includes the following upstream changes:

- [5dd90c562](https://github.com/facebook/react/commits/5dd90c562) Use content hash for react-native builds ([vercel#26734](facebook/react#26734)) (Samuel Susla)
- [559e83aeb](https://github.com/facebook/react/commits/559e83aeb) [Fizz] Allow an action provide a custom set of props to use for progressive enhancement ([vercel#26749](facebook/react#26749)) (Sebastian Markbåge)
- [67f4fb021](https://github.com/facebook/react/commits/67f4fb021) Allow forms to skip hydration of hidden inputs ([vercel#26735](facebook/react#26735)) (Sebastian Markbåge)
- [8ea96ef84](https://github.com/facebook/react/commits/8ea96ef84) [Fizz] Encode external fizz runtime into chunks eagerly ([vercel#26752](facebook/react#26752)) (Josh Story)
- [491aec5d6](https://github.com/facebook/react/commits/491aec5d6) Implement experimental_useOptimisticState ([vercel#26740](facebook/react#26740)) (Andrew Clark)
- [9545e4810](https://github.com/facebook/react/commits/9545e4810) Add nonce support to bootstrap scripts and external runtime ([vercel#26738](facebook/react#26738)) (Dan Ott)
- [86b0e9199](https://github.com/facebook/react/commits/86b0e9199) Gate DevTools test to fix CI ([#26742](facebook/react#26742)) (Andrew Clark)
- [b12bea62d](https://github.com/facebook/react/commits/b12bea62d) Preinits should support a nonce option ([#26744](facebook/react#26744)) (Josh Story)
- [efbd68511](https://github.com/facebook/react/commits/efbd68511) Remove unused `initialStatus` parameter from `useHostTransitionStatus` ([vercel#26743](facebook/react#26743)) (Sebastian Silbermann)
- [18282f881](https://github.com/facebook/react/commits/18282f881) Fix: Update while suspended fails to interrupt ([vercel#26739](facebook/react#26739)) (Andrew Clark)
- [540bab085](https://github.com/facebook/react/commits/540bab085) Implement experimental_useFormStatus ([#26722](facebook/react#26722)) (Andrew Clark)
acdlite added a commit to acdlite/next.js that referenced this pull request May 3, 2023
Includes the following upstream changes:

- [5dd90c562](https://github.com/facebook/react/commits/5dd90c562) Use content hash for react-native builds ([vercel#26734](facebook/react#26734)) (Samuel Susla)
- [559e83aeb](https://github.com/facebook/react/commits/559e83aeb) [Fizz] Allow an action provide a custom set of props to use for progressive enhancement ([vercel#26749](facebook/react#26749)) (Sebastian Markbåge)
- [67f4fb021](https://github.com/facebook/react/commits/67f4fb021) Allow forms to skip hydration of hidden inputs ([vercel#26735](facebook/react#26735)) (Sebastian Markbåge)
- [8ea96ef84](https://github.com/facebook/react/commits/8ea96ef84) [Fizz] Encode external fizz runtime into chunks eagerly ([vercel#26752](facebook/react#26752)) (Josh Story)
- [491aec5d6](https://github.com/facebook/react/commits/491aec5d6) Implement experimental_useOptimisticState ([vercel#26740](facebook/react#26740)) (Andrew Clark)
- [9545e4810](https://github.com/facebook/react/commits/9545e4810) Add nonce support to bootstrap scripts and external runtime ([vercel#26738](facebook/react#26738)) (Dan Ott)
- [86b0e9199](https://github.com/facebook/react/commits/86b0e9199) Gate DevTools test to fix CI ([#26742](facebook/react#26742)) (Andrew Clark)
- [b12bea62d](https://github.com/facebook/react/commits/b12bea62d) Preinits should support a nonce option ([#26744](facebook/react#26744)) (Josh Story)
- [efbd68511](https://github.com/facebook/react/commits/efbd68511) Remove unused `initialStatus` parameter from `useHostTransitionStatus` ([vercel#26743](facebook/react#26743)) (Sebastian Silbermann)
- [18282f881](https://github.com/facebook/react/commits/18282f881) Fix: Update while suspended fails to interrupt ([vercel#26739](facebook/react#26739)) (Andrew Clark)
- [540bab085](https://github.com/facebook/react/commits/540bab085) Implement experimental_useFormStatus ([#26722](facebook/react#26722)) (Andrew Clark)
ijjk pushed a commit to vercel/next.js that referenced this pull request May 3, 2023
Includes the following upstream changes:

- [b7972822b](https://github.com/facebook/react/commits/b7972822b)
useOptimisticState -> useOptimistic
([#26772](facebook/react#26772)) (Andrew Clark)
- [388686f29](https://github.com/facebook/react/commits/388686f29) Add
"canary" to list of allowed npm dist tags
([#26767](facebook/react#26767)) (Andrew Clark)
- [8a25302c6](https://github.com/facebook/react/commits/8a25302c6)
fix[dynamic-scripts-injection]: unregister content scripts before
registration ([#26765](facebook/react#26765))
(Ruslan Lesiutin)
- [2c2476834](https://github.com/facebook/react/commits/2c2476834)
Rename "next" prerelease channel to "canary"
([#26761](facebook/react#26761)) (Andrew Clark)
- [fa4314841](https://github.com/facebook/react/commits/fa4314841)
Remove deprecated workflow key from Circle config
([#26762](facebook/react#26762)) (Andrew Clark)
- [5dd90c562](https://github.com/facebook/react/commits/5dd90c562) Use
content hash for react-native builds
([#26734](facebook/react#26734)) (Samuel Susla)
- [559e83aeb](https://github.com/facebook/react/commits/559e83aeb)
[Fizz] Allow an action provide a custom set of props to use for
progressive enhancement
([#26749](facebook/react#26749)) (Sebastian
Markbåge)
- [67f4fb021](https://github.com/facebook/react/commits/67f4fb021) Allow
forms to skip hydration of hidden inputs
([#26735](facebook/react#26735)) (Sebastian
Markbåge)
- [8ea96ef84](https://github.com/facebook/react/commits/8ea96ef84)
[Fizz] Encode external fizz runtime into chunks eagerly
([#26752](facebook/react#26752)) (Josh Story)
- [491aec5d6](https://github.com/facebook/react/commits/491aec5d6)
Implement experimental_useOptimisticState
([#26740](facebook/react#26740)) (Andrew Clark)
- [9545e4810](https://github.com/facebook/react/commits/9545e4810) Add
nonce support to bootstrap scripts and external runtime
([#26738](facebook/react#26738)) (Dan Ott)
- [86b0e9199](https://github.com/facebook/react/commits/86b0e9199) Gate
DevTools test to fix CI
([#26742](facebook/react#26742)) (Andrew Clark)
- [b12bea62d](https://github.com/facebook/react/commits/b12bea62d)
Preinits should support a nonce option
([#26744](facebook/react#26744)) (Josh Story)
- [efbd68511](https://github.com/facebook/react/commits/efbd68511)
Remove unused `initialStatus` parameter from `useHostTransitionStatus`
([#26743](facebook/react#26743)) (Sebastian
Silbermann)
- [18282f881](https://github.com/facebook/react/commits/18282f881) Fix:
Update while suspended fails to interrupt
([#26739](facebook/react#26739)) (Andrew Clark)
- [540bab085](https://github.com/facebook/react/commits/540bab085)
Implement experimental_useFormStatus
([#26722](facebook/react#26722)) (Andrew Clark)

---------
facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request May 4, 2023
Summary:
in facebook/react#26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.

DiffTrain build for commit facebook/react@8ea96ef.

Changelog: [Internal]

<< DO NOT EDIT BELOW THIS LINE >>

Reviewed By: sammy-SC

Differential Revision: D45449587

Pulled By: tyao1

fbshipit-source-id: 76a5f34e42db5e9ad5a78b4b8f0d4e3dad2e7fcd
@gaearon
Copy link
Collaborator

gaearon commented May 5, 2023

jeongshin pushed a commit to jeongshin/react-native that referenced this pull request May 7, 2023
Summary:
in facebook/react#26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.

DiffTrain build for commit facebook/react@8ea96ef.

Changelog: [Internal]

<< DO NOT EDIT BELOW THIS LINE >>

Reviewed By: sammy-SC

Differential Revision: D45449587

Pulled By: tyao1

fbshipit-source-id: 76a5f34e42db5e9ad5a78b4b8f0d4e3dad2e7fcd
jerrydev0927 added a commit to jerrydev0927/react that referenced this pull request Jan 5, 2024
in facebook/react#26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.

DiffTrain build for [8ea96ef84d8f08ed1846dec9e8ed20d2225db0d3](facebook/react@8ea96ef)
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
…#26738)

Adds support for nonce on `bootstrapScripts`, `bootstrapModules` and the external fizz runtime
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
in facebook#26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
Adds support for nonce on `bootstrapScripts`, `bootstrapModules` and the external fizz runtime

DiffTrain build for commit 9545e48.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: React 18 renderToPipeableStream missing support for nonce for bootstrapScripts and bootstrapModules
5 participants