Skip to content

Commit 03b2e65

Browse files
authored
More CSP fixes (#540)
- Handle 304 cases. The reason we were getting a lot of CSP errors since #532 was that we weren't accounting for 304 Not Modified responses. In those cases, no `Content-Type` header is set, because the browser will already know it from the most recent 200 response. Our middleware incorrectly assumed it could check `type` after awaiting the downstream Koa static middleware. We now instead set the policy based on the path of the request (anything ending in `/`), since that will work for both 200 and 304 responses. There's an interesting question about whether we *should* set a CSP at all for a 304 response. There's some discussion about that [here](w3c/webappsec-csp#161) and [here](https://bugs.chromium.org/p/chromium/issues/detail?id=174301). Chrome *does* update the CSP if it's included in a 304 response, otherwise it ignores it in favor of the CSP from the last 200 response (same with all headers, except for the ones enumerated [here](https://chromium.googlesource.com/chromium/src/net/+/refs/heads/main/http/http_response_headers.cc#81)). The advantage of including it seems to be that if we update the CSP, but the content (and hence ETag) of a file has *not* changed, then the browser would otherwise continue using the stale CSP. OTOH, if you are using a nonce based script allowlist (we use hashes instead currently), then this wouldn't really work unless you did something clever to make the ETag aware of some CSP version. - Fix policy for Google Analytics. We covered the origin needed for the initial script, but then *that* script goes on to require a connection to a different origin. - Hook up a new Google Analytics test ID that I just created, so that we will catch CSP reports related to Google Analytics during dev and in PR builds going forward. Part of #517 Part of #531
1 parent 4890385 commit 03b2e65

File tree

3 files changed

+28
-10
lines changed

3 files changed

+28
-10
lines changed

cloudbuild-pr.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ steps:
2121
- --cache-ttl=168h # 1 week
2222
# Bake in this revision's corresponding playground sandbox url
2323
- --build-arg=PLAYGROUND_SANDBOX=https://pr$_PR_NUMBER-$SHORT_SHA---lit-dev-playground-5ftespv5na-uc.a.run.app/
24+
# Testing Google Analytics ID
25+
- --build-arg=GOOGLE_ANALYTICS_ID=G-PPMSZR9W18
2426

2527
# Create a new Cloud Run revision for the main site.
2628
#

packages/lit-dev-content/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"build:samples": "rm -rf samples/js && node ../lit-dev-tools-esm/lib/generate-js-samples.js",
1919
"build:samples:watch": "npm run build:samples -- --watch",
2020
"fonts:manrope": "rm -rf site/fonts temp && mkdir -p site/fonts && git clone https://github.com/sharanda/manrope.git temp/manrope && cd temp/manrope && git checkout 9ffbc349f4659065b62f780fe6e9d5a93518bd95 && cp fonts/web/*.woff2 ../../site/fonts/ && cd ../.. && rm -rf temp",
21-
"dev:build:site": "rm -rf _dev && ELEVENTY_ENV=dev PLAYGROUND_SANDBOX=http://localhost:5416/ GITHUB_CLIENT_ID=FAKE_CLIENT_ID GITHUB_AUTHORIZE_URL=http://localhost:5417/login/oauth/authorize eleventy",
21+
"dev:build:site": "rm -rf _dev && ELEVENTY_ENV=dev PLAYGROUND_SANDBOX=http://localhost:5416/ GITHUB_CLIENT_ID=FAKE_CLIENT_ID GITHUB_AUTHORIZE_URL=http://localhost:5417/login/oauth/authorize GOOGLE_ANALYTICS_ID=G-PPMSZR9W18 eleventy",
2222
"dev:build:site:watch": "npm run dev:build:site -- --watch",
2323
"dev:serve": "node ../lit-dev-tools-esm/lib/dev-server.js --open",
2424
"format": "../../node_modules/.bin/prettier \"**/*.{ts,js,json,html,css,md}\" --write"

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ export interface ContentSecurityPolicyMiddlewareOptions {
3939
const CSP_REPORT_URI = 'https://csp.withgoogle.com/csp/lit-dev';
4040

4141
/**
42-
* TODO(aomarks) Generate this automatically. See
42+
* TODO(aomarks) Generate these automatically. See
4343
* https://github.com/lit/lit.dev/issues/531.
4444
*/
45-
const GOOGLE_ANALYTICS_INLINE_SCRIPT_HASH = `'sha256-bG+QS/Ob2lFyxJ7r7PCtj/a8YofLHFx4t55RzjR1znI='`;
45+
const GOOGLE_ANALYTICS_INLINE_SCRIPT_HASH =
46+
`'sha256-bG+QS/Ob2lFyxJ7r7PCtj/a8YofLHFx4t55RzjR1znI='` + // With production GA ID.
47+
` 'sha256-RzTTI/28QrruyqG1AYHiMuUgzLJnScrkQZ+k4vM54sc='`; // With testing GA ID.
4648

4749
/**
4850
* Creates a Koa middleware that sets the lit.dev Content Security Policy (CSP)
@@ -71,7 +73,7 @@ export const contentSecurityPolicyMiddleware = (
7173
].join('; ');
7274

7375
// Policy for the main HTML entrypoints (homepage, docs, playground, etc.)
74-
const htmlCsp = makePolicy(
76+
const entrypointsCsp = makePolicy(
7577
// TODO(aomarks) We should also enable trusted types, but that will require
7678
// a policy in playground-elements for creating the worker, and a policy
7779
// es-module-lexer for doing an eval (see next comment for more on that).
@@ -80,7 +82,7 @@ export const contentSecurityPolicyMiddleware = (
8082
// TODO(aomarks) Remove unsafe-eval when https://crbug.com/1253267 is fixed.
8183
// See comment below about playgroundWorkerCsp.
8284
`'unsafe-eval'`,
83-
`https://www.googletagmanager.com/gtag/js`,
85+
`https://www.googletagmanager.com/`,
8486
GOOGLE_ANALYTICS_INLINE_SCRIPT_HASH,
8587
...(opts.inlineScriptHashes?.map((hash) => `'${hash}'`) ?? []),
8688
// In dev mode, data: scripts are required because @web/dev-server uses them
@@ -93,7 +95,12 @@ export const contentSecurityPolicyMiddleware = (
9395
//
9496
// In dev mode, ws: connections are required because @web/dev-server uses
9597
// them for automatic reloads.
96-
`connect-src 'self' https://unpkg.com/${opts.devMode ? ` ws:` : ''}`,
98+
`connect-src ${[
99+
`'self'`,
100+
'https://unpkg.com/',
101+
'https://www.google-analytics.com/',
102+
...(opts.devMode ? [` ws:`] : []),
103+
].join(' ')}`,
97104

98105
// Playground previews and embedded YouTube videos.
99106
`frame-src ${opts.playgroundPreviewOrigin} https://www.youtube-nocookie.com/`,
@@ -176,11 +183,19 @@ export const contentSecurityPolicyMiddleware = (
176183
const strictFallbackCsp = makePolicy(`default-src 'none'`);
177184

178185
return async (ctx, next) => {
179-
await next();
180-
181186
let policy: string;
182-
if (ctx.response.type === 'text/html') {
183-
policy = htmlCsp;
187+
// Note we can't rely on ctx.type being set by the downstream middleware,
188+
// because for a 304 Not Modified response, the Content-Type header will not
189+
// be set.
190+
//
191+
// Also note that we don't necessarily need to set a CSP on 304 responses at
192+
// all, because the browser will use the one from the previous 200 response
193+
// if missing (as it does for all headers). However, by including a CSP on
194+
// 304 responses, we cover the case where the CSP has changed, but the
195+
// file's content (and hence ETag) has not. Note this approach would not
196+
// work if we were using nonces instead of hashes.
197+
if (ctx.path.endsWith('/')) {
198+
policy = entrypointsCsp;
184199
} else if (ctx.path.endsWith('/playground-typescript-worker.js')) {
185200
policy = playgroundWorkerCsp;
186201
} else {
@@ -189,5 +204,6 @@ export const contentSecurityPolicyMiddleware = (
189204
// TODO(aomarks) Remove -Report-Only suffix when we are confident the
190205
// policy is working.
191206
ctx.set('Content-Security-Policy-Report-Only', policy);
207+
return next();
192208
};
193209
};

0 commit comments

Comments
 (0)