diff --git a/packages/next/client/head-manager.ts b/packages/next/client/head-manager.ts
index 9fbd8c24523e7..5606d76354fe3 100644
--- a/packages/next/client/head-manager.ts
+++ b/packages/next/client/head-manager.ts
@@ -40,6 +40,36 @@ function reactElementToDOM({ type, props }: JSX.Element): HTMLElement {
return el
}
+/**
+ * When a `nonce` is present on an element, browsers such as Chrome and Firefox strip it out of the
+ * actual HTML attributes for security reasons *when the element is added to the document*. Thus,
+ * given two equivalent elements that have nonces, `Element,isEqualNode()` will return false if one
+ * of those elements gets added to the document. Although the `element.nonce` property will be the
+ * same for both elements, the one that was added to the document will return an empty string for
+ * its nonce HTML attribute value.
+ *
+ * This custom `isEqualNode()` function therefore removes the nonce value from the `newTag` before
+ * comparing it to `oldTag`, restoring it afterwards.
+ *
+ * For more information, see:
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1211471#c12
+ */
+export function isEqualNode(oldTag: Element, newTag: Element) {
+ if (oldTag instanceof HTMLElement && newTag instanceof HTMLElement) {
+ const nonce = newTag.getAttribute('nonce')
+ // Only strip the nonce if `oldTag` has had it stripped. An element's nonce attribute will not
+ // be stripped if there is no content security policy response header that includes a nonce.
+ if (nonce && !oldTag.getAttribute('nonce')) {
+ const cloneTag = newTag.cloneNode(true) as typeof newTag
+ cloneTag.setAttribute('nonce', '')
+ cloneTag.nonce = nonce
+ return nonce === oldTag.nonce && oldTag.isEqualNode(cloneTag)
+ }
+ }
+
+ return oldTag.isEqualNode(newTag)
+}
+
function updateElements(type: string, components: JSX.Element[]): void {
const headEl = document.getElementsByTagName('head')[0]
const headCountEl: HTMLMetaElement = headEl.querySelector(
@@ -70,7 +100,7 @@ function updateElements(type: string, components: JSX.Element[]): void {
(newTag) => {
for (let k = 0, len = oldTags.length; k < len; k++) {
const oldTag = oldTags[k]
- if (oldTag.isEqualNode(newTag)) {
+ if (isEqualNode(oldTag, newTag)) {
oldTags.splice(k, 1)
return false
}
diff --git a/test/e2e/nonce-head-manager/app/next.config.js b/test/e2e/nonce-head-manager/app/next.config.js
new file mode 100644
index 0000000000000..17633cd928ded
--- /dev/null
+++ b/test/e2e/nonce-head-manager/app/next.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ async headers() {
+ return [
+ {
+ source: '/csp',
+ headers: [
+ {
+ key: 'Content-Security-Policy',
+ value: "script-src-elem 'nonce-abc123' 'unsafe-eval'",
+ },
+ ],
+ },
+ ]
+ },
+}
diff --git a/test/e2e/nonce-head-manager/app/pages/_document.js b/test/e2e/nonce-head-manager/app/pages/_document.js
new file mode 100644
index 0000000000000..4e5392345b48a
--- /dev/null
+++ b/test/e2e/nonce-head-manager/app/pages/_document.js
@@ -0,0 +1,17 @@
+import Document, { Head, Html, Main, NextScript } from 'next/document'
+
+class NextDocument extends Document {
+ render() {
+ return (
+
+