@@ -38,6 +38,12 @@ export interface ContentSecurityPolicyMiddlewareOptions {
38
38
*/
39
39
const CSP_REPORT_URI = 'https://csp.withgoogle.com/csp/lit-dev' ;
40
40
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
+
41
47
/**
42
48
* Creates a Koa middleware that sets the lit.dev Content Security Policy (CSP)
43
49
* headers.
@@ -47,23 +53,40 @@ const CSP_REPORT_URI = 'https://csp.withgoogle.com/csp/lit-dev';
47
53
* https://www.w3.org/TR/CSP3/
48
54
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
49
55
* https://speakerdeck.com/lweichselbaum/csp-a-successful-mess-between-hardening-and-mitigation
56
+ * https://csp-evaluator.withgoogle.com/
50
57
*/
51
58
export const contentSecurityPolicyMiddleware = (
52
59
opts : ContentSecurityPolicyMiddlewareOptions
53
60
) : 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 (
55
75
// TODO(aomarks) We should also enable trusted types, but that will require
56
76
// a policy in playground-elements for creating the worker, and a policy
57
77
// 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 ( ' ' ) } `,
67
90
68
91
// TODO(aomarks) Remove unpkg.com when https://crbug.com/1253267 is fixed.
69
92
// See comment below about playgroundWorkerCsp.
@@ -94,22 +117,27 @@ export const contentSecurityPolicyMiddleware = (
94
117
// The ytimg.com domain is needed for embedded YouTube videos.
95
118
`img-src 'self' data: https://i.ytimg.com/` ,
96
119
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
+
97
125
// TODO(aomarks) This could be 'none' if we didn't use <svg><use> elements,
98
126
// because Firefox does not follow the img-src directive for them, so there
99
127
// is no other directive to use. See
100
128
// https://bugzilla.mozilla.org/show_bug.cgi?id=1303364#c4 and
101
129
// 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
+ ) ;
106
132
133
+ // Policy for the playground-elements web worker script.
134
+ //
107
135
// TODO(aomarks) Currently this worker CSP will take effect in Firefox and
108
136
// Safari, but not Chrome. Chrome does not currently follow the CSP spec for
109
137
// workers; instead workers inherit the CSP policy of their parent context.
110
138
// This is being actively fixed (https://crbug.com/1253267), and once it ships
111
139
// we can remove unsafe-eval and unpkg.com from the main CSP above.
112
- const playgroundWorkerCsp = [
140
+ const playgroundWorkerCsp = makePolicy (
113
141
// unsafe-eval is needed because we use es-module-lexer to parse import
114
142
// statements in modules. es-module-lexer needs unsafe-eval because:
115
143
//
@@ -136,18 +164,30 @@ export const contentSecurityPolicyMiddleware = (
136
164
`connect-src https://unpkg.com/` ,
137
165
138
166
// 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'` ) ;
142
177
143
178
return async ( ctx , next ) => {
144
179
await next ( ) ;
180
+
181
+ let policy : string ;
145
182
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 ;
149
184
} 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 ;
151
188
}
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 ) ;
152
192
} ;
153
193
} ;
0 commit comments