Skip to content

Commit 91123b1

Browse files
authored
Further restrict CSP and fix misfiring Google Analytics violation (#532)
- For all requests other than our HTML entrypoints and the playground worker script, serve a super strict policy, just in case a response that shouldn't normally allow any code execution somehow actually does. Based on [this](w3c/webappsec#520 (comment)) and [this](webhintio/hint#3403 (comment)) comment. - Add some directives that I found through https://csp-evaluator.withgoogle.com/ and [this comment](w3c/webappsec#520 (comment)). - Temporary fix for inline Google Analytics script which was being reported as a violation. See #531 for details. Will fix properly in followup.
1 parent fd603ad commit 91123b1

File tree

1 file changed

+62
-22
lines changed

1 file changed

+62
-22
lines changed

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

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export interface ContentSecurityPolicyMiddlewareOptions {
3838
*/
3939
const CSP_REPORT_URI = 'https://csp.withgoogle.com/csp/lit-dev';
4040

41+
/**
42+
* TODO(aomarks) Generate this automatically. See
43+
* https://github.com/lit/lit.dev/issues/531.
44+
*/
45+
const GOOGLE_ANALYTICS_INLINE_SCRIPT_HASH = `'sha256-bG+QS/Ob2lFyxJ7r7PCtj/a8YofLHFx4t55RzjR1znI='`;
46+
4147
/**
4248
* Creates a Koa middleware that sets the lit.dev Content Security Policy (CSP)
4349
* headers.
@@ -47,23 +53,40 @@ const CSP_REPORT_URI = 'https://csp.withgoogle.com/csp/lit-dev';
4753
* https://www.w3.org/TR/CSP3/
4854
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
4955
* https://speakerdeck.com/lweichselbaum/csp-a-successful-mess-between-hardening-and-mitigation
56+
* https://csp-evaluator.withgoogle.com/
5057
*/
5158
export const contentSecurityPolicyMiddleware = (
5259
opts: ContentSecurityPolicyMiddlewareOptions
5360
): Koa.Middleware => {
54-
const mainCsp = [
61+
const makePolicy = (...directives: string[]) =>
62+
[
63+
...directives,
64+
// Prevent an injected <base> tag from modifying relative URLs.
65+
`base-uri 'none'`,
66+
// Prevent form submissions.
67+
`form-action 'none'`,
68+
// Disallow other sites from iframing this site (e.g. clickjacking).
69+
`frame-ancestors 'none'`,
70+
...(opts.reportViolations ? [`report-uri ${CSP_REPORT_URI}`] : []),
71+
].join('; ');
72+
73+
// Policy for the main HTML entrypoints (homepage, docs, playground, etc.)
74+
const htmlCsp = makePolicy(
5575
// TODO(aomarks) We should also enable trusted types, but that will require
5676
// a policy in playground-elements for creating the worker, and a policy
5777
// es-module-lexer for doing an eval (see next comment for more on that).
58-
59-
// TODO(aomarks) Remove unsafe-eval when https://crbug.com/1253267 is fixed.
60-
// See comment below about playgroundWorkerCsp.
61-
//
62-
// In dev mode, data: scripts are required because @web/dev-server uses them
63-
// for automatic reloads.
64-
`script-src 'self' 'unsafe-eval' ${
65-
opts.inlineScriptHashes?.map((hash) => `'${hash}'`).join(' ') ?? ''
66-
} https://www.googletagmanager.com/gtag/js ${opts.devMode ? ` data:` : ''}`,
78+
`script-src ${[
79+
`'self'`,
80+
// TODO(aomarks) Remove unsafe-eval when https://crbug.com/1253267 is fixed.
81+
// See comment below about playgroundWorkerCsp.
82+
`'unsafe-eval'`,
83+
`https://www.googletagmanager.com/gtag/js`,
84+
GOOGLE_ANALYTICS_INLINE_SCRIPT_HASH,
85+
...(opts.inlineScriptHashes?.map((hash) => `'${hash}'`) ?? []),
86+
// In dev mode, data: scripts are required because @web/dev-server uses them
87+
// for automatic reloads.
88+
...(opts.devMode ? [`data:`] : []),
89+
].join(' ')}`,
6790

6891
// TODO(aomarks) Remove unpkg.com when https://crbug.com/1253267 is fixed.
6992
// See comment below about playgroundWorkerCsp.
@@ -94,22 +117,27 @@ export const contentSecurityPolicyMiddleware = (
94117
// The ytimg.com domain is needed for embedded YouTube videos.
95118
`img-src 'self' data: https://i.ytimg.com/`,
96119

120+
// Disallow any embeds, applets, etc. This would usually be covered by
121+
// `default-src: 'none'`, but we can't set that for the reason explained
122+
// below.
123+
`object-src 'none'`,
124+
97125
// TODO(aomarks) This could be 'none' if we didn't use <svg><use> elements,
98126
// because Firefox does not follow the img-src directive for them, so there
99127
// is no other directive to use. See
100128
// https://bugzilla.mozilla.org/show_bug.cgi?id=1303364#c4 and
101129
// https://github.com/w3c/webappsec-csp/issues/199.
102-
`default-src 'self'`,
103-
104-
...(opts.reportViolations ? [`report-uri ${CSP_REPORT_URI}`] : []),
105-
].join('; ');
130+
`default-src 'self'`
131+
);
106132

133+
// Policy for the playground-elements web worker script.
134+
//
107135
// TODO(aomarks) Currently this worker CSP will take effect in Firefox and
108136
// Safari, but not Chrome. Chrome does not currently follow the CSP spec for
109137
// workers; instead workers inherit the CSP policy of their parent context.
110138
// This is being actively fixed (https://crbug.com/1253267), and once it ships
111139
// we can remove unsafe-eval and unpkg.com from the main CSP above.
112-
const playgroundWorkerCsp = [
140+
const playgroundWorkerCsp = makePolicy(
113141
// unsafe-eval is needed because we use es-module-lexer to parse import
114142
// statements in modules. es-module-lexer needs unsafe-eval because:
115143
//
@@ -136,18 +164,30 @@ export const contentSecurityPolicyMiddleware = (
136164
`connect-src https://unpkg.com/`,
137165

138166
// Disallow everything else.
139-
`default-src 'none'`,
140-
...(opts.reportViolations ? [`report-uri ${CSP_REPORT_URI}`] : []),
141-
].join('; ');
167+
`default-src 'none'`
168+
);
169+
170+
// For all other responses, set the strictest possible CSP, just in case a
171+
// response that shouldn't normally allow any code execution actually does.
172+
//
173+
// See https://github.com/w3c/webappsec/issues/520#issuecomment-488516726 and
174+
// https://github.com/webhintio/hint/issues/3403#issue-528402128 for
175+
// discussion of why this is a good practice.
176+
const strictFallbackCsp = makePolicy(`default-src 'none'`);
142177

143178
return async (ctx, next) => {
144179
await next();
180+
181+
let policy: string;
145182
if (ctx.response.type === 'text/html') {
146-
// TODO(aomarks) Remove -Report-Only suffix when we are confident the
147-
// policy is working.
148-
ctx.set('Content-Security-Policy-Report-Only', mainCsp);
183+
policy = htmlCsp;
149184
} else if (ctx.path.endsWith('/playground-typescript-worker.js')) {
150-
ctx.set('Content-Security-Policy-Report-Only', playgroundWorkerCsp);
185+
policy = playgroundWorkerCsp;
186+
} else {
187+
policy = strictFallbackCsp;
151188
}
189+
// TODO(aomarks) Remove -Report-Only suffix when we are confident the
190+
// policy is working.
191+
ctx.set('Content-Security-Policy-Report-Only', policy);
152192
};
153193
};

0 commit comments

Comments
 (0)