crypto: fix cross-realm SharedArrayBuffer validation

PR-URL: https://github.com/nodejs/node/pull/57974
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Antoine du Hamel 2025-04-24 15:28:51 +02:00 committed by GitHub
parent 2c315d24f5
commit 5d15cbb416
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 55 deletions

View File

@ -25,9 +25,6 @@ const {
String, String,
TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetSymbolToStringTag, TypedArrayPrototypeGetSymbolToStringTag,
globalThis: {
SharedArrayBuffer,
},
} = primordials; } = primordials;
const { const {
@ -47,7 +44,7 @@ const {
validateMaxBufferLength, validateMaxBufferLength,
kNamedCurveAliases, kNamedCurveAliases,
} = require('internal/crypto/util'); } = require('internal/crypto/util');
const { isArrayBuffer } = require('internal/util/types'); const { isArrayBuffer, isSharedArrayBuffer } = require('internal/util/types');
// https://tc39.es/ecma262/#sec-tonumber // https://tc39.es/ecma262/#sec-tonumber
function toNumber(value, opts = kEmptyObject) { function toNumber(value, opts = kEmptyObject) {
@ -195,13 +192,6 @@ converters.object = (V, opts) => {
const isNonSharedArrayBuffer = isArrayBuffer; const isNonSharedArrayBuffer = isArrayBuffer;
function isSharedArrayBuffer(V) {
// SharedArrayBuffers can be disabled with --no-harmony-sharedarraybuffer.
if (SharedArrayBuffer !== undefined)
return ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V);
return false;
}
converters.Uint8Array = (V, opts = kEmptyObject) => { converters.Uint8Array = (V, opts = kEmptyObject) => {
if (!ArrayBufferIsView(V) || if (!ArrayBufferIsView(V) ||
TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') { TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') {

View File

@ -1,5 +1,4 @@
'use strict'; 'use strict';
// Flags: --expose-internals
const common = require('../common'); const common = require('../common');
if (!common.hasCrypto) if (!common.hasCrypto)
common.skip('missing crypto'); common.skip('missing crypto');
@ -7,7 +6,6 @@ if (!common.hasCrypto)
const assert = require('assert'); const assert = require('assert');
const { subtle } = globalThis.crypto; const { subtle } = globalThis.crypto;
const vm = require('vm'); const vm = require('vm');
const { isArrayBuffer } = require('internal/util/types');
// Test with same-realm ArrayBuffer // Test with same-realm ArrayBuffer
{ {
@ -15,7 +13,6 @@ const { isArrayBuffer } = require('internal/util/types');
subtle.digest('SHA-256', samerealmData) subtle.digest('SHA-256', samerealmData)
.then(common.mustCall((result) => { .then(common.mustCall((result) => {
assert(isArrayBuffer(result));
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
})); }));
} }
@ -35,11 +32,30 @@ const { isArrayBuffer } = require('internal/util/types');
// This should still work, since we're checking structural type // This should still work, since we're checking structural type
subtle.digest('SHA-256', crossrealmBuffer) subtle.digest('SHA-256', crossrealmBuffer)
.then(common.mustCall((result) => { .then(common.mustCall((result) => {
assert(isArrayBuffer(result));
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
})); }));
} }
// Cross-realm SharedArrayBuffer should be handled like any SharedArrayBuffer
{
const context = vm.createContext({});
const crossrealmSAB = vm.runInContext('new SharedArrayBuffer(4)', context);
assert.notStrictEqual(
Object.getPrototypeOf(crossrealmSAB),
SharedArrayBuffer.prototype
);
Promise.allSettled([
subtle.digest('SHA-256', new Uint8Array(new SharedArrayBuffer(4))),
subtle.digest('SHA-256', new Uint8Array(crossrealmSAB)),
]).then(common.mustCall((r) => {
assert.partialDeepStrictEqual(r, [
{ status: 'rejected' },
{ status: 'rejected' },
]);
assert.strictEqual(r[1].reason.message, r[0].reason.message);
}));
}
// Test with both TypedArray buffer methods // Test with both TypedArray buffer methods
{ {
const context = vm.createContext({}); const context = vm.createContext({});
@ -48,14 +64,12 @@ const { isArrayBuffer } = require('internal/util/types');
// Test the .buffer property // Test the .buffer property
subtle.digest('SHA-256', crossrealmUint8Array.buffer) subtle.digest('SHA-256', crossrealmUint8Array.buffer)
.then(common.mustCall((result) => { .then(common.mustCall((result) => {
assert(isArrayBuffer(result));
assert.strictEqual(result.byteLength, 32); assert.strictEqual(result.byteLength, 32);
})); }));
// Test passing the TypedArray directly (should work both before and after the fix) // Test passing the TypedArray directly (should work both before and after the fix)
subtle.digest('SHA-256', crossrealmUint8Array) subtle.digest('SHA-256', crossrealmUint8Array)
.then(common.mustCall((result) => { .then(common.mustCall((result) => {
assert(isArrayBuffer(result));
assert.strictEqual(result.byteLength, 32); assert.strictEqual(result.byteLength, 32);
})); }));
} }
@ -76,34 +90,32 @@ const { isArrayBuffer } = require('internal/util/types');
name: 'AES-GCM', name: 'AES-GCM',
length: 256 length: 256
}, true, ['encrypt', 'decrypt']) }, true, ['encrypt', 'decrypt'])
.then(common.mustCall((key) => { .then(async (key) => {
// Create an initialization vector // Create an initialization vector
const iv = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt using the cross-realm ArrayBuffer // Encrypt using the cross-realm ArrayBuffer
return subtle.encrypt( const ciphertext = await subtle.encrypt(
{ name: 'AES-GCM', iv }, { name: 'AES-GCM', iv },
key, key,
crossRealmBuffer crossRealmBuffer
).then((ciphertext) => { );
// Decrypt // Decrypt
return subtle.decrypt( const plaintext = await subtle.decrypt(
{ name: 'AES-GCM', iv }, { name: 'AES-GCM', iv },
key, key,
ciphertext ciphertext
); );
}).then(common.mustCall((plaintext) => {
// Verify the decrypted content matches original // Verify the decrypted content matches original
const decryptedView = new Uint8Array(plaintext); const decryptedView = new Uint8Array(plaintext);
for (let i = 0; i < dataView.length; i++) { for (let i = 0; i < dataView.length; i++) {
assert.strictEqual( assert.strictEqual(
decryptedView[i], decryptedView[i],
dataView[i], dataView[i],
`Byte at position ${i} doesn't match` `Byte at position ${i} doesn't match`
); );
} }
})); }).then(common.mustCall());
}));
} }
// Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer // Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer
@ -122,32 +134,31 @@ const { isArrayBuffer } = require('internal/util/types');
name: 'AES-GCM', name: 'AES-GCM',
length: 256 length: 256
}, true, ['encrypt', 'decrypt']) }, true, ['encrypt', 'decrypt'])
.then(common.mustCall((key) => { .then(async (key) => {
// Create an initialization vector // Create an initialization vector
const iv = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt using the TypedArray view of cross-realm ArrayBuffer // Encrypt using the TypedArray view of cross-realm ArrayBuffer
return subtle.encrypt( const ciphertext = await subtle.encrypt(
{ name: 'AES-GCM', iv }, { name: 'AES-GCM', iv },
key, key,
dataView dataView
).then((ciphertext) => { );
// Decrypt // Decrypt
return subtle.decrypt( const plaintext = await subtle.decrypt(
{ name: 'AES-GCM', iv }, { name: 'AES-GCM', iv },
key, key,
ciphertext ciphertext
);
// Verify the decrypted content matches original
const decryptedView = new Uint8Array(plaintext);
for (let i = 0; i < dataView.length; i++) {
assert.strictEqual(
decryptedView[i],
dataView[i],
`Byte at position ${i} doesn't match`
); );
}).then(common.mustCall((plaintext) => { }
// Verify the decrypted content matches original }).then(common.mustCall());
const decryptedView = new Uint8Array(plaintext);
for (let i = 0; i < dataView.length; i++) {
assert.strictEqual(
decryptedView[i],
dataView[i],
`Byte at position ${i} doesn't match`
);
}
}));
}));
} }