From fcc73ca80b48f2383670de9f738880764a6cffd7 Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Wed, 12 Feb 2025 13:52:09 -0800 Subject: [PATCH] fix: Use Web Crypto compatible function for nonce generation --- src/steps/csp.js | 4 +- test/rendering.test.js | 336 ++++++++++++++++++----------------------- 2 files changed, 153 insertions(+), 187 deletions(-) diff --git a/src/steps/csp.js b/src/steps/csp.js index d6e48658..eebf527b 100644 --- a/src/steps/csp.js +++ b/src/steps/csp.js @@ -59,7 +59,9 @@ function shouldApplyNonce(metaCSPText, headersCSPText) { * @returns {string} */ function createNonce() { - return cryptoImpl.randomBytes(18).toString('base64'); + const array = new Uint8Array(18); + cryptoImpl.getRandomValues(array); + return btoa(String.fromCharCode(...array)); } /** diff --git a/test/rendering.test.js b/test/rendering.test.js index a70ff1f5..d3c925b4 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -11,15 +11,41 @@ */ /* eslint-env mocha */ import assert from 'assert'; +import esmock from 'esmock'; import path from 'path'; import { readFile } from 'fs/promises'; import { JSDOM } from 'jsdom'; import { assertHTMLEquals } from './utils.js'; - -import { htmlPipe, PipelineRequest, PipelineState } from '../src/index.js'; import { FileS3Loader } from './FileS3Loader.js'; -// eslint-disable-next-line import/no-unresolved -import cryptoImpl from '#crypto'; + +const mockCrypto = { + getRandomValues: (array) => { + const mockRandomValues = new TextEncoder().encode('rA4nd0mmmrA4nd0mmm'); + for (let i = 0; i < array.length; i += 1) { + array[i] = mockRandomValues[i]; + } + }, +}; + +const { htmlPipe, PipelineRequest, PipelineState } = await esmock('../src/index.js', { + '../src/html-pipe.js': await esmock('../src/html-pipe.js', { + '../src/steps/render.js': await esmock('../src/steps/render.js', { + '../src/steps/csp.js': await esmock('../src/steps/csp.js', { + '#crypto': mockCrypto, + }), + }), + '../src/steps/render-code.js': await esmock('../src/steps/render-code.js', { + '../src/steps/csp.js': await esmock('../src/steps/csp.js', { + '#crypto': mockCrypto, + }), + }), + '../src/steps/fetch-404.js': await esmock('../src/steps/fetch-404.js', { + '../src/steps/csp.js': await esmock('../src/steps/csp.js', { + '#crypto': mockCrypto, + }), + }), + }), +}); const METADATA = { data: { @@ -547,147 +573,116 @@ describe('Rendering', () => { }); 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; - } + 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')); }); 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; - } + 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';`); }); 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; - } + 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; - } + 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; - } + 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 +700,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: { @@ -1027,51 +1021,33 @@ describe('Rendering', () => { }); 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';`, - }, - ], - }, - }; + 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; - } + 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 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; - } + 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 () => { - 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; - } + 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 () => { @@ -1080,29 +1056,17 @@ describe('Rendering', () => { }); 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; - } + 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'); - 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; - } + 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';`); }); }); });