From 6bf7fd7f4ecbf74e38e4e02ffbeb452de068921f Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Tue, 22 Apr 2025 10:01:21 -0300 Subject: [PATCH] crypto: fix cross-realm check of `ArrayBuffer` This patch modifies the `isNonSharedArrayBuffer` function in the WebIDL implementation for the SubtleCrypto API to properly handle `ArrayBuffer` instances created in different JavaScript realms. Before this fix, when a `TypedArray.buffer` from a different realm (e.g., from a VM context or worker thread) was passed to `SubtleCrypto.digest()`, it would fail with: > TypeError: Failed to execute 'digest' on 'SubtleCrypto': 2nd argument > is not instance of ArrayBuffer, Buffer, TypedArray, or DataView." The fix use the `isArrayBuffer` function from `internal/util/types` to detect cross-realm `ArrayBuffer` instances when the prototype chain check fails. PR-URL: https://github.com/nodejs/node/pull/57828 Refs: https://github.com/storacha/w3up/issues/1591 Reviewed-By: James M Snell Reviewed-By: Jordan Harband Reviewed-By: Antoine du Hamel --- lib/internal/crypto/webidl.js | 6 +- .../test-crypto-subtle-cross-realm.js | 153 ++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-crypto-subtle-cross-realm.js diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index f557f81cab0..f7c3aed3d95 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -12,7 +12,6 @@ const { ArrayBufferIsView, - ArrayBufferPrototype, ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypeSort, @@ -48,6 +47,7 @@ const { validateMaxBufferLength, kNamedCurveAliases, } = require('internal/crypto/util'); +const { isArrayBuffer } = require('internal/util/types'); // https://tc39.es/ecma262/#sec-tonumber function toNumber(value, opts = kEmptyObject) { @@ -193,9 +193,7 @@ converters.object = (V, opts) => { return V; }; -function isNonSharedArrayBuffer(V) { - return ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V); -} +const isNonSharedArrayBuffer = isArrayBuffer; function isSharedArrayBuffer(V) { // SharedArrayBuffers can be disabled with --no-harmony-sharedarraybuffer. diff --git a/test/parallel/test-crypto-subtle-cross-realm.js b/test/parallel/test-crypto-subtle-cross-realm.js new file mode 100644 index 00000000000..6261670d5aa --- /dev/null +++ b/test/parallel/test-crypto-subtle-cross-realm.js @@ -0,0 +1,153 @@ +'use strict'; +// Flags: --expose-internals +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const vm = require('vm'); +const { isArrayBuffer } = require('internal/util/types'); + +// Test with same-realm ArrayBuffer +{ + const samerealmData = new Uint8Array([1, 2, 3, 4]).buffer; + + subtle.digest('SHA-256', samerealmData) + .then(common.mustCall((result) => { + assert(isArrayBuffer(result)); + assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes + })); +} + +// Test with cross-realm ArrayBuffer +{ + const context = vm.createContext({}); + const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context); + const crossrealmBuffer = crossrealmUint8Array.buffer; + + // Verify it's truly cross-realm + assert.notStrictEqual( + Object.getPrototypeOf(crossrealmBuffer), + ArrayBuffer.prototype + ); + + // This should still work, since we're checking structural type + subtle.digest('SHA-256', crossrealmBuffer) + .then(common.mustCall((result) => { + assert(isArrayBuffer(result)); + assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes + })); +} + +// Test with both TypedArray buffer methods +{ + const context = vm.createContext({}); + const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context); + + // Test the .buffer property + subtle.digest('SHA-256', crossrealmUint8Array.buffer) + .then(common.mustCall((result) => { + assert(isArrayBuffer(result)); + assert.strictEqual(result.byteLength, 32); + })); + + // Test passing the TypedArray directly (should work both before and after the fix) + subtle.digest('SHA-256', crossrealmUint8Array) + .then(common.mustCall((result) => { + assert(isArrayBuffer(result)); + assert.strictEqual(result.byteLength, 32); + })); +} + +// Test with AES-GCM encryption/decryption using cross-realm ArrayBuffer +{ + const context = vm.createContext({}); + const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context); + + // Fill the buffer with some data + const dataView = new Uint8Array(crossRealmBuffer); + for (let i = 0; i < dataView.length; i++) { + dataView[i] = i % 256; + } + + // Generate a key + subtle.generateKey({ + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']) + .then(common.mustCall((key) => { + // Create an initialization vector + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Encrypt using the cross-realm ArrayBuffer + return subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + crossRealmBuffer + ).then((ciphertext) => { + // Decrypt + return subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + ciphertext + ); + }).then(common.mustCall((plaintext) => { + // 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` + ); + } + })); + })); +} + +// Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer +{ + const context = vm.createContext({}); + const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context); + + // Fill the buffer with some data + const dataView = new Uint8Array(crossRealmBuffer); + for (let i = 0; i < dataView.length; i++) { + dataView[i] = i % 256; + } + + // Generate a key + subtle.generateKey({ + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']) + .then(common.mustCall((key) => { + // Create an initialization vector + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Encrypt using the TypedArray view of cross-realm ArrayBuffer + return subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + dataView + ).then((ciphertext) => { + // Decrypt + return subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + ciphertext + ); + }).then(common.mustCall((plaintext) => { + // 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` + ); + } + })); + })); +}