Skip to content

Commit 982d482

Browse files
committed
crypto: fix cross-realm ArrayBuffer validation in WebCrypto
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. This ensures compatibility with TypedArray.buffer across JavaScript realms. See storacha/w3up#1591 for more details.
1 parent 958fd91 commit 982d482

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

lib/internal/crypto/webidl.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const {
4848
validateMaxBufferLength,
4949
kNamedCurveAliases,
5050
} = require('internal/crypto/util');
51+
const { isArrayBuffer } = require('internal/util/types');
5152

5253
// https://tc39.es/ecma262/#sec-tonumber
5354
function toNumber(value, opts = kEmptyObject) {
@@ -194,7 +195,7 @@ converters.object = (V, opts) => {
194195
};
195196

196197
function isNonSharedArrayBuffer(V) {
197-
return ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V);
198+
return isArrayBuffer(V);
198199
}
199200

200201
function isSharedArrayBuffer(V) {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const { subtle } = crypto;
9+
const vm = require('vm');
10+
const { isArrayBuffer } = require('internal/util/types');
11+
12+
// Test with same-realm ArrayBuffer
13+
{
14+
const samerealmData = new Uint8Array([1, 2, 3, 4]).buffer;
15+
16+
subtle.digest('SHA-256', samerealmData)
17+
.then(common.mustCall((result) => {
18+
assert(isArrayBuffer(result));
19+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
20+
}));
21+
}
22+
23+
// Test with cross-realm ArrayBuffer
24+
{
25+
const context = vm.createContext({});
26+
const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context);
27+
const crossrealmBuffer = crossrealmUint8Array.buffer;
28+
29+
// Verify it's truly cross-realm
30+
assert.notStrictEqual(
31+
Object.getPrototypeOf(crossrealmBuffer),
32+
ArrayBuffer.prototype
33+
);
34+
35+
// This should still work, since we're checking structural type
36+
subtle.digest('SHA-256', crossrealmBuffer)
37+
.then(common.mustCall((result) => {
38+
assert(isArrayBuffer(result));
39+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
40+
}));
41+
}
42+
43+
// Test with both TypedArray buffer methods
44+
{
45+
const context = vm.createContext({});
46+
const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context);
47+
48+
// Test the .buffer property
49+
subtle.digest('SHA-256', crossrealmUint8Array.buffer)
50+
.then(common.mustCall((result) => {
51+
assert(isArrayBuffer(result));
52+
assert.strictEqual(result.byteLength, 32);
53+
}));
54+
55+
// Test passing the TypedArray directly (should work both before and after the fix)
56+
subtle.digest('SHA-256', crossrealmUint8Array)
57+
.then(common.mustCall((result) => {
58+
assert(isArrayBuffer(result));
59+
assert.strictEqual(result.byteLength, 32);
60+
}));
61+
}
62+
63+
// Test with AES-GCM encryption/decryption using cross-realm ArrayBuffer
64+
{
65+
const context = vm.createContext({});
66+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
67+
68+
// Fill the buffer with some data
69+
const dataView = new Uint8Array(crossRealmBuffer);
70+
for (let i = 0; i < dataView.length; i++) {
71+
dataView[i] = i % 256;
72+
}
73+
74+
// Generate a key
75+
subtle.generateKey({
76+
name: 'AES-GCM',
77+
length: 256
78+
}, true, ['encrypt', 'decrypt'])
79+
.then(common.mustCall((key) => {
80+
// Create an initialization vector
81+
const iv = crypto.getRandomValues(new Uint8Array(12));
82+
83+
// Encrypt using the cross-realm ArrayBuffer
84+
return subtle.encrypt(
85+
{ name: 'AES-GCM', iv },
86+
key,
87+
crossRealmBuffer
88+
).then((ciphertext) => {
89+
// Decrypt
90+
return subtle.decrypt(
91+
{ name: 'AES-GCM', iv },
92+
key,
93+
ciphertext
94+
);
95+
}).then(common.mustCall((plaintext) => {
96+
// Verify the decrypted content matches original
97+
const decryptedView = new Uint8Array(plaintext);
98+
for (let i = 0; i < dataView.length; i++) {
99+
assert.strictEqual(
100+
decryptedView[i],
101+
dataView[i],
102+
`Byte at position ${i} doesn't match`
103+
);
104+
}
105+
}));
106+
}));
107+
}
108+
109+
// Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer
110+
{
111+
const context = vm.createContext({});
112+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
113+
114+
// Fill the buffer with some data
115+
const dataView = new Uint8Array(crossRealmBuffer);
116+
for (let i = 0; i < dataView.length; i++) {
117+
dataView[i] = i % 256;
118+
}
119+
120+
// Generate a key
121+
subtle.generateKey({
122+
name: 'AES-GCM',
123+
length: 256
124+
}, true, ['encrypt', 'decrypt'])
125+
.then(common.mustCall((key) => {
126+
// Create an initialization vector
127+
const iv = crypto.getRandomValues(new Uint8Array(12));
128+
129+
// Encrypt using the TypedArray view of cross-realm ArrayBuffer
130+
return subtle.encrypt(
131+
{ name: 'AES-GCM', iv },
132+
key,
133+
dataView
134+
).then((ciphertext) => {
135+
// Decrypt
136+
return subtle.decrypt(
137+
{ name: 'AES-GCM', iv },
138+
key,
139+
ciphertext
140+
);
141+
}).then(common.mustCall((plaintext) => {
142+
// Verify the decrypted content matches original
143+
const decryptedView = new Uint8Array(plaintext);
144+
for (let i = 0; i < dataView.length; i++) {
145+
assert.strictEqual(
146+
decryptedView[i],
147+
dataView[i],
148+
`Byte at position ${i} doesn't match`
149+
);
150+
}
151+
}));
152+
}));
153+
}

0 commit comments

Comments
 (0)