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
This commit is contained in:
Eric Eastwood 2022-10-19 12:07:39 -05:00 committed by GitHub
parent 0962075f8d
commit a0089b0fe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 79 additions and 13 deletions

View File

@ -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',
`
<script type="text/javascript">
<script type="text/javascript" nonce="${renderOptions.pageOptions.cspNonce}">
window.matrixPublicArchiveContext = ${safeJson(serializedContext)}
</script>
`

View File

@ -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.

View File

@ -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(`<title>${pageOptions.title}</title>`)}
${pageOptions.styles
.map((styleUrl) => `<link href="${styleUrl}" rel="stylesheet">`)
.map(
(styleUrl) =>
`<link href="${styleUrl}" rel="stylesheet" nonce="${pageOptions.cspNonce}">`
)
.join('\n')}
</head>
<body>
${hydrogenHtmlOutput}
${pageOptions.scripts
.map((scriptUrl) => `<script type="text/javascript" src="${scriptUrl}"></script>`)
.map(
(scriptUrl) =>
`<script type="text/javascript" src="${scriptUrl}" nonce="${pageOptions.cspNonce}"></script>`
)
.join('\n')}
<script type="text/javascript">window.tracingSpansForRequest = ${safeJson(
serializedSpans
)};</script>
<script type="text/javascript" nonce="${pageOptions.cspNonce}">
window.tracingSpansForRequest = ${safeJson(serializedSpans)};
</script>
</body>
</html>
`;

View File

@ -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 <script> and <style> tags that have the nonce provided
//
// To ensure compatibility with very old browser versions (4+ years), we add
// 'unsafe-inline' as a fallback. All recent browsers will ignore 'unsafe-inline' if
// a CSP nonce or hash is present. (via
// https://web.dev/strict-csp/#step-4-add-fallbacks-to-support-safari-and-older-browsers)
`script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline';`,
// Hydrogen uses a bunch of inline styles and `style-src-attr` isn't well supported
// in Firefox to allow it specifically. In the future, when it has better support we
// should switch to a strict nonce based style directive.
`style-src 'self' 'unsafe-inline';`,
// We only need to load fonts from ourself
`font-src 'self';`,
// We only need to be able to load images/media from ourself and the homeserver media repo
`img-src 'self' ${matrixServerUrl};`,
`media-src 'self' ${matrixServerUrl};`,
// Only allow the room directory search form to submit to ourself
`form-action 'self';`,
// We have no need ourself to embed in an iframe. And we shouldn't allow others to
// iframe embed which can lead to clickjacking. We also have the
// `prevent-clickjacking-middleware` to cover this.
`frame-ancestors 'none';`,
// Extra restriction since we have no plans to change the `<base>`
`base-uri 'self'`,
];
res.set('Content-Security-Policy', directives.join(' '));
// Make this available for down-stream routes to reference and use
res.locals.cspNonce = nonce;
next();
}
module.exports = contentSecurityPolicyMiddleware;

View File

@ -7,10 +7,12 @@ const asyncHandler = require('../lib/express-async-handler');
const { handleTracingMiddleware } = require('../tracing/tracing-middleware');
const getVersionTags = require('../lib/get-version-tags');
const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware');
const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware');
function installRoutes(app) {
app.use(handleTracingMiddleware);
app.use(preventClickjackingMiddleware);
app.use(contentSecurityPolicyMiddleware);
let healthCheckResponse;
app.get(

View File

@ -80,6 +80,7 @@ router.get(
title: `Matrix Public Archive`,
styles: [hydrogenStylesUrl, stylesUrl, roomDirectoryStylesUrl],
scripts: [jsBundleUrl],
cspNonce: res.locals.cspNonce,
}
);

View File

@ -287,6 +287,7 @@ router.get(
styles: [hydrogenStylesUrl, stylesUrl],
scripts: [jsBundleUrl],
shouldIndex,
cspNonce: res.locals.cspNonce,
}
);

View File

@ -48,6 +48,8 @@ async function timeoutMiddleware(req, res, next) {
humanReadableSpans = [noTracingDataAvailableItem];
}
const cspNonce = res.locals.cspNonce;
const hydrogenStylesUrl = urlJoin(basePath, '/hydrogen-styles.css');
const stylesUrl = urlJoin(basePath, '/css/styles.css');
@ -57,8 +59,8 @@ async function timeoutMiddleware(req, res, next) {
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server timeout - Matrix Public Archive</title>
<link href="${hydrogenStylesUrl}" rel="stylesheet">
<link href="${stylesUrl}" rel="stylesheet">
<link href="${hydrogenStylesUrl}" rel="stylesheet" nonce="${cspNonce}">
<link href="${stylesUrl}" rel="stylesheet" nonce="${cspNonce}">
</head>
${/* We add the .hydrogen class here just to get normal body styles */ ''}
<body class="hydrogen">
@ -75,9 +77,9 @@ async function timeoutMiddleware(req, res, next) {
}</span></h2>`
)}
<script type="text/javascript">window.tracingSpansForRequest = ${safeJson(
serializedSpans
)};</script>
<script type="text/javascript" nonce="${cspNonce}">
window.tracingSpansForRequest = ${safeJson(serializedSpans)};
</script>
</body>
</html>
`;