diff --git a/src/steps/csp.js b/src/steps/csp.js index d6e48658..991a7a5a 100644 --- a/src/steps/csp.js +++ b/src/steps/csp.js @@ -56,10 +56,19 @@ function shouldApplyNonce(metaCSPText, headersCSPText) { /** * Create a nonce for CSP + * Constraints: + * - we can only use web crypto functions to be compatible with cloudflare. + * - we need at least 128bits of entropy, according to the documentation. * @returns {string} */ function createNonce() { - return cryptoImpl.randomBytes(18).toString('base64'); + // 1 UUIDv4 = 122 bits entropy + 4 hex characters/16 bits from second UUIDv4 = 138 bits entropy + const randomHex = cryptoImpl.randomUUID().replaceAll('-', '') + + cryptoImpl.randomUUID().slice(0, 4); + + // transform into byte array before encoding for compression + const byteArray = new Uint8Array(randomHex.match(/.{2}/g).map((byte) => parseInt(byte, 16))); + return btoa(String.fromCharCode(...byteArray)); } /** diff --git a/test/rendering.test.js b/test/rendering.test.js index a70ff1f5..061610f8 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -496,198 +496,139 @@ describe('Rendering', () => { }); }); - describe('Miscellaneous', () => { - it('sets the surrogate-keys correctly', async () => { - const resp = await testRender('page-block-empty-cols'); - assert.strictEqual(resp.headers.get('x-surrogate-key'), 'rDFj9gBeGHx_FI2T foo-id_metadata super-test--helix-pages--adobe_head foo-id'); - }); - - it('sets the surrogate-keys correctly for plain', async () => { - const resp = await testRenderPlain('one-section'); - assert.strictEqual(resp.headers.get('x-surrogate-key'), 'oHjg_WDu20CBS4rD foo-id_metadata super-test--helix-pages--adobe_head foo-id'); - }); - - it('sets the surrogate-keys correctly for index.plain.html', async () => { - const resp = await testRenderPlain('one-section/index', 'one-section/index'); - assert.strictEqual(resp.headers.get('x-surrogate-key'), 'Vp-I6NB8PSor1sI6 foo-id_metadata super-test--helix-pages--adobe_head foo-id'); - }); - - it('renders the fedpub header correctly', async () => { - await testRenderPlain('fedpub-header'); - }); - - it('renders styling test document correctly', async () => { - await testRenderPlain('styling'); - }); + describe('csp', () => { + const originalRandomUUID = cryptoImpl.randomUUID; + const uuids = [ + '7241346e-6430-6d6d-6d72-41346e64306d', + '6d6d0000-0000-0000-0000-000000000000', + ]; + let uuidIdx = 0; - it('renders document with gridtables correctly', async () => { - await testRender('page-with-gridtables'); + beforeEach(() => { + uuidIdx = 0; + // eslint-disable-next-line no-plusplus + cryptoImpl.randomUUID = () => uuids[uuidIdx++]; }); - it('renders document with many image references quickly', async () => { - await testRender('gt-many-refs'); + afterEach(() => { + cryptoImpl.randomUUID = originalRandomUUID; }); - it('renders header correctly if head is missing', async () => { + it('renders csp nonce meta', async () => { config = { ...DEFAULT_CONFIG, - head: null, + head: { + // eslint-disable-next-line quotes + html: `\n` + + '\n' + + '\n' + + '\n' + + '\n' + + '', + }, }; - await testRender('no-head-html', 'html'); + const { headers } = await testRender('nonce-meta', 'html'); + assert.ok(!headers.get('content-security-policy')); }); - it('renders header correctly if head has linefeed', async () => { + it('renders csp nonce headers', async () => { + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); config = { ...DEFAULT_CONFIG, + headers: { + '/**': [ + { + key: 'Content-Security-Policy', + // 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', + html: '\n' + + '\n' + + '\n' + + '\n' + + '', }, }; - await testRender('head-with-script', 'html'); - }); - - it('renders csp nonce meta', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - head: { - // eslint-disable-next-line quotes - html: `\n` - + '\n' - + '\n' - + '\n' - + '\n' - + '', - }, - }; - const { headers } = await testRender('nonce-meta', 'html'); - assert.ok(!headers.get('content-security-policy')); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); - - it('renders csp nonce headers', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - headers: { - '/**': [ - { - key: 'Content-Security-Policy', - // 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'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } + const { headers } = await testRender('nonce-headers', 'html'); + // eslint-disable-next-line quotes + 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 metadata - move as header', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - head: { - // eslint-disable-next-line quotes - html: `\n` - + '\n' - + '\n' - + '\n' - + '\n' - + '', - }, - }; - const { headers } = await testRender('nonce-meta-move-as-header', 'html'); - // eslint-disable-next-line quotes - assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + config = { + ...DEFAULT_CONFIG, + head: { + // eslint-disable-next-line quotes + html: `\n` + + '\n' + + '\n' + + '\n' + + '\n' + + '', + }, + }; + const { headers } = await testRender('nonce-meta-move-as-header', 'html'); + // eslint-disable-next-line quotes + 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 headers and metadata - move as header', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - headers: { - '/**': [ - { - key: 'content-security-policy', - value: 'frame-ancestors \'self\'', - }, - ], - }, - head: { - // eslint-disable-next-line quotes - html: `\n` - + '\n' - + '\n' - + '\n' - + '\n' - + '', - }, - }; - const { headers } = await testRender('nonce-headers-meta', 'html'); - assert.strictEqual(headers.get('content-security-policy'), 'frame-ancestors \'self\''); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + config = { + ...DEFAULT_CONFIG, + headers: { + '/**': [ + { + key: 'content-security-policy', + value: 'frame-ancestors \'self\'', + }, + ], + }, + head: { + // eslint-disable-next-line quotes + html: `\n` + + '\n' + + '\n' + + '\n' + + '\n' + + '', + }, + }; + const { headers } = await testRender('nonce-headers-meta', 'html'); + assert.strictEqual(headers.get('content-security-policy'), 'frame-ancestors \'self\''); }); it('renders csp nonce script only', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - headers: { - '/**': [ - { - key: 'content-security-policy', - // eslint-disable-next-line quotes - value: `script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';`, - }, - ], - }, - head: { - html: '\n' - + '\n' - + '\n' - + '\n' - + '', - }, - }; - const { headers } = await testRender('nonce-script-only', 'html'); - // eslint-disable-next-line quotes - assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + config = { + ...DEFAULT_CONFIG, + headers: { + '/**': [ + { + key: 'content-security-policy', + // eslint-disable-next-line quotes + value: `script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';`, + }, + ], + }, + head: { + html: '\n' + + '\n' + + '\n' + + '\n' + + '', + }, + }; + const { headers } = await testRender('nonce-script-only', 'html'); + // eslint-disable-next-line quotes + assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; base-uri 'self'; object-src 'none';`); }); it('does not alter csp nonce if already set to a different value by meta', async () => { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); config = { ...DEFAULT_CONFIG, head: { @@ -705,7 +646,6 @@ describe('Rendering', () => { }); it('does not alter csp nonce if already set to a different value by header', async () => { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); config = { ...DEFAULT_CONFIG, headers: { @@ -730,6 +670,109 @@ describe('Rendering', () => { assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-r4nD0m' 'strict-dynamic'; style-src 'nonce-r4nD0m'; base-uri 'self'; object-src 'none';`); }); + it('renders static html from the codebus and applies csp from header with nonce', async () => { + config = { + ...DEFAULT_CONFIG, + headers: { + '/**': [ + { + key: 'content-security-policy', + // 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'), `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')); + }); + + it('renders static html from the codebus and applies csp from meta with nonce moved as header', async () => { + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta-move-as-header.html')); + // eslint-disable-next-line quotes + 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 with different nonce without altering', async () => { + const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta-different.html')); + assert.ok(!headers.get('content-security-policy')); + }); + + it('renders static html from the codebus and applies csp without altering the HTML structure', async () => { + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-fragment.html')); + assert.ok(!headers.get('content-security-policy')); + }); + + it('renders 404 html from codebus and applies csp', async () => { + loader + .rewrite('404.html', 'super-test/404-csp-nonce.html') + .headers('super-test/404-test.html', 'x-amz-meta-x-source-last-modified', 'Mon, 12 Oct 2009 17:50:00 GMT'); + cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); + const { headers } = await testRenderCode('not-found', 404, '404-csp-nonce', true); + // eslint-disable-next-line quotes + assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; base-uri 'self'; object-src 'none';`); + }); + }); + + describe('Miscellaneous', () => { + it('sets the surrogate-keys correctly', async () => { + const resp = await testRender('page-block-empty-cols'); + assert.strictEqual(resp.headers.get('x-surrogate-key'), 'rDFj9gBeGHx_FI2T foo-id_metadata super-test--helix-pages--adobe_head foo-id'); + }); + + it('sets the surrogate-keys correctly for plain', async () => { + const resp = await testRenderPlain('one-section'); + assert.strictEqual(resp.headers.get('x-surrogate-key'), 'oHjg_WDu20CBS4rD foo-id_metadata super-test--helix-pages--adobe_head foo-id'); + }); + + it('sets the surrogate-keys correctly for index.plain.html', async () => { + const resp = await testRenderPlain('one-section/index', 'one-section/index'); + assert.strictEqual(resp.headers.get('x-surrogate-key'), 'Vp-I6NB8PSor1sI6 foo-id_metadata super-test--helix-pages--adobe_head foo-id'); + }); + + it('renders the fedpub header correctly', async () => { + await testRenderPlain('fedpub-header'); + }); + + it('renders styling test document correctly', async () => { + await testRenderPlain('styling'); + }); + + it('renders document with gridtables correctly', async () => { + await testRender('page-with-gridtables'); + }); + + it('renders document with many image references quickly', async () => { + await testRender('gt-many-refs'); + }); + + it('renders header correctly if head is missing', async () => { + config = { + ...DEFAULT_CONFIG, + head: null, + }; + await testRender('no-head-html', 'html'); + }); + + it('renders header correctly if head has linefeed', async () => { + config = { + ...DEFAULT_CONFIG, + head: { + html: '\n', + }, + }; + await testRender('head-with-script', 'html'); + }); + it('renders 404 if content not found', async () => { await testRender('not-found', 'html'); // preview (code coverage) @@ -1025,84 +1068,5 @@ describe('Rendering', () => { link: '; rel=modulepreload; as=script; crossorigin=use-credentials', }); }); - - it('renders static html from the codebus and applies csp from header with nonce', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - config = { - ...DEFAULT_CONFIG, - headers: { - '/**': [ - { - key: 'content-security-policy', - // 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'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); - - it('renders static html from the codebus and applies csp from meta with nonce', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta.html')); - assert.ok(!headers.get('content-security-policy')); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); - - it('renders static html from the codebus and applies csp from meta with nonce moved as header', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta-move-as-header.html')); - // eslint-disable-next-line quotes - assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; style-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); - - it('renders static html from the codebus and applies csp with different nonce without altering', async () => { - const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-meta-different.html')); - assert.ok(!headers.get('content-security-policy')); - }); - - it('renders static html from the codebus and applies csp without altering the HTML structure', async () => { - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - const { headers } = await testRenderCode(new URL('https://helix-pages.com/static-nonce-fragment.html')); - assert.ok(!headers.get('content-security-policy')); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); - - it('renders 404 html from codebus and applies csp', async () => { - loader - .rewrite('404.html', 'super-test/404-csp-nonce.html') - .headers('super-test/404-test.html', 'x-amz-meta-x-source-last-modified', 'Mon, 12 Oct 2009 17:50:00 GMT'); - const originalRandomBytes = cryptoImpl.randomBytes; - try { - cryptoImpl.randomBytes = () => Buffer.from('rA4nd0mmmrA4nd0mmm'); - const { headers } = await testRenderCode('not-found', 404, '404-csp-nonce', true); - // eslint-disable-next-line quotes - assert.strictEqual(headers.get('content-security-policy'), `script-src 'nonce-ckE0bmQwbW1tckE0bmQwbW1t' 'strict-dynamic'; base-uri 'self'; object-src 'none';`); - } finally { - cryptoImpl.randomBytes = originalRandomBytes; - } - }); }); });