Skip to content

Commit a123834

Browse files
authored
Remove unsafe-inline from CSP by hashing inline scripts (#527)
Part of #517
1 parent 4468653 commit a123834

File tree

3 files changed

+39
-9
lines changed

3 files changed

+39
-9
lines changed

packages/lit-dev-content/.eleventy.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
const {createSearchIndex} = require('../lit-dev-tools/lib/search/plugin.js');
2020
const {preCompress} = require('../lit-dev-tools/lib/pre-compress.js');
2121
const luxon = require('luxon');
22+
const crypto = require('crypto');
2223

2324
// Use the same slugify as 11ty for markdownItAnchor. It's similar to Jekyll,
2425
// and preserves the existing URL fragments
@@ -29,6 +30,8 @@ const OUTPUT_DIR = DEV ? '_dev' : '_site';
2930
const PLAYGROUND_SANDBOX =
3031
process.env.PLAYGROUND_SANDBOX || 'http://localhost:6416/';
3132

33+
const cspInlineScriptHashes = new Set();
34+
3235
module.exports = function (eleventyConfig) {
3336
// https://github.com/JordanShurmer/eleventy-plugin-toc#readme
3437
eleventyConfig.addPlugin(pluginTOC, {
@@ -333,7 +336,12 @@ ${content}
333336
if (DEV) {
334337
return `<script type="module" src="/js/${path}"></script>`;
335338
}
336-
const script = fsSync.readFileSync(`rollupout/${path}`, 'utf8');
339+
// Note we must trim before hashing, because our html-minifier will trim
340+
// inline script trailing newlines, and otherwise our hash will be wrong.
341+
const script = fsSync.readFileSync(`rollupout/${path}`, 'utf8').trim();
342+
const hash =
343+
'sha256-' + crypto.createHash('sha256').update(script).digest('base64');
344+
cspInlineScriptHashes.add(hash);
337345
return `<script type="module">${script}</script>`;
338346
});
339347

@@ -351,6 +359,10 @@ ${content}
351359
);
352360
});
353361

362+
eleventyConfig.on('beforeBuild', () => {
363+
cspInlineScriptHashes.clear();
364+
});
365+
354366
eleventyConfig.on('afterBuild', async () => {
355367
// The eleventyNavigation plugin requires that each section heading in our
356368
// docs has its own actual markdown file. But we don't actually use these
@@ -412,6 +424,14 @@ ${content}
412424
// them directly instead of spending its own cycles. Note this adds ~4
413425
// seconds to the build, but it's disabled during dev.
414426
await preCompress({glob: `${OUTPUT_DIR}/**/*`});
427+
428+
// Note we only need to write CSP inline script hashes for the production
429+
// output, because in dev mode we don't inline scripts.
430+
await fs.writeFile(
431+
path.join(OUTPUT_DIR, 'csp-inline-script-hashes.txt'),
432+
[...cspInlineScriptHashes].join('\n'),
433+
'utf8'
434+
);
415435
}
416436
});
417437

packages/lit-dev-server/src/middleware/content-security-policy-middleware.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export interface ContentSecurityPolicyMiddlewareOptions {
2020
* If true, CSP violations will be reported to the Google CSP Collector.
2121
*/
2222
reportViolations?: boolean;
23+
24+
/**
25+
* An array of "<hash-algorithm>-<base64-value>" CSP sources that will be
26+
* allowlisted to run as inline scripts.
27+
*/
28+
inlineScriptHashes?: string[];
2329
}
2430

2531
/**
@@ -40,11 +46,6 @@ export const contentSecurityPolicyMiddleware = (
4046
// a policy in playground-elements for creating the worker, and a policy
4147
// es-module-lexer for doing an eval (see next comment for more on that).
4248

43-
// TODO(aomarks) unsafe-inline is needed because we use some inline scripts
44-
// on some pages. We should generate hashes for them during Eleventy build
45-
// and allowlist them here using `<hash-algorithm>-<base64-value>`
46-
// directives.
47-
//
4849
// TODO(aomarks) unsafe-eval is needed for an eval that is made by
4950
// es-module-lexer to perform JavaScript string unescaping
5051
// (https://github.com/guybedford/es-module-lexer/blob/91964da6b086dc5029091eeef481180a814ce24a/src/lexer.js#L32).
@@ -57,9 +58,9 @@ export const contentSecurityPolicyMiddleware = (
5758
//
5859
// In dev mode, data: scripts are required because @web/dev-server uses them
5960
// for automatic reloads.
60-
`script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com/gtag/js ${
61-
opts.devMode ? ` data:` : ''
62-
}`,
61+
`script-src 'self' 'unsafe-eval' ${
62+
opts.inlineScriptHashes?.map((hash) => `'${hash}'`).join(' ') ?? ''
63+
} https://www.googletagmanager.com/gtag/js ${opts.devMode ? ` data:` : ''}`,
6364

6465
// unpkg.com is needed to allow the Playground worker to fetch dependencies.
6566
// TODO(aomarks) After https://crbug.com/1253267 is fixed we can serve a

packages/lit-dev-server/src/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import koaConditionalGet from 'koa-conditional-get';
1010
import koaEtag from 'koa-etag';
1111
import {fileURLToPath} from 'url';
1212
import * as path from 'path';
13+
import * as fs from 'fs';
1314
import {redirectMiddleware} from './middleware/redirect-middleware.js';
1415
import {playgroundMiddleware} from './middleware/playground-middleware.js';
1516
import {contentSecurityPolicyMiddleware} from './middleware/content-security-policy-middleware.js';
@@ -42,8 +43,16 @@ const app = new Koa();
4243
if (mode === 'playground') {
4344
app.use(playgroundMiddleware());
4445
} else {
46+
const inlineScriptHashes = fs
47+
.readFileSync(
48+
path.join(contentPackage, '_site', 'csp-inline-script-hashes.txt'),
49+
'utf8'
50+
)
51+
.trim()
52+
.split('\n');
4553
app.use(
4654
contentSecurityPolicyMiddleware({
55+
inlineScriptHashes,
4756
reportViolations: process.env.REPORT_CSP_VIOLATIONS === 'true',
4857
})
4958
);

0 commit comments

Comments
 (0)