Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
SafeSet,
StringPrototypeToLowerCase,
} = primordials;

const { Buffer } = require('buffer');
Expand Down Expand Up @@ -332,7 +333,7 @@ function cfrgImportKey(
return undefined;
}

if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}

Expand Down
3 changes: 2 additions & 1 deletion lib/internal/crypto/ml_dsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
SafeSet,
StringPrototypeToLowerCase,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeSet,
Uint8Array,
Expand Down Expand Up @@ -276,7 +277,7 @@ function mlDsaImportKey(
return undefined;
}

if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}

Expand Down
3 changes: 2 additions & 1 deletion lib/internal/crypto/ml_kem.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const {
PromiseWithResolvers,
SafeSet,
StringPrototypeToLowerCase,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeSet,
Uint8Array,
Expand Down Expand Up @@ -209,7 +210,7 @@ function mlKemImportKey(
return undefined;
}

if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}

Expand Down
20 changes: 10 additions & 10 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const {
ObjectEntries,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
Promise,
PromiseWithResolvers,
StringPrototypeToUpperCase,
Symbol,
TypedArrayPrototypeGetBuffer,
Expand Down Expand Up @@ -656,15 +656,15 @@ function onDone(resolve, reject, err, result) {
}

function jobPromise(getJob) {
return new Promise((resolve, reject) => {
try {
const job = getJob();
job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject);
job.run();
} catch (err) {
onDone(resolve, reject, err);
}
});
const { promise, resolve, reject } = PromiseWithResolvers();
try {
const job = getJob();
job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject);
job.run();
} catch (err) {
onDone(resolve, reject, err);
}
return promise;
}

// In WebCrypto, the publicExponent option in RSA is represented as a
Expand Down
12 changes: 7 additions & 5 deletions lib/internal/crypto/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,14 @@ converters.object = (V, opts) => {

const isNonSharedArrayBuffer = isArrayBuffer;

/**
* @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) {
if (
typeof V === 'string' ?
!StringPrototypeStartsWith(StringPrototypeToLowerCase(V), 'sha') :
V.name?.toLowerCase?.().startsWith('sha') === false
)
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');
}
Expand Down
131 changes: 131 additions & 0 deletions test/parallel/test-webcrypto-promise-prototype-pollution.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as common from '../common/index.mjs';

if (!common.hasCrypto) common.skip('missing crypto');

// WebCrypto subtle methods must not leak intermediate values
// through Promise.prototype.then pollution.
// Regression test for https://github.com/nodejs/node/pull/61492
// and https://github.com/nodejs/node/issues/59699.

import * as assert from 'node:assert/strict';
import { hasOpenSSL } from '../common/crypto.js';

const { subtle } = globalThis.crypto;

const originalThen = Promise.prototype.then;
const intercepted = [];

// Pollute Promise.prototype.then to record all resolved values.
Promise.prototype.then = function(onFulfilled, ...rest) {
return originalThen.call(this, function(value) {
intercepted.push(value);
return typeof onFulfilled === 'function' ? onFulfilled(value) : value;
}, ...rest);
};

async function test(label, fn) {
const result = await fn();
assert.strictEqual(
intercepted.length, 0,
`Promise.prototype.then was called during ${label}`
);
Comment thread
panva marked this conversation as resolved.
Outdated
return result;
}

await test('digest', () =>
subtle.digest('SHA-256', new Uint8Array([1, 2, 3])));

await test('generateKey (AES-CBC)', () =>
subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));

await test('generateKey (ECDSA)', () =>
subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']));

const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32));

const importedKey = await test('importKey', () =>
subtle.importKey('raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']));

const exportableKey = await test('importKey (extractable)', () =>
subtle.importKey('raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));

await test('exportKey', () =>
subtle.exportKey('raw', exportableKey));

const iv = globalThis.crypto.getRandomValues(new Uint8Array(16));
const plaintext = new TextEncoder().encode('Hello, world!');

const ciphertext = await test('encrypt', () =>
subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext));

await test('decrypt', () =>
subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext));

const signingKey = await test('generateKey (HMAC)', () =>
subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']));

const data = new TextEncoder().encode('test data');

const signature = await test('sign', () =>
subtle.sign('HMAC', signingKey, data));

await test('verify', () =>
subtle.verify('HMAC', signingKey, signature, data));

const pbkdf2Key = await test('importKey (PBKDF2)', () =>
subtle.importKey('raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']));

await test('deriveBits', () =>
subtle.deriveBits(
{ name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' },
pbkdf2Key, 256));

// deriveKey — this was the primary leak reported in the issue
await test('deriveKey', () =>
subtle.deriveKey(
{ name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' },
pbkdf2Key,
{ name: 'AES-CBC', length: 256 },
true,
['encrypt', 'decrypt']));

const wrappingKey = await test('generateKey (AES-KW)', () =>
subtle.generateKey({ name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']));

const keyToWrap = await test('generateKey (AES-CBC for wrap)', () =>
subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));

const wrapped = await test('wrapKey', () =>
subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'));

await test('unwrapKey', () =>
subtle.unwrapKey(
'raw', wrapped, wrappingKey, 'AES-KW',
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));

const { privateKey } = await test('generateKey (ECDSA for getPublicKey)', () =>
subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']));

await test('getPublicKey', () =>
subtle.getPublicKey(privateKey, ['verify']));

if (hasOpenSSL(3, 5)) {
const kemPair = await test('generateKey (ML-KEM-768)', () =>
subtle.generateKey(
{ name: 'ML-KEM-768' }, false,
['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']));

const { ciphertext: ct1 } = await test('encapsulateKey', () =>
subtle.encapsulateKey(
{ name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']));

await test('decapsulateKey', () =>
subtle.decapsulateKey(
{ name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']));

const { ciphertext: ct2 } = await test('encapsulateBits', () =>
subtle.encapsulateBits({ name: 'ML-KEM-768' }, kemPair.publicKey));

await test('decapsulateBits', () =>
subtle.decapsulateBits({ name: 'ML-KEM-768' }, kemPair.privateKey, ct2));
}
Loading