diff --git a/src/steps/csp.js b/src/steps/csp.js
index eebf527b..aeac9bb2 100644
--- a/src/steps/csp.js
+++ b/src/steps/csp.js
@@ -40,17 +40,22 @@ function parseCSP(csp) {
/**
* Computes where nonces should be applied
* @param {string | null | undefined} metaCSPText The actual CSP value from the meta tag
- * @param {string | null | undefined} headersCSPText The actual CSP value from the headers
+ * @param {string | null | undefined} headerCSPText The actual CSP value from the header
+ * @param {string | null | undefined} headerCSPROText The actual CSP value from report-only header
* @returns {scriptNonce: boolean, styleNonce: boolean}
*/
-function shouldApplyNonce(metaCSPText, headersCSPText) {
+function shouldApplyNonce(metaCSPText, headerCSPText, headerCSPROText) {
const metaBased = parseCSP(metaCSPText);
- const headersBased = parseCSP(headersCSPText);
+ const headerBased = parseCSP(headerCSPText);
+ const headerROBased = parseCSP(headerCSPROText);
+
return {
scriptNonce: metaBased['script-src']?.includes(NONCE_AEM)
- || headersBased['script-src']?.includes(NONCE_AEM),
+ || headerBased['script-src']?.includes(NONCE_AEM)
+ || headerROBased['script-src']?.includes(NONCE_AEM),
styleNonce: metaBased['style-src']?.includes(NONCE_AEM)
- || headersBased['style-src']?.includes(NONCE_AEM),
+ || headerBased['style-src']?.includes(NONCE_AEM)
+ || headerROBased['style-src']?.includes(NONCE_AEM),
};
}
@@ -73,23 +78,36 @@ export function getHeaderCSP(res) {
return res.headers?.get('content-security-policy');
}
+export function getHeaderCSPRO(res) {
+ return res.headers?.get('content-security-policy-report-only');
+}
+
/**
* Apply CSP with nonces on an AST
* @param {PipelineResponse} res
* @param {Object} tree
* @param {Object} metaCSP
- * @param {string} headersCSP
+ * @param {string} headerCSP
+ * @param {string} headerCSPRO
*/
-function createAndApplyNonceOnAST(res, tree, metaCSP, headersCSP) {
+function createAndApplyNonceOnAST(res, tree, metaCSP, headerCSP, headerCSPRO) {
const nonce = createNonce();
- const { scriptNonce, styleNonce } = shouldApplyNonce(metaCSP?.properties.content, headersCSP);
+ const { scriptNonce, styleNonce } = shouldApplyNonce(
+ metaCSP?.properties.content,
+ headerCSP,
+ headerCSPRO,
+ );
if (metaCSP) {
metaCSP.properties.content = metaCSP.properties.content.replaceAll(NONCE_AEM, `'nonce-${nonce}'`);
}
- if (headersCSP) {
- res.headers.set('content-security-policy', headersCSP.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
+ if (headerCSP) {
+ res.headers.set('content-security-policy', headerCSP.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
+ }
+
+ if (headerCSPRO) {
+ res.headers.set('content-security-policy-report-only', headerCSPRO.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
}
visit(tree, (node) => {
@@ -130,15 +148,18 @@ export function getMetaCSP(tree) {
export function contentSecurityPolicyOnAST(res, tree) {
const metaCSP = getMetaCSP(tree);
const headersCSP = getHeaderCSP(res);
-
- if (!metaCSP && !headersCSP) {
+ const headersCSPRO = getHeaderCSPRO(res);
+ if (!metaCSP && !headersCSP && !headersCSPRO) {
// No CSP defined
return;
}
// CSP with nonce
- if (metaCSP?.properties.content.includes(NONCE_AEM) || headersCSP?.includes(NONCE_AEM)) {
- createAndApplyNonceOnAST(res, tree, metaCSP, headersCSP);
+ if (metaCSP?.properties.content.includes(NONCE_AEM)
+ || headersCSP?.includes(NONCE_AEM)
+ || headersCSPRO?.includes(NONCE_AEM)
+ ) {
+ createAndApplyNonceOnAST(res, tree, metaCSP, headersCSP, headersCSPRO);
}
if (metaCSP?.properties['move-as-header'] === 'true') {
@@ -159,15 +180,17 @@ export function contentSecurityPolicyOnCode(state, res) {
}
const cspHeader = getHeaderCSP(res);
+ const cspROHeader = getHeaderCSPRO(res);
if (!(
cspHeader?.includes(NONCE_AEM)
+ || cspROHeader?.includes(NONCE_AEM)
|| (checkResponseBodyForMetaBasedCSP(res) && checkResponseBodyForAEMNonce(res))
)) {
return;
}
const nonce = createNonce();
- let { scriptNonce, styleNonce } = shouldApplyNonce(null, cspHeader);
+ let { scriptNonce, styleNonce } = shouldApplyNonce(null, cspHeader, cspROHeader);
const html = res.body;
const chunks = [];
@@ -232,4 +255,8 @@ export function contentSecurityPolicyOnCode(state, res) {
if (cspHeader) {
res.headers.set('content-security-policy', cspHeader.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
}
+
+ if (cspROHeader) {
+ res.headers.set('content-security-policy-report-only', cspROHeader.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
+ }
}
diff --git a/test/rendering.test.js b/test/rendering.test.js
index c2136020..0572c570 100644
--- a/test/rendering.test.js
+++ b/test/rendering.test.js
@@ -629,6 +629,31 @@ describe('Rendering', () => {
assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`);
});
+ it('renders csp nonce header - report-only', async () => {
+ config = {
+ ...DEFAULT_CONFIG,
+ headers: {
+ '/**': [
+ {
+ key: 'Content-Security-Policy-Report-Only',
+ // eslint-disable-next-line quotes
+ value: `script-src 'nonce-aem' 'strict-dynamic'; style-src 'nonce-aem'; base-uri 'self'; object-src 'none';`,
+ },
+ ],
+ },
+ head: {
+ html: '\n'
+ + '\n'
+ + '\n'
+ + '\n'
+ + '',
+ },
+ };
+ const { headers } = await testRender('nonce-headers', 'html');
+ // eslint-disable-next-line quotes
+ assert.strictEqual(headers.get('content-security-policy-report-only'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`);
+ });
+
it('renders csp nonce metadata - move as header', async () => {
config = {
...DEFAULT_CONFIG,
@@ -1054,6 +1079,25 @@ describe('Rendering', () => {
assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`);
});
+ it('renders static html from the codebus and applies csp from header - report-only with nonce', async () => {
+ config = {
+ ...DEFAULT_CONFIG,
+ headers: {
+ '/**': [
+ {
+ key: 'content-security-policy-report-only',
+ // eslint-disable-next-line quotes
+ value: `script-src 'nonce-aem' 'strict-dynamic'; style-src 'nonce-aem'; base-uri 'self'; object-src 'none';`,
+ },
+ ],
+ },
+ };
+
+ const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-header.html'));
+ // eslint-disable-next-line quotes
+ assert.strictEqual(headers.get('content-security-policy-report-only'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`);
+ });
+
it('renders static html from the codebus and applies csp from meta with nonce', async () => {
const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta.html'));
assert.ok(!headers.get('content-security-policy'));