Skip to content

Commit 1436758

Browse files
fforbeckRafaelGSS
authored andcommitted
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: #57828 Refs: storacha/w3up#1591 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jordan Harband <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 8ab165f commit 1436758

File tree

2 files changed

+155
-4
lines changed

2 files changed

+155
-4
lines changed

lib/internal/crypto/webidl.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
const {
1414
ArrayBufferIsView,
15-
ArrayBufferPrototype,
1615
ArrayPrototypeIncludes,
1716
ArrayPrototypePush,
1817
ArrayPrototypeSort,
@@ -48,6 +47,7 @@ const {
4847
validateMaxBufferLength,
4948
kNamedCurveAliases,
5049
} = require('internal/crypto/util');
50+
const { isArrayBuffer } = require('internal/util/types');
5151

5252
// https://tc39.es/ecma262/#sec-tonumber
5353
function toNumber(value, opts = kEmptyObject) {
@@ -193,9 +193,7 @@ converters.object = (V, opts) => {
193193
return V;
194194
};
195195

196-
function isNonSharedArrayBuffer(V) {
197-
return ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V);
198-
}
196+
const isNonSharedArrayBuffer = isArrayBuffer;
199197

200198
function isSharedArrayBuffer(V) {
201199
// SharedArrayBuffers can be disabled with --no-harmony-sharedarraybuffer.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const { subtle } = globalThis.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)