From 06eac7c8f5e3ac3af12ca676125405d21feb035d Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Tue, 18 Aug 2026 15:35:12 +0200 Subject: [PATCH 1/3] test: a csp meta bound for a header does not survive composition --- test/render/compose.test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/render/compose.test.js b/test/render/compose.test.js index 0bcd710f..86ea5d72 100644 --- a/test/render/compose.test.js +++ b/test/render/compose.test.js @@ -66,4 +66,27 @@ describe('render compose', () => { assert.ok(!html.includes('urn:adobe:aue')); assert.ok(!html.includes('universal-editor-service')); }); + it('drops a CSP meta the pipeline would have moved to a header', async () => { + const cspHead = '' + + ''; + + const tree = await composeHtml(daCtx, aemCtx, '

content

', cspHead); + const html = serializeHtml(tree); + + assert.ok(!html.includes('Content-Security-Policy')); + assert.ok(!html.includes('move-to-http-header')); + // the rest of head.html is untouched + assert.ok(html.includes('src="/scripts/aem.js"')); + }); + + it('keeps a CSP meta the pipeline would have left in place', async () => { + const cspHead = ''; + + const tree = await composeHtml(daCtx, aemCtx, '

content

', cspHead); + const html = serializeHtml(tree); + + assert.ok(html.includes('Content-Security-Policy')); + }); }); From c6840ad3f6cc9fc90fd49cc5a58a3b2cae492e69 Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Tue, 18 Aug 2026 15:37:00 +0200 Subject: [PATCH 2/3] fix: drop a csp meta the pipeline would have moved to a header --- src/render/compose.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/render/compose.js b/src/render/compose.js index 5e738cd4..8a1a30ee 100644 --- a/src/render/compose.js +++ b/src/render/compose.js @@ -33,6 +33,15 @@ function injectAEMHtmlHeadEntries(daCtx, headNode, headHtmlStr) { const { org, site, orgSiteInPath } = daCtx; const aemHeadHtmlTree = fromHtml(headHtmlStr, { fragment: true }); + // the config service hands over head.html as the code bus holds it, so a meta the pipeline + // would have turned into a response header is still a meta here. Emitting it applies a CSP + // the page was never meant to carry, and the UE scripts injected later have no nonce. + aemHeadHtmlTree.children = aemHeadHtmlTree.children.filter( + (node) => !(node.type === 'element' + && node.tagName === 'meta' + && node.properties?.['move-to-http-header'] !== undefined), + ); + // TODO: reuse fixUrlsWhenLocalDev from aemCtx.js instead of duplicating. if (orgSiteInPath) { const headScriptsAndLinks = selectAll( From 6b20e809cd3cccf104b1ac1ff8dc45b33b812c4d Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Tue, 18 Aug 2026 19:49:08 +0200 Subject: [PATCH 3/3] fix: also strip the placeholder nonce, tighten the meta filter drops the nonce="aem" the pipeline would have rewritten, and narrows the meta drop to metas whose csp references that same placeholder. covers tobi's #273 review. --- src/render/compose.js | 12 ++++++++---- test/render/compose.test.js | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/render/compose.js b/src/render/compose.js index 8a1a30ee..ec4ef459 100644 --- a/src/render/compose.js +++ b/src/render/compose.js @@ -33,14 +33,18 @@ function injectAEMHtmlHeadEntries(daCtx, headNode, headHtmlStr) { const { org, site, orgSiteInPath } = daCtx; const aemHeadHtmlTree = fromHtml(headHtmlStr, { fragment: true }); - // the config service hands over head.html as the code bus holds it, so a meta the pipeline - // would have turned into a response header is still a meta here. Emitting it applies a CSP - // the page was never meant to carry, and the UE scripts injected later have no nonce. + // the pipeline moves this meta to a response header and rewrites the placeholder + // nonce; drop both. aemHeadHtmlTree.children = aemHeadHtmlTree.children.filter( (node) => !(node.type === 'element' && node.tagName === 'meta' - && node.properties?.['move-to-http-header'] !== undefined), + && node.properties?.['move-to-http-header'] !== undefined + && typeof node.properties?.content === 'string' + && node.properties.content.includes("'nonce-aem'")), ); + selectAll('[nonce=aem]', aemHeadHtmlTree).forEach((n) => { + delete n.properties.nonce; + }); // TODO: reuse fixUrlsWhenLocalDev from aemCtx.js instead of duplicating. if (orgSiteInPath) { diff --git a/test/render/compose.test.js b/test/render/compose.test.js index 86ea5d72..41454a2d 100644 --- a/test/render/compose.test.js +++ b/test/render/compose.test.js @@ -77,6 +77,7 @@ describe('render compose', () => { assert.ok(!html.includes('Content-Security-Policy')); assert.ok(!html.includes('move-to-http-header')); + assert.ok(!html.includes('nonce="aem"')); // the rest of head.html is untouched assert.ok(html.includes('src="/scripts/aem.js"')); });