@@ -5,10 +5,19 @@ const { performance } = require('perf_hooks')
5
5
const { isBlobLike, toUSVString, ReadableStreamFrom } = require ( '../core/util' )
6
6
const assert = require ( 'assert' )
7
7
const { isUint8Array } = require ( 'util/types' )
8
- const { createHash } = require ( 'crypto' )
9
8
10
9
let File
11
10
11
+ // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
12
+ /** @type {import('crypto')|undefined } */
13
+ let crypto
14
+
15
+ try {
16
+ crypto = require ( 'crypto' )
17
+ } catch {
18
+
19
+ }
20
+
12
21
// https://fetch.spec.whatwg.org/#block-bad-port
13
22
const badPorts = [
14
23
'1' , '7' , '9' , '11' , '13' , '15' , '17' , '19' , '20' , '21' , '22' , '23' , '25' , '37' , '42' , '43' , '53' , '69' , '77' , '79' ,
@@ -341,9 +350,114 @@ function determineRequestsReferrer (request) {
341
350
return 'no-referrer'
342
351
}
343
352
344
- function matchRequestIntegrity ( request , bytes ) {
345
- const [ algo , expectedHashValue ] = request . integrity . split ( '-' , 2 )
346
- return createHash ( algo ) . update ( bytes ) . digest ( 'hex' ) === expectedHashValue
353
+ /**
354
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
355
+ * @param {Uint8Array } bytes
356
+ * @param {string } metadataList
357
+ */
358
+ function bytesMatch ( bytes , metadataList ) {
359
+ // If node is not built with OpenSSL support, we cannot check
360
+ // a request's integrity, so allow it by default (the spec will
361
+ // allow requests if an invalid hash is given, as precedence).
362
+ /* istanbul ignore if: only if node is built with --without-ssl */
363
+ if ( crypto === undefined ) {
364
+ return true
365
+ }
366
+
367
+ // 1. Let parsedMetadata be the result of parsing metadataList.
368
+ const parsedMetadata = parseMetadata ( metadataList )
369
+
370
+ // 2. If parsedMetadata is no metadata, return true.
371
+ if ( parsedMetadata === 'no metadata' ) {
372
+ return true
373
+ }
374
+
375
+ // 3. If parsedMetadata is the empty set, return true.
376
+ if ( parsedMetadata . length === 0 ) {
377
+ return true
378
+ }
379
+
380
+ // 4. Let metadata be the result of getting the strongest
381
+ // metadata from parsedMetadata.
382
+ // Note: this will only work for SHA- algorithms and it's lazy *at best*.
383
+ const metadata = parsedMetadata . sort ( ( c , d ) => d . algo . localeCompare ( c . algo ) )
384
+
385
+ // 5. For each item in metadata:
386
+ for ( const item of metadata ) {
387
+ // 1. Let algorithm be the alg component of item.
388
+ const algorithm = item . algo
389
+
390
+ // 2. Let expectedValue be the val component of item.
391
+ const expectedValue = item . hash
392
+
393
+ // 3. Let actualValue be the result of applying algorithm to bytes.
394
+ // Note: "applying algorithm to bytes" converts the result to base64
395
+ const actualValue = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64' )
396
+
397
+ // 4. If actualValue is a case-sensitive match for expectedValue,
398
+ // return true.
399
+ if ( actualValue === expectedValue ) {
400
+ return true
401
+ }
402
+ }
403
+
404
+ // 6. Return false.
405
+ return false
406
+ }
407
+
408
+ // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
409
+ // hash-algo is defined in Content Security Policy 2 Section 4.2
410
+ // base64-value is similary defined there
411
+ // VCHAR is defined https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
412
+ const parseHashWithOptions = / ( (?< algo > s h a 2 5 6 | s h a 3 8 4 | s h a 5 1 2 ) - (?< hash > [ A - z 0 - 9 + / ] { 1 } .* = { 1 , 2 } ) ) ( + [ \x21 - \x7e ] ? ) ? / i
413
+
414
+ /**
415
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
416
+ * @param {string } metadata
417
+ */
418
+ function parseMetadata ( metadata ) {
419
+ // 1. Let result be the empty set.
420
+ /** @type {{ algo: string, hash: string }[] } */
421
+ const result = [ ]
422
+
423
+ // 2. Let empty be equal to true.
424
+ let empty = true
425
+
426
+ const supportedHashes = crypto . getHashes ( )
427
+
428
+ // 3. For each token returned by splitting metadata on spaces:
429
+ for ( const token of metadata . split ( ' ' ) ) {
430
+ // 1. Set empty to false.
431
+ empty = false
432
+
433
+ // 2. Parse token as a hash-with-options.
434
+ const parsedToken = parseHashWithOptions . exec ( token )
435
+
436
+ // 3. If token does not parse, continue to the next token.
437
+ if ( parsedToken === null || parsedToken . groups === undefined ) {
438
+ // Note: Chromium blocks the request at this point, but Firefox
439
+ // gives a warning that an invalid integrity was given. The
440
+ // correct behavior is to ignore these, and subsequently not
441
+ // check the integrity of the resource.
442
+ continue
443
+ }
444
+
445
+ // 4. Let algorithm be the hash-algo component of token.
446
+ const algorithm = parsedToken . groups . algo
447
+
448
+ // 5. If algorithm is a hash function recognized by the user
449
+ // agent, add the parsed token to result.
450
+ if ( supportedHashes . includes ( algorithm . toLowerCase ( ) ) ) {
451
+ result . push ( parsedToken . groups )
452
+ }
453
+ }
454
+
455
+ // 4. Return no metadata if empty is true, otherwise return result.
456
+ if ( empty === true ) {
457
+ return 'no metadata'
458
+ }
459
+
460
+ return result
347
461
}
348
462
349
463
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
@@ -501,7 +615,6 @@ module.exports = {
501
615
toUSVString,
502
616
tryUpgradeRequestToAPotentiallyTrustworthyURL,
503
617
coarsenedSharedCurrentTime,
504
- matchRequestIntegrity,
505
618
determineRequestsReferrer,
506
619
makePolicyContainer,
507
620
clonePolicyContainer,
@@ -528,5 +641,6 @@ module.exports = {
528
641
isValidHeaderValue,
529
642
hasOwn,
530
643
isErrorLike,
531
- fullyReadBody
644
+ fullyReadBody,
645
+ bytesMatch
532
646
}
0 commit comments