[Fiber] Fix false-positive hydration mismatch on `nonce` attributes (#37030)
Co-authored-by: Suneil Nyamathi <snyamathi@gmail.com>
MaxwellCohen committed
Jul 16, 2026 at 11:54 UTC
58a6360f8545e79a0e537bdddd34206ab1042ec3
2 files changed
+79
-2
packages/react-dom-bindings/src/client/DOMPropertyOperations.js
+10
-2
@@ -42,7 +42,11 @@ export function getValueForAttribute(
42
}
43
return expected === undefined ? undefined : null;
44
}
45
- const value = node.getAttribute(name);
45
+ // When CSP is enabled, browsers hide the nonce attribute
46
+ // so we need to access the nonce property directly
47
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cryptographicnonce
48
+ const isNonce = name.toLowerCase() === 'nonce';
49
+ const value = isNonce ? (node as any).nonce : node.getAttribute(name);
50
if (__DEV__) {
51
checkAttributeStringCoercion(expected, name);
52
}
@@ -79,7 +83,11 @@ export function getValueForAttributeOnCustomComponent(
83
}
84
return expected === undefined ? undefined : null;
85
}
82
- const value = node.getAttribute(name);
86
+ // When CSP is enabled, browsers hide the nonce attribute
87
+ // so we need to access the nonce property directly
88
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cryptographicnonce
89
+ const isNonce = name.toLowerCase() === 'nonce';
90
+ const value = isNonce ? (node as any).nonce : node.getAttribute(name);
91
92
if (value === '' && expected === true) {
93
return true;
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+69
@@ -47,6 +47,7 @@ describe('ReactDOMServerHydration', () => {
47
});
48
49
afterEach(() => {
50
+ jest.restoreAllMocks();
51
window.removeEventListener('error', errorHandler);
52
document.body.removeChild(container);
53
console.error = realConsoleError;
@@ -525,6 +526,74 @@ describe('ReactDOMServerHydration', () => {
526
]
527
`);
528
});
529
+
530
+ describe('nonce', () => {
531
+ // Nonce is on HTMLOrSVGElement, so cover a few host tags that hydrate
532
+ // attributes through getValueForAttribute.
533
+ function App() {
534
+ return (
535
+ <div>
536
+ <script nonce="r4nd0m" src="https://example.com/script.js" />
537
+ <style nonce="r4nd0m">{`body { background-color: red; }`}</style>
538
+ <link
539
+ rel="stylesheet"
540
+ nonce="r4nd0m"
541
+ href="https://example.com/style.css"
542
+ />
543
+ <img nonce="r4nd0m" src="https://example.com/image.png" />
544
+ <video nonce="r4nd0m" src="https://example.com/video.mp4" />
545
+ <audio nonce="r4nd0m" src="https://example.com/audio.mp3" />
546
+ <iframe nonce="r4nd0m" src="https://example.com/iframe.html" />
547
+ <form nonce="r4nd0m">
548
+ <input type="text" nonce="r4nd0m" />
549
+ </form>
550
+ <my-element nonce="r4nd0m" />
551
+ </div>
552
+ );
553
+ }
554
+
555
+ // @gate __DEV__
556
+ it('does not warn when nonce matches and CSP hides getAttribute', () => {
557
+ // When CSP is enabled, browsers hide the nonce content attribute
558
+ // (getAttribute("nonce") returns "") while .nonce remains readable.
559
+ // JSDOM does not implement this, so mock it for this case.
560
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cryptographicnonce
561
+ const originalGetAttribute = window.Element.prototype.getAttribute;
562
+ spyOnDevAndProd(
563
+ window.Element.prototype,
564
+ 'getAttribute',
565
+ ).mockImplementation(function (name) {
566
+ if (typeof name === 'string' && name.toLowerCase() === 'nonce') {
567
+ return '';
568
+ }
569
+ return originalGetAttribute.call(this, name);
570
+ });
571
+
572
+ const htmlString = ReactDOMServer.renderToString(<App />);
573
+ container.innerHTML = htmlString;
574
+
575
+ // validate that the nonce attribute is hidden by getAttribute
576
+ // mimicking the behavior of browsers when CSP is enabled
577
+ const script = container.querySelector('script');
578
+ expect(script.getAttribute('nonce')).toBe('');
579
+ expect(script.nonce).toBe('r4nd0m');
580
+
581
+ expect(testMismatch(App)).toEqual([]);
582
+ });
583
+
584
+ // @gate __DEV__
585
+ it('does not warn when nonce matches without CSP hiding', () => {
586
+ const htmlString = ReactDOMServer.renderToString(<App />);
587
+ container.innerHTML = htmlString;
588
+
589
+ // validate that the nonce attribute is visible via getAttribute
590
+ // when CSP is disabled
591
+ const script = container.querySelector('script');
592
+ expect(script.getAttribute('nonce')).toBe('r4nd0m');
593
+ expect(script.nonce).toBe('r4nd0m');
594
+ expect(testMismatch(App)).toEqual([]);
595
+ });
596
+ });
597
});
598
599
describe('extra nodes on the client', () => {