From 33a4935b84daadb15e025df0e87bf9fa4894283b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:14:41 +0200 Subject: [PATCH] crypto: validate normalized hash names Check the normalized SHA algorithm instead of reading the caller hash name before conversion. This prevents changing getters from bypassing the supported hash check. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Reviewed-By: James M Snell Reviewed-By: Aviv Keller --- lib/internal/crypto/util.js | 8 +++- lib/internal/crypto/webidl.js | 19 -------- .../test-webcrypto-hash-normalization.js | 47 +++++++++++++++++++ 3 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 test/parallel/test-webcrypto-hash-normalization.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 8c4a2d272b1..05da8442ba2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -23,6 +23,7 @@ const { SafeMap, SafeSet, StringPrototypeCharCodeAt, + StringPrototypeStartsWith, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -841,7 +842,12 @@ function normalizeAlgorithm(algorithm, op) { getBufferSourceBytes(idlValue), ); } else if (idlType === 'HashAlgorithmIdentifier') { - normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); + const hash = normalizeAlgorithm(idlValue, 'digest'); + if (!StringPrototypeStartsWith(hash.name, 'SHA')) { + throw lazyDOMException( + `Only SHA hashes are supported in ${desiredType}`, 'NotSupportedError'); + } + normalizedAlgorithm[member] = hash; } else if (idlType === 'AlgorithmIdentifier') { // This extension point is not used by any supported algorithm (yet?) throw lazyDOMException('Not implemented.', 'NotSupportedError'); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 73b3af6a7f5..5b3fc8d605b 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -7,7 +7,6 @@ const { ObjectPrototypeHasOwnProperty, StringPrototypeCharCodeAt, StringPrototypeSplit, - StringPrototypeStartsWith, StringPrototypeToLowerCase, } = primordials; @@ -67,18 +66,6 @@ function namedCurveValidator(V, dict) { const converters = { __proto__: null, ...webidl }; -/** - * @param {string | object} V - The hash algorithm identifier (string or object). - * @param {string} label - The dictionary name for the error message. - */ -function ensureSHA(V, label) { - const name = typeof V === 'string' ? V : V.name; - if (typeof name !== 'string' || - !StringPrototypeStartsWith(StringPrototypeToLowerCase(name), 'sha')) - throw lazyDOMException( - `Only SHA hashes are supported in ${label}`, 'NotSupportedError'); -} - converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) if (type(V) === 'Object') { @@ -200,7 +187,6 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), required: true, }, ], @@ -213,7 +199,6 @@ converters.RsaHashedImportParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), required: true, }, ], @@ -342,7 +327,6 @@ converters.EcdsaParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), required: true, }, ], @@ -368,7 +352,6 @@ for (let i = 0; i < kHmacDictionaries.length; i++) { { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, name), required: true, }, { @@ -432,7 +415,6 @@ converters.HkdfParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'HkdfParams'), required: true, }, { @@ -500,7 +482,6 @@ converters.Pbkdf2Params = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), required: true, }, ], diff --git a/test/parallel/test-webcrypto-hash-normalization.js b/test/parallel/test-webcrypto-hash-normalization.js new file mode 100644 index 00000000000..c9c09ef1b8d --- /dev/null +++ b/test/parallel/test-webcrypto-hash-normalization.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(32); + let reads = 0; + const key = await subtle.importKey('raw', data, { + name: 'HMAC', + hash: { + get name() { + reads++; + return reads === 1 ? 'SHA-384' : 'SHA-256'; + }, + }, + }, false, ['sign']); + assert.strictEqual(reads, 1); + assert.strictEqual(key.algorithm.hash.name, 'SHA-384'); + assert.strictEqual((await subtle.sign('HMAC', key, data)).byteLength, 48); + + const converted = await subtle.importKey('raw', data, { + name: 'HMAC', + hash: { name: { toString() { return 'SHA-256'; } } }, + }, false, ['sign']); + assert.strictEqual(converted.algorithm.hash.name, 'SHA-256'); + + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: {}, + }, false, ['sign']), TypeError); + const cshakeAvailable = SubtleCrypto.supports('digest', { + name: 'cSHAKE128', outputLength: 256, + }); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: 'cSHAKE128', + }, false, ['sign']), cshakeAvailable ? TypeError : { name: 'NotSupportedError' }); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: 'MD5', length: -1, + }, false, ['sign']), TypeError); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: { name: 'cSHAKE128', outputLength: 256 }, + }, false, ['sign']), { name: 'NotSupportedError' }); +})().then(common.mustCall());