| 'use strict' | ||
| /** @type {Record<string, string | undefined>} */ | ||
| const headerNameLowerCasedRecord = {} | ||
| // https://developer.mozilla.org/docs/Web/HTTP/Headers | ||
| const wellknownHeaderNames = [ | ||
| 'Accept', | ||
| 'Accept-Encoding', | ||
| 'Accept-Language', | ||
| 'Accept-Ranges', | ||
| 'Access-Control-Allow-Credentials', | ||
| 'Access-Control-Allow-Headers', | ||
| 'Access-Control-Allow-Methods', | ||
| 'Access-Control-Allow-Origin', | ||
| 'Access-Control-Expose-Headers', | ||
| 'Access-Control-Max-Age', | ||
| 'Access-Control-Request-Headers', | ||
| 'Access-Control-Request-Method', | ||
| 'Age', | ||
| 'Allow', | ||
| 'Alt-Svc', | ||
| 'Alt-Used', | ||
| 'Authorization', | ||
| 'Cache-Control', | ||
| 'Clear-Site-Data', | ||
| 'Connection', | ||
| 'Content-Disposition', | ||
| 'Content-Encoding', | ||
| 'Content-Language', | ||
| 'Content-Length', | ||
| 'Content-Location', | ||
| 'Content-Range', | ||
| 'Content-Security-Policy', | ||
| 'Content-Security-Policy-Report-Only', | ||
| 'Content-Type', | ||
| 'Cookie', | ||
| 'Cross-Origin-Embedder-Policy', | ||
| 'Cross-Origin-Opener-Policy', | ||
| 'Cross-Origin-Resource-Policy', | ||
| 'Date', | ||
| 'Device-Memory', | ||
| 'Downlink', | ||
| 'ECT', | ||
| 'ETag', | ||
| 'Expect', | ||
| 'Expect-CT', | ||
| 'Expires', | ||
| 'Forwarded', | ||
| 'From', | ||
| 'Host', | ||
| 'If-Match', | ||
| 'If-Modified-Since', | ||
| 'If-None-Match', | ||
| 'If-Range', | ||
| 'If-Unmodified-Since', | ||
| 'Keep-Alive', | ||
| 'Last-Modified', | ||
| 'Link', | ||
| 'Location', | ||
| 'Max-Forwards', | ||
| 'Origin', | ||
| 'Permissions-Policy', | ||
| 'Pragma', | ||
| 'Proxy-Authenticate', | ||
| 'Proxy-Authorization', | ||
| 'RTT', | ||
| 'Range', | ||
| 'Referer', | ||
| 'Referrer-Policy', | ||
| 'Refresh', | ||
| 'Retry-After', | ||
| 'Sec-WebSocket-Accept', | ||
| 'Sec-WebSocket-Extensions', | ||
| 'Sec-WebSocket-Key', | ||
| 'Sec-WebSocket-Protocol', | ||
| 'Sec-WebSocket-Version', | ||
| 'Server', | ||
| 'Server-Timing', | ||
| 'Service-Worker-Allowed', | ||
| 'Service-Worker-Navigation-Preload', | ||
| 'Set-Cookie', | ||
| 'SourceMap', | ||
| 'Strict-Transport-Security', | ||
| 'Supports-Loading-Mode', | ||
| 'TE', | ||
| 'Timing-Allow-Origin', | ||
| 'Trailer', | ||
| 'Transfer-Encoding', | ||
| 'Upgrade', | ||
| 'Upgrade-Insecure-Requests', | ||
| 'User-Agent', | ||
| 'Vary', | ||
| 'Via', | ||
| 'WWW-Authenticate', | ||
| 'X-Content-Type-Options', | ||
| 'X-DNS-Prefetch-Control', | ||
| 'X-Frame-Options', | ||
| 'X-Permitted-Cross-Domain-Policies', | ||
| 'X-Powered-By', | ||
| 'X-Requested-With', | ||
| 'X-XSS-Protection' | ||
| ] | ||
| for (let i = 0; i < wellknownHeaderNames.length; ++i) { | ||
| const key = wellknownHeaderNames[i] | ||
| const lowerCasedKey = key.toLowerCase() | ||
| headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = | ||
| lowerCasedKey | ||
| } | ||
| // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. | ||
| Object.setPrototypeOf(headerNameLowerCasedRecord, null) | ||
| module.exports = { | ||
| wellknownHeaderNames, | ||
| headerNameLowerCasedRecord | ||
| } |
+11
-0
@@ -12,2 +12,3 @@ 'use strict' | ||
| const { stringify } = require('querystring') | ||
| const { headerNameLowerCasedRecord } = require('./constants') | ||
@@ -222,2 +223,11 @@ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) | ||
| /** | ||
| * Retrieves a header name and returns its lowercase value. | ||
| * @param {string | Buffer} value Header name | ||
| * @returns {string} | ||
| */ | ||
| function headerNameToString (value) { | ||
| return headerNameLowerCasedRecord[value] || value.toLowerCase() | ||
| } | ||
| function parseHeaders (headers, obj = {}) { | ||
@@ -494,2 +504,3 @@ // For H2 support | ||
| isDestroyed, | ||
| headerNameToString, | ||
| parseRawHeaders, | ||
@@ -496,0 +507,0 @@ parseHeaders, |
+108
-35
@@ -10,2 +10,4 @@ 'use strict' | ||
| let supportedHashes = [] | ||
| // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable | ||
@@ -17,4 +19,6 @@ /** @type {import('crypto')|undefined} */ | ||
| crypto = require('crypto') | ||
| const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] | ||
| supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) | ||
| /* c8 ignore next 3 */ | ||
| } catch { | ||
| } | ||
@@ -547,3 +551,6 @@ | ||
| // 3. If parsedMetadata is the empty set, 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) { | ||
@@ -553,11 +560,8 @@ return true | ||
| // 4. Let metadata be the result of getting the strongest | ||
| // 5. Let metadata be the result of getting the strongest | ||
| // metadata from parsedMetadata. | ||
| const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) | ||
| // get the strongest algorithm | ||
| const strongest = list[0].algo | ||
| // get all entries that use the strongest algorithm; ignore weaker | ||
| const metadata = list.filter((item) => item.algo === strongest) | ||
| const strongest = getStrongestMetadata(parsedMetadata) | ||
| const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) | ||
| // 5. For each item in metadata: | ||
| // 6. For each item in metadata: | ||
| for (const item of metadata) { | ||
@@ -568,3 +572,3 @@ // 1. Let algorithm be the alg component of item. | ||
| // 2. Let expectedValue be the val component of item. | ||
| let expectedValue = item.hash | ||
| const expectedValue = item.hash | ||
@@ -574,11 +578,11 @@ // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e | ||
| if (expectedValue.endsWith('==')) { | ||
| expectedValue = expectedValue.slice(0, -2) | ||
| } | ||
| // 3. Let actualValue be the result of applying algorithm to bytes. | ||
| let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') | ||
| if (actualValue.endsWith('==')) { | ||
| actualValue = actualValue.slice(0, -2) | ||
| if (actualValue[actualValue.length - 1] === '=') { | ||
| if (actualValue[actualValue.length - 2] === '=') { | ||
| actualValue = actualValue.slice(0, -2) | ||
| } else { | ||
| actualValue = actualValue.slice(0, -1) | ||
| } | ||
| } | ||
@@ -588,18 +592,8 @@ | ||
| // return true. | ||
| if (actualValue === expectedValue) { | ||
| if (compareBase64Mixed(actualValue, expectedValue)) { | ||
| return true | ||
| } | ||
| let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') | ||
| if (actualBase64URL.endsWith('==')) { | ||
| actualBase64URL = actualBase64URL.slice(0, -2) | ||
| } | ||
| if (actualBase64URL === expectedValue) { | ||
| return true | ||
| } | ||
| } | ||
| // 6. Return false. | ||
| // 7. Return false. | ||
| return false | ||
@@ -611,3 +605,3 @@ } | ||
| // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 | ||
| const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i | ||
| const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i | ||
@@ -626,4 +620,2 @@ /** | ||
| const supportedHashes = crypto.getHashes() | ||
| // 3. For each token returned by splitting metadata on spaces: | ||
@@ -638,3 +630,7 @@ for (const token of metadata.split(' ')) { | ||
| // 3. If token does not parse, continue to the next token. | ||
| if (parsedToken === null || parsedToken.groups === undefined) { | ||
| if ( | ||
| parsedToken === null || | ||
| parsedToken.groups === undefined || | ||
| parsedToken.groups.algo === undefined | ||
| ) { | ||
| // Note: Chromium blocks the request at this point, but Firefox | ||
@@ -648,7 +644,7 @@ // gives a warning that an invalid integrity was given. The | ||
| // 4. Let algorithm be the hash-algo component of token. | ||
| const algorithm = parsedToken.groups.algo | ||
| 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.toLowerCase())) { | ||
| if (supportedHashes.includes(algorithm)) { | ||
| result.push(parsedToken.groups) | ||
@@ -666,2 +662,78 @@ } | ||
| /** | ||
| * @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 | ||
@@ -1082,3 +1154,4 @@ function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { | ||
| readAllBytes, | ||
| normalizeMethodRecord | ||
| normalizeMethodRecord, | ||
| parseMetadata | ||
| } |
@@ -187,8 +187,13 @@ 'use strict' | ||
| function shouldRemoveHeader (header, removeContent, unknownOrigin) { | ||
| return ( | ||
| (header.length === 4 && header.toString().toLowerCase() === 'host') || | ||
| (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || | ||
| (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || | ||
| (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') | ||
| ) | ||
| if (header.length === 4) { | ||
| return util.headerNameToString(header) === 'host' | ||
| } | ||
| if (removeContent && util.headerNameToString(header).startsWith('content-')) { | ||
| return true | ||
| } | ||
| if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { | ||
| const name = util.headerNameToString(header) | ||
| return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' | ||
| } | ||
| return false | ||
| } | ||
@@ -195,0 +200,0 @@ |
+2
-2
| { | ||
| "name": "undici", | ||
| "version": "5.28.3", | ||
| "version": "5.28.4", | ||
| "description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -113,3 +113,3 @@ "homepage": "https://undici.nodejs.org", | ||
| "form-data": "^4.0.0", | ||
| "formdata-node": "^6.0.3", | ||
| "formdata-node": "^4.3.1", | ||
| "https-pem": "^3.0.0", | ||
@@ -116,0 +116,0 @@ "husky": "^8.0.1", |
Network access
Supply chain riskThis module accesses the network.
Found 5 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 3 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 5 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 3 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1171978
0.43%155
0.65%20693
0.96%