Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const { HeadersList } = require('./headers')
const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
const zlib = require('node:zlib')
const {
bytesMatch,
makePolicyContainer,
clonePolicyContainer,
requestBadPort,
Expand Down Expand Up @@ -63,6 +62,7 @@ const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requ
const { getGlobalDispatcher } = require('../../global')
const { webidl } = require('../webidl')
const { STATUS_CODES } = require('node:http')
const { bytesMatch } = require('../sri/sri')
const GET_OR_HEAD = ['GET', 'HEAD']

const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
Expand Down
216 changes: 0 additions & 216 deletions lib/web/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,6 @@ const assert = require('node:assert')
const { isUint8Array } = require('node:util/types')
const { webidl } = require('../webidl')

let supportedHashes = []

// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
/** @type {import('crypto')} */
let crypto
try {
crypto = require('node:crypto')
const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
/* c8 ignore next 3 */
} catch {

}

function responseURL (response) {
// https://fetch.spec.whatwg.org/#responses
// A response has an associated URL. It is a pointer to the last URL
Expand Down Expand Up @@ -698,206 +684,6 @@ function isURLPotentiallyTrustworthy (url) {
return isOriginPotentiallyTrustworthy(url.origin)
}

/**
* @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
* @param {Uint8Array} bytes
* @param {string} metadataList
*/
function bytesMatch (bytes, metadataList) {
// If node is not built with OpenSSL support, we cannot check
// a request's integrity, so allow it by default (the spec will
// allow requests if an invalid hash is given, as precedence).
/* istanbul ignore if: only if node is built with --without-ssl */
if (crypto === undefined) {
return true
}

// 1. Let parsedMetadata be the result of parsing metadataList.
const parsedMetadata = parseMetadata(metadataList)

// 2. If parsedMetadata is no metadata, return true.
if (parsedMetadata === 'no metadata') {
return true
}

// 3. If response is not eligible for integrity validation, return false.
// TODO

// 4. If parsedMetadata is the empty set, return true.
if (parsedMetadata.length === 0) {
return true
}

// 5. Let metadata be the result of getting the strongest
// metadata from parsedMetadata.
const strongest = getStrongestMetadata(parsedMetadata)
const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)

// 6. For each item in metadata:
for (const item of metadata) {
// 1. Let algorithm be the alg component of item.
const algorithm = item.algo

// 2. Let expectedValue be the val component of item.
const expectedValue = item.hash

// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
// "be liberal with padding". This is annoying, and it's not even in the spec.

// 3. Let actualValue be the result of applying algorithm to bytes.
let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')

if (actualValue[actualValue.length - 1] === '=') {
if (actualValue[actualValue.length - 2] === '=') {
actualValue = actualValue.slice(0, -2)
} else {
actualValue = actualValue.slice(0, -1)
}
}

// 4. If actualValue is a case-sensitive match for expectedValue,
// return true.
if (compareBase64Mixed(actualValue, expectedValue)) {
return true
}
}

// 7. Return false.
return false
}

// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
// https://www.w3.org/TR/CSP2/#source-list-syntax
// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i

/**
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
* @param {string} metadata
*/
function parseMetadata (metadata) {
// 1. Let result be the empty set.
/** @type {{ algo: string, hash: string }[]} */
const result = []

// 2. Let empty be equal to true.
let empty = true

// 3. For each token returned by splitting metadata on spaces:
for (const token of metadata.split(' ')) {
// 1. Set empty to false.
empty = false

// 2. Parse token as a hash-with-options.
const parsedToken = parseHashWithOptions.exec(token)

// 3. If token does not parse, continue to the next token.
if (
parsedToken === null ||
parsedToken.groups === undefined ||
parsedToken.groups.algo === undefined
) {
// Note: Chromium blocks the request at this point, but Firefox
// gives a warning that an invalid integrity was given. The
// correct behavior is to ignore these, and subsequently not
// check the integrity of the resource.
continue
}

// 4. Let algorithm be the hash-algo component of token.
const algorithm = parsedToken.groups.algo.toLowerCase()

// 5. If algorithm is a hash function recognized by the user
// agent, add the parsed token to result.
if (supportedHashes.includes(algorithm)) {
result.push(parsedToken.groups)
}
}

// 4. Return no metadata if empty is true, otherwise return result.
if (empty === true) {
return 'no metadata'
}

return result
}

/**
* @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
*/
function getStrongestMetadata (metadataList) {
// Let algorithm be the algo component of the first item in metadataList.
// Can be sha256
let algorithm = metadataList[0].algo
// If the algorithm is sha512, then it is the strongest
// and we can return immediately
if (algorithm[3] === '5') {
return algorithm
}

for (let i = 1; i < metadataList.length; ++i) {
const metadata = metadataList[i]
// If the algorithm is sha512, then it is the strongest
// and we can break the loop immediately
if (metadata.algo[3] === '5') {
algorithm = 'sha512'
break
// If the algorithm is sha384, then a potential sha256 or sha384 is ignored
} else if (algorithm[3] === '3') {
continue
// algorithm is sha256, check if algorithm is sha384 and if so, set it as
// the strongest
} else if (metadata.algo[3] === '3') {
algorithm = 'sha384'
}
}
return algorithm
}

function filterMetadataListByAlgorithm (metadataList, algorithm) {
if (metadataList.length === 1) {
return metadataList
}

let pos = 0
for (let i = 0; i < metadataList.length; ++i) {
if (metadataList[i].algo === algorithm) {
metadataList[pos++] = metadataList[i]
}
}

metadataList.length = pos

return metadataList
}

/**
* Compares two base64 strings, allowing for base64url
* in the second string.
*
* @param {string} actualValue always base64
* @param {string} expectedValue base64 or base64url
* @returns {boolean}
*/
function compareBase64Mixed (actualValue, expectedValue) {
if (actualValue.length !== expectedValue.length) {
return false
}
for (let i = 0; i < actualValue.length; ++i) {
if (actualValue[i] !== expectedValue[i]) {
if (
(actualValue[i] === '+' && expectedValue[i] === '-') ||
(actualValue[i] === '/' && expectedValue[i] === '_')
) {
continue
}
return false
}
}

return true
}

// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
// TODO
Expand Down Expand Up @@ -1763,7 +1549,6 @@ module.exports = {
isValidHeaderValue,
isErrorLike,
fullyReadBody,
bytesMatch,
readableStreamClose,
isomorphicEncode,
urlIsLocal,
Expand All @@ -1772,7 +1557,6 @@ module.exports = {
readAllBytes,
simpleRangeHeaderValue,
buildContentRange,
parseMetadata,
createInflate,
extractMimeType,
getDecodeSplit,
Expand Down
9 changes: 9 additions & 0 deletions lib/web/sri/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Subresource Integrity

based on Editor’s Draft, 12 June 2025

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.

## Links

- [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/)
Loading
Loading