Skip to content

Commit 3127367

Browse files
UzlopakKhafraDev
andauthored
feat: extract sri from fetch, upgrade to latest spec (#4307)
* feat: extract sri from fetch, upgrade to latest spec * improve coverage * fix * remove ts-check * Apply suggestions from code review * fix * switch order according to my change in the spec * fix comment * imrprove with better helper functions * rename sri to subresource-integrity * fix * Update test/fetch/integrity.js Co-authored-by: Khafra <maitken033380023@gmail.com> * adapt --------- Co-authored-by: Khafra <maitken033380023@gmail.com>
1 parent dfa2d15 commit 3127367

13 files changed

Lines changed: 659 additions & 288 deletions

File tree

lib/web/fetch/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const { HeadersList } = require('./headers')
1414
const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
1515
const zlib = require('node:zlib')
1616
const {
17-
bytesMatch,
1817
makePolicyContainer,
1918
clonePolicyContainer,
2019
requestBadPort,
@@ -62,6 +61,7 @@ const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requ
6261
const { getGlobalDispatcher } = require('../../global')
6362
const { webidl } = require('../webidl')
6463
const { STATUS_CODES } = require('node:http')
64+
const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
6565
const { createDeferredPromise } = require('../../util/promise')
6666
const GET_OR_HEAD = ['GET', 'HEAD']
6767

lib/web/fetch/util.js

Lines changed: 0 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,6 @@ const assert = require('node:assert')
1111
const { isUint8Array } = require('node:util/types')
1212
const { webidl } = require('../webidl')
1313

14-
let supportedHashes = []
15-
16-
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
17-
/** @type {import('crypto')} */
18-
let crypto
19-
try {
20-
crypto = require('node:crypto')
21-
const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
22-
supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
23-
/* c8 ignore next 3 */
24-
} catch {
25-
26-
}
27-
2814
function responseURL (response) {
2915
// https://fetch.spec.whatwg.org/#responses
3016
// A response has an associated URL. It is a pointer to the last URL
@@ -698,206 +684,6 @@ function isURLPotentiallyTrustworthy (url) {
698684
return isOriginPotentiallyTrustworthy(url.origin)
699685
}
700686

701-
/**
702-
* @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
703-
* @param {Uint8Array} bytes
704-
* @param {string} metadataList
705-
*/
706-
function bytesMatch (bytes, metadataList) {
707-
// If node is not built with OpenSSL support, we cannot check
708-
// a request's integrity, so allow it by default (the spec will
709-
// allow requests if an invalid hash is given, as precedence).
710-
/* istanbul ignore if: only if node is built with --without-ssl */
711-
if (crypto === undefined) {
712-
return true
713-
}
714-
715-
// 1. Let parsedMetadata be the result of parsing metadataList.
716-
const parsedMetadata = parseMetadata(metadataList)
717-
718-
// 2. If parsedMetadata is no metadata, return true.
719-
if (parsedMetadata === 'no metadata') {
720-
return true
721-
}
722-
723-
// 3. If response is not eligible for integrity validation, return false.
724-
// TODO
725-
726-
// 4. If parsedMetadata is the empty set, return true.
727-
if (parsedMetadata.length === 0) {
728-
return true
729-
}
730-
731-
// 5. Let metadata be the result of getting the strongest
732-
// metadata from parsedMetadata.
733-
const strongest = getStrongestMetadata(parsedMetadata)
734-
const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)
735-
736-
// 6. For each item in metadata:
737-
for (const item of metadata) {
738-
// 1. Let algorithm be the alg component of item.
739-
const algorithm = item.algo
740-
741-
// 2. Let expectedValue be the val component of item.
742-
const expectedValue = item.hash
743-
744-
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
745-
// "be liberal with padding". This is annoying, and it's not even in the spec.
746-
747-
// 3. Let actualValue be the result of applying algorithm to bytes.
748-
let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
749-
750-
if (actualValue[actualValue.length - 1] === '=') {
751-
if (actualValue[actualValue.length - 2] === '=') {
752-
actualValue = actualValue.slice(0, -2)
753-
} else {
754-
actualValue = actualValue.slice(0, -1)
755-
}
756-
}
757-
758-
// 4. If actualValue is a case-sensitive match for expectedValue,
759-
// return true.
760-
if (compareBase64Mixed(actualValue, expectedValue)) {
761-
return true
762-
}
763-
}
764-
765-
// 7. Return false.
766-
return false
767-
}
768-
769-
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
770-
// https://www.w3.org/TR/CSP2/#source-list-syntax
771-
// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
772-
const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i
773-
774-
/**
775-
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
776-
* @param {string} metadata
777-
*/
778-
function parseMetadata (metadata) {
779-
// 1. Let result be the empty set.
780-
/** @type {{ algo: string, hash: string }[]} */
781-
const result = []
782-
783-
// 2. Let empty be equal to true.
784-
let empty = true
785-
786-
// 3. For each token returned by splitting metadata on spaces:
787-
for (const token of metadata.split(' ')) {
788-
// 1. Set empty to false.
789-
empty = false
790-
791-
// 2. Parse token as a hash-with-options.
792-
const parsedToken = parseHashWithOptions.exec(token)
793-
794-
// 3. If token does not parse, continue to the next token.
795-
if (
796-
parsedToken === null ||
797-
parsedToken.groups === undefined ||
798-
parsedToken.groups.algo === undefined
799-
) {
800-
// Note: Chromium blocks the request at this point, but Firefox
801-
// gives a warning that an invalid integrity was given. The
802-
// correct behavior is to ignore these, and subsequently not
803-
// check the integrity of the resource.
804-
continue
805-
}
806-
807-
// 4. Let algorithm be the hash-algo component of token.
808-
const algorithm = parsedToken.groups.algo.toLowerCase()
809-
810-
// 5. If algorithm is a hash function recognized by the user
811-
// agent, add the parsed token to result.
812-
if (supportedHashes.includes(algorithm)) {
813-
result.push(parsedToken.groups)
814-
}
815-
}
816-
817-
// 4. Return no metadata if empty is true, otherwise return result.
818-
if (empty === true) {
819-
return 'no metadata'
820-
}
821-
822-
return result
823-
}
824-
825-
/**
826-
* @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
827-
*/
828-
function getStrongestMetadata (metadataList) {
829-
// Let algorithm be the algo component of the first item in metadataList.
830-
// Can be sha256
831-
let algorithm = metadataList[0].algo
832-
// If the algorithm is sha512, then it is the strongest
833-
// and we can return immediately
834-
if (algorithm[3] === '5') {
835-
return algorithm
836-
}
837-
838-
for (let i = 1; i < metadataList.length; ++i) {
839-
const metadata = metadataList[i]
840-
// If the algorithm is sha512, then it is the strongest
841-
// and we can break the loop immediately
842-
if (metadata.algo[3] === '5') {
843-
algorithm = 'sha512'
844-
break
845-
// If the algorithm is sha384, then a potential sha256 or sha384 is ignored
846-
} else if (algorithm[3] === '3') {
847-
continue
848-
// algorithm is sha256, check if algorithm is sha384 and if so, set it as
849-
// the strongest
850-
} else if (metadata.algo[3] === '3') {
851-
algorithm = 'sha384'
852-
}
853-
}
854-
return algorithm
855-
}
856-
857-
function filterMetadataListByAlgorithm (metadataList, algorithm) {
858-
if (metadataList.length === 1) {
859-
return metadataList
860-
}
861-
862-
let pos = 0
863-
for (let i = 0; i < metadataList.length; ++i) {
864-
if (metadataList[i].algo === algorithm) {
865-
metadataList[pos++] = metadataList[i]
866-
}
867-
}
868-
869-
metadataList.length = pos
870-
871-
return metadataList
872-
}
873-
874-
/**
875-
* Compares two base64 strings, allowing for base64url
876-
* in the second string.
877-
*
878-
* @param {string} actualValue always base64
879-
* @param {string} expectedValue base64 or base64url
880-
* @returns {boolean}
881-
*/
882-
function compareBase64Mixed (actualValue, expectedValue) {
883-
if (actualValue.length !== expectedValue.length) {
884-
return false
885-
}
886-
for (let i = 0; i < actualValue.length; ++i) {
887-
if (actualValue[i] !== expectedValue[i]) {
888-
if (
889-
(actualValue[i] === '+' && expectedValue[i] === '-') ||
890-
(actualValue[i] === '/' && expectedValue[i] === '_')
891-
) {
892-
continue
893-
}
894-
return false
895-
}
896-
}
897-
898-
return true
899-
}
900-
901687
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
902688
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
903689
// TODO
@@ -1761,7 +1547,6 @@ module.exports = {
17611547
isValidHeaderValue,
17621548
isErrorLike,
17631549
fullyReadBody,
1764-
bytesMatch,
17651550
readableStreamClose,
17661551
isomorphicEncode,
17671552
urlIsLocal,
@@ -1770,7 +1555,6 @@ module.exports = {
17701555
readAllBytes,
17711556
simpleRangeHeaderValue,
17721557
buildContentRange,
1773-
parseMetadata,
17741558
createInflate,
17751559
extractMimeType,
17761560
getDecodeSplit,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Subresource Integrity
2+
3+
based on Editor’s Draft, 12 June 2025
4+
5+
This module provides support for Subresource Integrity (SRI) in the context of web fetch operations. SRI is a security feature that allows clients to verify that fetched resources are delivered without unexpected manipulation.
6+
7+
## Links
8+
9+
- [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/)

0 commit comments

Comments
 (0)