@samitouri / QOS-React / commits / 58a6360f85

[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', () => {