From a0089b0fe4ce343bc11e2fc10322936e0ba90ac7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 19 Oct 2022 12:07:39 -0500 Subject: [PATCH] Add `Content-Security-Policy` (CSP) (#81) Add `Content-Security-Policy` (CSP) that restricts the page to just what it is expected to do. This helps limit the damage that can be done by any XSS attack. Fix https://github.com/matrix-org/internal-config/issues/1341 --- .../render-hydrogen-to-string-unsafe.js | 4 +- .../render-hydrogen-to-string.js | 3 +- ...-hydrogen-vm-render-script-to-page-html.js | 18 +++++-- .../content-security-policy-middleware.js | 51 +++++++++++++++++++ server/routes/install-routes.js | 2 + server/routes/room-directory-routes.js | 1 + server/routes/room-routes.js | 1 + server/routes/timeout-middleware.js | 12 +++-- 8 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 server/routes/content-security-policy-middleware.js diff --git a/server/hydrogen-render/render-hydrogen-to-string-unsafe.js b/server/hydrogen-render/render-hydrogen-to-string-unsafe.js index 9b59c3f..6895d4e 100644 --- a/server/hydrogen-render/render-hydrogen-to-string-unsafe.js +++ b/server/hydrogen-render/render-hydrogen-to-string-unsafe.js @@ -66,6 +66,8 @@ async function _renderHydrogenToStringUnsafe(renderOptions) { assert(renderOptions); assert(renderOptions.vmRenderScriptFilePath); assert(renderOptions.vmRenderContext); + assert(renderOptions.pageOptions); + assert(renderOptions.pageOptions.cspNonce); const { dom, vmContext } = createDomAndSetupVmContext(); @@ -78,7 +80,7 @@ async function _renderHydrogenToStringUnsafe(renderOptions) { dom.document.body.insertAdjacentHTML( 'beforeend', ` - ` diff --git a/server/hydrogen-render/render-hydrogen-to-string.js b/server/hydrogen-render/render-hydrogen-to-string.js index bd9f562..cddffcc 100644 --- a/server/hydrogen-render/render-hydrogen-to-string.js +++ b/server/hydrogen-render/render-hydrogen-to-string.js @@ -15,8 +15,7 @@ const runInChildProcess = require('../child-process-runner/run-in-child-process' const RENDER_TIMEOUT = 5000; async function renderHydrogenToString(renderOptions) { - assert(renderOptions.vmRenderScriptFilePath); - assert(renderOptions.vmRenderContext); + assert(renderOptions); // We expect `config` but we should sanity check that we aren't leaking the access token // to the client if someone naievely copied the whole `config` object to here. diff --git a/server/hydrogen-render/render-hydrogen-vm-render-script-to-page-html.js b/server/hydrogen-render/render-hydrogen-vm-render-script-to-page-html.js index 8012307..56b8de2 100644 --- a/server/hydrogen-render/render-hydrogen-vm-render-script-to-page-html.js +++ b/server/hydrogen-render/render-hydrogen-vm-render-script-to-page-html.js @@ -18,10 +18,12 @@ async function renderHydrogenVmRenderScriptToPageHtml( assert(pageOptions.title); assert(pageOptions.styles); assert(pageOptions.scripts); + assert(pageOptions.cspNonce); const hydrogenHtmlOutput = await renderHydrogenToString({ vmRenderScriptFilePath, vmRenderContext, + pageOptions, }); const serializableSpans = getSerializableSpans(); @@ -41,17 +43,23 @@ async function renderHydrogenVmRenderScriptToPageHtml( ${maybeNoIndexHtml} ${sanitizeHtml(`${pageOptions.title}`)} ${pageOptions.styles - .map((styleUrl) => ``) + .map( + (styleUrl) => + `` + ) .join('\n')} ${hydrogenHtmlOutput} ${pageOptions.scripts - .map((scriptUrl) => ``) + .map( + (scriptUrl) => + `` + ) .join('\n')} - + `; diff --git a/server/routes/content-security-policy-middleware.js b/server/routes/content-security-policy-middleware.js new file mode 100644 index 0000000..216f0ec --- /dev/null +++ b/server/routes/content-security-policy-middleware.js @@ -0,0 +1,51 @@ +'use strict'; + +const crypto = require('crypto'); +const assert = require('assert'); + +const config = require('../lib/config'); +const matrixServerUrl = config.get('matrixServerUrl'); +assert(matrixServerUrl); + +function contentSecurityPolicyMiddleware(req, res, next) { + const nonce = crypto.randomBytes(16).toString('hex'); + + // Based on https://web.dev/strict-csp/ + const directives = [ + // Default to fully-restrictive and only allow what's needed below + `default-src 'none';`, + // Only ${/* We add the .hydrogen class here just to get normal body styles */ ''} @@ -75,9 +77,9 @@ async function timeoutMiddleware(req, res, next) { }` )} - + `;