-
Notifications
You must be signed in to change notification settings - Fork 46
/
csp.ts
442 lines (388 loc) · 13.7 KB
/
csp.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/**
* @fileoverview CSP definitions and helper functions.
* @author [email protected] (Lukas Weichselbaum)
*
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Finding, Severity, Type} from './finding';
/**
* Content Security Policy object.
* List of valid CSP directives:
* - http://www.w3.org/TR/CSP2/#directives
* - https://www.w3.org/TR/upgrade-insecure-requests/
*/
export class Csp {
directives: Record<string, string[]|undefined> = {};
/**
* Creates a CSP object from a list of directives.
* @param directives CSP directives.
*/
constructor(directives: Record<string, string[]|undefined> = {}) {
for (const [directive, directiveValues] of Object.entries(directives)) {
if (directiveValues) {
this.directives[directive] = [...directiveValues];
}
}
}
/**
* Clones a CSP object.
* @return clone of parsedCsp.
*/
clone(): Csp {
// Use the constructor that takes in directives to create a deep copy.
return new Csp(this.directives);
}
/**
* Converts this CSP back into a string.
* @return CSP string.
*/
convertToString(): string {
let cspString = '';
for (const [directive, directiveValues] of Object.entries(
this.directives)) {
cspString += directive;
if (directiveValues !== undefined) {
for (let value, i = 0; (value = directiveValues[i]); i++) {
cspString += ' ';
cspString += value;
}
}
cspString += '; ';
}
return cspString;
}
/**
* Returns CSP as it would be seen by a UA supporting a specific CSP version.
* @param cspVersion CSP.
* @param optFindings findings about ignored directive values will be added
* to this array, if passed. (e.g. CSP2 ignores 'unsafe-inline' in
* presence of a nonce or a hash)
* @return The effective CSP.
*/
getEffectiveCsp(cspVersion: Version, optFindings?: Finding[]): Csp {
const findings = optFindings || [];
const effectiveCsp = this.clone();
[Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM]
.forEach(directiveToNormalize => {
const directive = effectiveCsp.getEffectiveDirective(
directiveToNormalize) as Directive;
const values = this.directives[directive] || [];
const effectiveCspValues = effectiveCsp.directives[directive];
if (effectiveCspValues &&
(effectiveCsp.policyHasScriptNonces(directive) ||
effectiveCsp.policyHasScriptHashes(directive))) {
if (cspVersion >= Version.CSP2) {
// Ignore 'unsafe-inline' in CSP >= v2, if a nonce or a hash is
// present.
if (values.includes(Keyword.UNSAFE_INLINE)) {
arrayRemove(effectiveCspValues, Keyword.UNSAFE_INLINE);
findings.push(new Finding(
Type.IGNORED,
'unsafe-inline is ignored if a nonce or a hash is present. ' +
'(CSP2 and above)',
Severity.NONE, directive, Keyword.UNSAFE_INLINE));
}
} else {
// remove nonces and hashes (not supported in CSP < v2).
for (const value of values) {
if (value.startsWith('\'nonce-') || value.startsWith('\'sha')) {
arrayRemove(effectiveCspValues, value);
}
}
}
}
if (effectiveCspValues && this.policyHasStrictDynamic(directive)) {
// Ignore allowlist in CSP >= v3 in presence of 'strict-dynamic'.
if (cspVersion >= Version.CSP3) {
for (const value of values) {
// Because of 'strict-dynamic' all host-source and scheme-source
// expressions, as well as the "'unsafe-inline'" and "'self'
// keyword-sources will be ignored.
// https://w3c.github.io/webappsec-csp/#strict-dynamic-usage
if (!value.startsWith('\'') || value === Keyword.SELF ||
value === Keyword.UNSAFE_INLINE) {
arrayRemove(effectiveCspValues, value);
findings.push(new Finding(
Type.IGNORED,
'Because of strict-dynamic this entry is ignored in CSP3 and above',
Severity.NONE, directive, value));
}
}
} else {
// strict-dynamic not supported.
arrayRemove(effectiveCspValues, Keyword.STRICT_DYNAMIC);
}
}
});
if (cspVersion < Version.CSP3) {
// Remove CSP3 directives from pre-CSP3 policies.
// https://w3c.github.io/webappsec-csp/#changes-from-level-2
delete effectiveCsp.directives[Directive.REPORT_TO];
delete effectiveCsp.directives[Directive.WORKER_SRC];
delete effectiveCsp.directives[Directive.MANIFEST_SRC];
delete effectiveCsp.directives[Directive.TRUSTED_TYPES];
delete effectiveCsp.directives[Directive.REQUIRE_TRUSTED_TYPES_FOR];
delete effectiveCsp.directives[Directive.SCRIPT_SRC_ATTR];
delete effectiveCsp.directives[Directive.SCRIPT_SRC_ELEM];
delete effectiveCsp.directives[Directive.STYLE_SRC_ATTR];
delete effectiveCsp.directives[Directive.STYLE_SRC_ELEM];
}
return effectiveCsp;
}
/**
* Returns default-src if directive is a fetch directive and is not present in
* this CSP. Otherwise the provided directive is returned.
* @param directive CSP.
* @return The effective directive.
*/
getEffectiveDirective(directive: string): string {
if (directive in this.directives) {
return directive;
}
if ((directive === Directive.SCRIPT_SRC_ATTR ||
directive === Directive.SCRIPT_SRC_ELEM) &&
Directive.SCRIPT_SRC in this.directives) {
return Directive.SCRIPT_SRC;
}
if ((directive === Directive.STYLE_SRC_ATTR ||
directive === Directive.STYLE_SRC_ELEM) &&
Directive.STYLE_SRC in this.directives) {
return Directive.STYLE_SRC;
}
// Only fetch directives default to default-src.
if (FETCH_DIRECTIVES.includes(directive as Directive)) {
return Directive.DEFAULT_SRC;
}
return directive;
}
/**
* Returns the passed directives if present in this CSP or default-src
* otherwise.
* @param directives CSP.
* @return The effective directives.
*/
getEffectiveDirectives(directives: string[]): string[] {
const effectiveDirectives =
new Set(directives.map((val) => this.getEffectiveDirective(val)));
return [...effectiveDirectives];
}
/**
* Checks if this CSP is using nonces for scripts.
* @return true, if this CSP is using script nonces.
*/
policyHasScriptNonces(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.some((val) => isNonce(val));
}
/**
* Checks if this CSP is using hashes for scripts.
* @return true, if this CSP is using script hashes.
*/
policyHasScriptHashes(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.some((val) => isHash(val));
}
/**
* Checks if this CSP is using strict-dynamic.
* @return true, if this CSP is using CSP nonces.
*/
policyHasStrictDynamic(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.includes(Keyword.STRICT_DYNAMIC);
}
}
/**
* CSP directive source keywords.
*/
export enum Keyword {
SELF = '\'self\'',
NONE = '\'none\'',
UNSAFE_INLINE = '\'unsafe-inline\'',
UNSAFE_EVAL = '\'unsafe-eval\'',
WASM_EVAL = '\'wasm-eval\'',
WASM_UNSAFE_EVAL = '\'wasm-unsafe-eval\'',
STRICT_DYNAMIC = '\'strict-dynamic\'',
UNSAFE_HASHED_ATTRIBUTES = '\'unsafe-hashed-attributes\'',
UNSAFE_HASHES = '\'unsafe-hashes\'',
REPORT_SAMPLE = '\'report-sample\'',
BLOCK = '\'block\'',
ALLOW = '\'allow\'',
}
/**
* CSP directive source keywords.
*/
export enum TrustedTypesSink {
SCRIPT = '\'script\'',
}
/**
* CSP v3 directives.
* List of valid CSP directives:
* - http://www.w3.org/TR/CSP2/#directives
* - https://www.w3.org/TR/upgrade-insecure-requests/
*
*/
export enum Directive {
// Fetch directives
CHILD_SRC = 'child-src',
CONNECT_SRC = 'connect-src',
DEFAULT_SRC = 'default-src',
FONT_SRC = 'font-src',
FRAME_SRC = 'frame-src',
IMG_SRC = 'img-src',
MEDIA_SRC = 'media-src',
OBJECT_SRC = 'object-src',
SCRIPT_SRC = 'script-src',
SCRIPT_SRC_ATTR = 'script-src-attr',
SCRIPT_SRC_ELEM = 'script-src-elem',
STYLE_SRC = 'style-src',
STYLE_SRC_ATTR = 'style-src-attr',
STYLE_SRC_ELEM = 'style-src-elem',
PREFETCH_SRC = 'prefetch-src',
MANIFEST_SRC = 'manifest-src',
WORKER_SRC = 'worker-src',
// Document directives
BASE_URI = 'base-uri',
PLUGIN_TYPES = 'plugin-types',
SANDBOX = 'sandbox',
DISOWN_OPENER = 'disown-opener',
// Navigation directives
FORM_ACTION = 'form-action',
FRAME_ANCESTORS = 'frame-ancestors',
NAVIGATE_TO = 'navigate-to',
// Reporting directives
REPORT_TO = 'report-to',
REPORT_URI = 'report-uri',
// Other directives
BLOCK_ALL_MIXED_CONTENT = 'block-all-mixed-content',
UPGRADE_INSECURE_REQUESTS = 'upgrade-insecure-requests',
REFLECTED_XSS = 'reflected-xss',
REFERRER = 'referrer',
REQUIRE_SRI_FOR = 'require-sri-for',
TRUSTED_TYPES = 'trusted-types',
// https://github.com/WICG/trusted-types
REQUIRE_TRUSTED_TYPES_FOR = 'require-trusted-types-for',
WEBRTC = 'webrtc',
}
/**
* CSP v3 fetch directives.
* Fetch directives control the locations from which resources may be loaded.
* https://w3c.github.io/webappsec-csp/#directives-fetch
*
*/
export const FETCH_DIRECTIVES: Directive[] = [
Directive.CHILD_SRC, Directive.CONNECT_SRC, Directive.DEFAULT_SRC,
Directive.FONT_SRC, Directive.FRAME_SRC, Directive.IMG_SRC,
Directive.MANIFEST_SRC, Directive.MEDIA_SRC, Directive.OBJECT_SRC,
Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM,
Directive.STYLE_SRC, Directive.STYLE_SRC_ATTR, Directive.STYLE_SRC_ELEM,
Directive.WORKER_SRC
];
/**
* CSP version.
*/
export enum Version {
CSP1 = 1,
CSP2,
CSP3
}
/**
* Checks if a string is a valid CSP directive.
* @param directive value to check.
* @return True if directive is a valid CSP directive.
*/
export function isDirective(directive: string): boolean {
return Object.values(Directive).includes(directive as Directive);
}
/**
* Checks if a string is a valid CSP keyword.
* @param keyword value to check.
* @return True if keyword is a valid CSP keyword.
*/
export function isKeyword(keyword: string): boolean {
return Object.values(Keyword).includes(keyword as Keyword);
}
/**
* Checks if a string is a valid URL scheme.
* Scheme part + ":"
* For scheme part see https://tools.ietf.org/html/rfc3986#section-3.1
* @param urlScheme value to check.
* @return True if urlScheme has a valid scheme.
*/
export function isUrlScheme(urlScheme: string): boolean {
const pattern = new RegExp('^[a-zA-Z][+a-zA-Z0-9.-]*:$');
return pattern.test(urlScheme);
}
/**
* A regex pattern to check nonce prefix and Base64 formatting of a nonce value.
*/
export const STRICT_NONCE_PATTERN =
new RegExp('^\'nonce-[a-zA-Z0-9+/_-]+[=]{0,2}\'$');
/** A regex pattern for checking if nonce prefix. */
export const NONCE_PATTERN = new RegExp('^\'nonce-(.+)\'$');
/**
* Checks if a string is a valid CSP nonce.
* See http://www.w3.org/TR/CSP2/#nonce_value
* @param nonce value to check.
* @param strictCheck Check if the nonce uses the base64 charset.
* @return True if nonce is has a valid CSP nonce.
*/
export function isNonce(nonce: string, strictCheck?: boolean): boolean {
const pattern = strictCheck ? STRICT_NONCE_PATTERN : NONCE_PATTERN;
return pattern.test(nonce);
}
/**
* A regex pattern to check hash prefix and Base64 formatting of a hash value.
*/
export const STRICT_HASH_PATTERN =
new RegExp('^\'(sha256|sha384|sha512)-[a-zA-Z0-9+/]+[=]{0,2}\'$');
/** A regex pattern to check hash prefix. */
export const HASH_PATTERN = new RegExp('^\'(sha256|sha384|sha512)-(.+)\'$');
/**
* Checks if a string is a valid CSP hash.
* See http://www.w3.org/TR/CSP2/#hash_value
* @param hash value to check.
* @param strictCheck Check if the hash uses the base64 charset.
* @return True if hash is has a valid CSP hash.
*/
export function isHash(hash: string, strictCheck?: boolean): boolean {
const pattern = strictCheck ? STRICT_HASH_PATTERN : HASH_PATTERN;
return pattern.test(hash);
}
/**
* Class to represent all generic CSP errors.
*/
export class CspError extends Error {
/**
* @param message An optional error message.
*/
constructor(message?: string) {
super(message);
}
}
/**
* Mutate the given array to remove the first instance of the given item
*/
function arrayRemove<T>(arr: T[], item: T): void {
if (arr.includes(item)) {
const idx = arr.findIndex(elem => item === elem);
arr.splice(idx, 1);
}
}