Skip to content

Commit 941520a

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 adds a fallback check using Object.prototype.toString to detect cross-realm ArrayBuffer instances when the prototype chain check fails. This ensures compatibility with TypedArray.buffer across JavaScript realms while maintaining the performance of the existing check for same-realm objects. See storacha/w3up#1591 for more details.
1 parent be79f4a commit 941520a

File tree

2 files changed

+169
-1
lines changed

2 files changed

+169
-1
lines changed

lib/internal/crypto/webidl.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
Number,
2121
NumberIsFinite,
2222
ObjectPrototypeIsPrototypeOf,
23+
ObjectPrototypeToString,
2324
SafeArrayIterator,
2425
String,
2526
TypedArrayPrototypeGetBuffer,
@@ -165,7 +166,16 @@ converters.object = (V, opts) => {
165166
};
166167

167168
function isNonSharedArrayBuffer(V) {
168-
return ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V);
169+
if (ObjectPrototypeIsPrototypeOf.call(ArrayBufferPrototype, V)) {
170+
return true;
171+
}
172+
173+
// This a fallback mechanism that handles cross-realm ArrayBuffers.
174+
return (
175+
typeof V === 'object' &&
176+
V !== null &&
177+
ObjectPrototypeToString.call(V) === '[object ArrayBuffer]'
178+
);
169179
}
170180

171181
function isSharedArrayBuffer(V) {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
11+
// Test with same-realm ArrayBuffer
12+
{
13+
const samerealmData = new Uint8Array([1, 2, 3, 4]).buffer;
14+
15+
subtle.digest('SHA-256', samerealmData)
16+
.then((result) => {
17+
assert(result instanceof ArrayBuffer);
18+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
19+
})
20+
.catch(common.mustNotCall());
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((result) => {
38+
assert(result instanceof ArrayBuffer);
39+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
40+
})
41+
.catch(common.mustNotCall('Should not reject cross-realm ArrayBuffer'));
42+
}
43+
44+
// Test with both TypedArray buffer methods
45+
{
46+
const context = vm.createContext({});
47+
const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context);
48+
49+
// Test the .buffer property (this was failing before)
50+
subtle.digest('SHA-256', crossrealmUint8Array.buffer)
51+
.then((result) => {
52+
assert(result instanceof ArrayBuffer);
53+
assert.strictEqual(result.byteLength, 32);
54+
})
55+
.catch(common.mustNotCall('Should not reject TypedArray.buffer'));
56+
57+
// Test passing the TypedArray directly (should work both before and after the fix)
58+
subtle.digest('SHA-256', crossrealmUint8Array)
59+
.then((result) => {
60+
assert(result instanceof ArrayBuffer);
61+
assert.strictEqual(result.byteLength, 32);
62+
})
63+
.catch(common.mustNotCall('Should not reject TypedArray'));
64+
}
65+
66+
// Test with AES-GCM encryption/decryption using cross-realm ArrayBuffer
67+
{
68+
const context = vm.createContext({});
69+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
70+
71+
// Fill the buffer with some data
72+
const dataView = new Uint8Array(crossRealmBuffer);
73+
for (let i = 0; i < dataView.length; i++) {
74+
dataView[i] = i % 256;
75+
}
76+
77+
// Generate a key
78+
subtle.generateKey({
79+
name: 'AES-GCM',
80+
length: 256
81+
}, true, ['encrypt', 'decrypt'])
82+
.then((key) => {
83+
// Create an initialization vector
84+
const iv = crypto.getRandomValues(new Uint8Array(12));
85+
86+
// Encrypt using the cross-realm ArrayBuffer
87+
return subtle.encrypt(
88+
{ name: 'AES-GCM', iv },
89+
key,
90+
crossRealmBuffer
91+
).then((ciphertext) => {
92+
// Decrypt
93+
return subtle.decrypt(
94+
{ name: 'AES-GCM', iv },
95+
key,
96+
ciphertext
97+
);
98+
}).then((plaintext) => {
99+
// Verify the decrypted content matches original
100+
const decryptedView = new Uint8Array(plaintext);
101+
for (let i = 0; i < dataView.length; i++) {
102+
assert.strictEqual(
103+
decryptedView[i],
104+
dataView[i],
105+
`Byte at position ${i} doesn't match`
106+
);
107+
}
108+
});
109+
})
110+
.catch(common.mustNotCall('Should not reject AES-GCM with cross-realm ArrayBuffer'));
111+
}
112+
113+
// Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer
114+
{
115+
const context = vm.createContext({});
116+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
117+
118+
// Fill the buffer with some data
119+
const dataView = new Uint8Array(crossRealmBuffer);
120+
for (let i = 0; i < dataView.length; i++) {
121+
dataView[i] = i % 256;
122+
}
123+
124+
// Generate a key
125+
subtle.generateKey({
126+
name: 'AES-GCM',
127+
length: 256
128+
}, true, ['encrypt', 'decrypt'])
129+
.then((key) => {
130+
// Create an initialization vector
131+
const iv = crypto.getRandomValues(new Uint8Array(12));
132+
133+
// Encrypt using the TypedArray view of cross-realm ArrayBuffer
134+
return subtle.encrypt(
135+
{ name: 'AES-GCM', iv },
136+
key,
137+
dataView
138+
).then((ciphertext) => {
139+
// Decrypt
140+
return subtle.decrypt(
141+
{ name: 'AES-GCM', iv },
142+
key,
143+
ciphertext
144+
);
145+
}).then((plaintext) => {
146+
// Verify the decrypted content matches original
147+
const decryptedView = new Uint8Array(plaintext);
148+
for (let i = 0; i < dataView.length; i++) {
149+
assert.strictEqual(
150+
decryptedView[i],
151+
dataView[i],
152+
`Byte at position ${i} doesn't match`
153+
);
154+
}
155+
});
156+
})
157+
.catch(common.mustNotCall('Should not reject AES-GCM with TypedArray view'));
158+
}

0 commit comments

Comments
 (0)