ai-evaluate 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -3
- package/dist/capnweb-bundle.d.ts +10 -0
- package/dist/capnweb-bundle.d.ts.map +1 -0
- package/dist/capnweb-bundle.js +2596 -0
- package/dist/capnweb-bundle.js.map +1 -0
- package/dist/evaluate.d.ts +1 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +186 -7
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/miniflare-pool.d.ts +109 -0
- package/dist/miniflare-pool.d.ts.map +1 -0
- package/dist/miniflare-pool.js +308 -0
- package/dist/miniflare-pool.js.map +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +42 -10
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +66 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +169 -0
- package/dist/shared.js.map +1 -0
- package/dist/type-guards.d.ts +21 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +216 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +26 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-template/code-transforms.d.ts +9 -0
- package/dist/worker-template/code-transforms.d.ts.map +1 -0
- package/dist/worker-template/code-transforms.js +28 -0
- package/dist/worker-template/code-transforms.js.map +1 -0
- package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
- package/dist/worker-template/core.d.ts.map +1 -0
- package/dist/worker-template/core.js +502 -0
- package/dist/worker-template/core.js.map +1 -0
- package/dist/worker-template/helpers.d.ts +14 -0
- package/dist/worker-template/helpers.d.ts.map +1 -0
- package/dist/worker-template/helpers.js +79 -0
- package/dist/worker-template/helpers.js.map +1 -0
- package/dist/worker-template/index.d.ts +14 -0
- package/dist/worker-template/index.d.ts.map +1 -0
- package/dist/worker-template/index.js +19 -0
- package/dist/worker-template/index.js.map +1 -0
- package/dist/worker-template/sdk-generator.d.ts +17 -0
- package/dist/worker-template/sdk-generator.d.ts.map +1 -0
- package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
- package/dist/worker-template/sdk-generator.js.map +1 -0
- package/dist/worker-template/test-generator.d.ts +16 -0
- package/dist/worker-template/test-generator.d.ts.map +1 -0
- package/dist/worker-template/test-generator.js +357 -0
- package/dist/worker-template/test-generator.js.map +1 -0
- package/dist/worker-template.d.ts +2 -2
- package/dist/worker-template.d.ts.map +1 -1
- package/dist/worker-template.js +64 -31
- package/dist/worker-template.js.map +1 -1
- package/example/package.json +7 -3
- package/example/src/index.ts +194 -40
- package/example/wrangler.jsonc +18 -2
- package/package.json +1 -3
- package/src/capnweb-bundle.ts +2596 -0
- package/src/evaluate.ts +216 -7
- package/src/index.ts +3 -1
- package/src/miniflare-pool.ts +395 -0
- package/src/node.ts +56 -11
- package/src/shared.ts +186 -0
- package/src/type-guards.ts +323 -0
- package/src/types.ts +18 -2
- package/src/validation.ts +120 -0
- package/src/worker-template/code-transforms.ts +32 -0
- package/src/worker-template/core.ts +557 -0
- package/src/worker-template/helpers.ts +90 -0
- package/src/worker-template/index.ts +23 -0
- package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
- package/src/worker-template/test-generator.ts +358 -0
- package/test/miniflare-pool.test.ts +246 -0
- package/test/node.test.ts +467 -0
- package/test/security.test.ts +1009 -0
- package/test/shared.test.ts +105 -0
- package/test/type-guards.test.ts +303 -0
- package/test/validation.test.ts +240 -0
- package/test/worker-template.test.ts +21 -19
- package/src/evaluate.js +0 -187
- package/src/index.js +0 -10
- package/src/node.d.ts +0 -17
- package/src/node.d.ts.map +0 -1
- package/src/node.js +0 -168
- package/src/node.js.map +0 -1
- package/src/types.d.ts +0 -172
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -4
- package/src/types.js.map +0 -1
- package/src/worker-template.d.ts.map +0 -1
- package/src/worker-template.js.map +0 -1
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security tests for ai-evaluate sandbox
|
|
3
|
+
*
|
|
4
|
+
* Tests various sandbox escape attempts, resource exhaustion,
|
|
5
|
+
* network isolation, code injection, and environment isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
import { evaluate } from '../src/node.js'
|
|
9
|
+
|
|
10
|
+
describe('security', () => {
|
|
11
|
+
describe('sandbox escape attempts', () => {
|
|
12
|
+
describe('prototype pollution', () => {
|
|
13
|
+
it('blocks Object.prototype pollution from affecting host', async () => {
|
|
14
|
+
const result = await evaluate({
|
|
15
|
+
script: `
|
|
16
|
+
Object.prototype.polluted = true;
|
|
17
|
+
return ({}).polluted;
|
|
18
|
+
`,
|
|
19
|
+
})
|
|
20
|
+
// The main environment should be unaffected by sandbox prototype pollution
|
|
21
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('blocks Array.prototype pollution from affecting host', async () => {
|
|
25
|
+
const result = await evaluate({
|
|
26
|
+
script: `
|
|
27
|
+
Array.prototype.polluted = true;
|
|
28
|
+
return [].polluted;
|
|
29
|
+
`,
|
|
30
|
+
})
|
|
31
|
+
expect(([] as unknown as Record<string, unknown>).polluted).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('blocks Function.prototype pollution from affecting host', async () => {
|
|
35
|
+
const result = await evaluate({
|
|
36
|
+
script: `
|
|
37
|
+
Function.prototype.polluted = true;
|
|
38
|
+
return (function(){}).polluted;
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
expect((function () {} as unknown as Record<string, unknown>).polluted).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('global scope access', () => {
|
|
46
|
+
it('blocks access to globalThis.process', async () => {
|
|
47
|
+
const result = await evaluate({
|
|
48
|
+
script: `
|
|
49
|
+
if (typeof globalThis.process !== 'undefined') {
|
|
50
|
+
return { hasProcess: true, env: globalThis.process.env };
|
|
51
|
+
}
|
|
52
|
+
return { hasProcess: false };
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
// Sandbox should not have access to Node.js process
|
|
56
|
+
if (result.success) {
|
|
57
|
+
expect(result.value).toEqual({ hasProcess: false })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('blocks access to global.require', async () => {
|
|
62
|
+
const result = await evaluate({
|
|
63
|
+
script: `
|
|
64
|
+
if (typeof global !== 'undefined' && typeof global.require === 'function') {
|
|
65
|
+
return { hasRequire: true };
|
|
66
|
+
}
|
|
67
|
+
if (typeof require === 'function') {
|
|
68
|
+
return { hasRequire: true };
|
|
69
|
+
}
|
|
70
|
+
return { hasRequire: false };
|
|
71
|
+
`,
|
|
72
|
+
})
|
|
73
|
+
if (result.success) {
|
|
74
|
+
expect(result.value).toEqual({ hasRequire: false })
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('blocks access to __dirname and __filename', async () => {
|
|
79
|
+
const result = await evaluate({
|
|
80
|
+
script: `
|
|
81
|
+
return {
|
|
82
|
+
hasDirname: typeof __dirname !== 'undefined',
|
|
83
|
+
hasFilename: typeof __filename !== 'undefined'
|
|
84
|
+
};
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
if (result.success) {
|
|
88
|
+
expect(result.value).toEqual({ hasDirname: false, hasFilename: false })
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('constructor access', () => {
|
|
94
|
+
it('blocks constructor-based global access to process', async () => {
|
|
95
|
+
const result = await evaluate({
|
|
96
|
+
script: `
|
|
97
|
+
try {
|
|
98
|
+
const global = ({}).constructor.constructor('return this')();
|
|
99
|
+
if (global.process) {
|
|
100
|
+
return { escaped: true, hasProcess: true };
|
|
101
|
+
}
|
|
102
|
+
return { escaped: true, hasProcess: false };
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { escaped: false, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
})
|
|
108
|
+
// Either the access is blocked or it doesn't have process
|
|
109
|
+
if (result.success && typeof result.value === 'object' && result.value !== null) {
|
|
110
|
+
const value = result.value as Record<string, unknown>
|
|
111
|
+
if (value.escaped) {
|
|
112
|
+
expect(value.hasProcess).toBe(false)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('blocks Function constructor escape to process', async () => {
|
|
118
|
+
const result = await evaluate({
|
|
119
|
+
script: `
|
|
120
|
+
try {
|
|
121
|
+
const fn = new Function('return this.process');
|
|
122
|
+
const proc = fn();
|
|
123
|
+
return { hasProcess: !!proc };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { blocked: true, error: e.message };
|
|
126
|
+
}
|
|
127
|
+
`,
|
|
128
|
+
})
|
|
129
|
+
if (result.success && typeof result.value === 'object' && result.value !== null) {
|
|
130
|
+
const value = result.value as Record<string, unknown>
|
|
131
|
+
if (!value.blocked) {
|
|
132
|
+
expect(value.hasProcess).toBe(false)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('blocks eval-based escape attempts', async () => {
|
|
138
|
+
const result = await evaluate({
|
|
139
|
+
script: `
|
|
140
|
+
try {
|
|
141
|
+
const proc = eval('this.process || globalThis.process');
|
|
142
|
+
return { hasProcess: !!proc };
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return { blocked: true, error: e.message };
|
|
145
|
+
}
|
|
146
|
+
`,
|
|
147
|
+
})
|
|
148
|
+
if (result.success && typeof result.value === 'object' && result.value !== null) {
|
|
149
|
+
const value = result.value as Record<string, unknown>
|
|
150
|
+
if (!value.blocked) {
|
|
151
|
+
expect(value.hasProcess).toBe(false)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('resource exhaustion', () => {
|
|
159
|
+
describe('infinite loops with timeout', () => {
|
|
160
|
+
// Note: Synchronous infinite loops are hard to interrupt in JavaScript.
|
|
161
|
+
// The sandbox relies on workerd/Miniflare CPU time limits.
|
|
162
|
+
// These tests are skipped by default to avoid hanging CI.
|
|
163
|
+
|
|
164
|
+
it.skip('terminates infinite while loop (manual test)', async () => {
|
|
165
|
+
const result = await evaluate({
|
|
166
|
+
script: 'while(true){}',
|
|
167
|
+
timeout: 2000,
|
|
168
|
+
})
|
|
169
|
+
expect(result.success).toBe(false)
|
|
170
|
+
expect(result.error).toBeDefined()
|
|
171
|
+
}, 30000)
|
|
172
|
+
|
|
173
|
+
it.skip('terminates infinite for loop (manual test)', async () => {
|
|
174
|
+
const result = await evaluate({
|
|
175
|
+
script: 'for(;;){}',
|
|
176
|
+
timeout: 2000,
|
|
177
|
+
})
|
|
178
|
+
expect(result.success).toBe(false)
|
|
179
|
+
expect(result.error).toBeDefined()
|
|
180
|
+
}, 30000)
|
|
181
|
+
|
|
182
|
+
it.skip('terminates busy loop (manual test)', async () => {
|
|
183
|
+
const result = await evaluate({
|
|
184
|
+
script: `
|
|
185
|
+
let i = 0;
|
|
186
|
+
while(true) { i++; }
|
|
187
|
+
return i;
|
|
188
|
+
`,
|
|
189
|
+
timeout: 2000,
|
|
190
|
+
})
|
|
191
|
+
expect(result.success).toBe(false)
|
|
192
|
+
}, 30000)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('memory bombs', () => {
|
|
196
|
+
// Note: Memory bomb tests can crash the test runner.
|
|
197
|
+
// These are skipped by default to avoid CI issues.
|
|
198
|
+
|
|
199
|
+
it.skip('handles large array allocation attempt (manual test)', async () => {
|
|
200
|
+
const result = await evaluate({
|
|
201
|
+
script: `
|
|
202
|
+
try {
|
|
203
|
+
const arr = new Array(1e9).fill('x'.repeat(1000));
|
|
204
|
+
return { allocated: true };
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return { blocked: true, error: e.message };
|
|
207
|
+
}
|
|
208
|
+
`,
|
|
209
|
+
timeout: 5000,
|
|
210
|
+
})
|
|
211
|
+
if (result.success && typeof result.value === 'object') {
|
|
212
|
+
const value = result.value as Record<string, unknown>
|
|
213
|
+
expect(value.blocked).toBe(true)
|
|
214
|
+
}
|
|
215
|
+
}, 15000)
|
|
216
|
+
|
|
217
|
+
it('handles moderate string expansion', async () => {
|
|
218
|
+
const result = await evaluate({
|
|
219
|
+
script: `
|
|
220
|
+
try {
|
|
221
|
+
let s = 'x';
|
|
222
|
+
// Limited to 20 iterations (~1MB) to avoid crashing
|
|
223
|
+
for (let i = 0; i < 20; i++) {
|
|
224
|
+
s = s + s; // exponential growth
|
|
225
|
+
}
|
|
226
|
+
return { length: s.length };
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return { blocked: true, error: e.message };
|
|
229
|
+
}
|
|
230
|
+
`,
|
|
231
|
+
timeout: 5000,
|
|
232
|
+
})
|
|
233
|
+
expect(result).toBeDefined()
|
|
234
|
+
if (result.success && typeof result.value === 'object') {
|
|
235
|
+
const value = result.value as Record<string, unknown>
|
|
236
|
+
// 2^20 = ~1 million characters
|
|
237
|
+
expect(value.length).toBe(1048576)
|
|
238
|
+
}
|
|
239
|
+
}, 15000)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('stack overflow', () => {
|
|
243
|
+
it('handles recursive function', async () => {
|
|
244
|
+
const result = await evaluate({
|
|
245
|
+
script: `
|
|
246
|
+
function recurse() { return recurse(); }
|
|
247
|
+
try {
|
|
248
|
+
recurse();
|
|
249
|
+
return { completed: true };
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { overflow: true, error: e.message };
|
|
252
|
+
}
|
|
253
|
+
`,
|
|
254
|
+
timeout: 5000,
|
|
255
|
+
})
|
|
256
|
+
if (result.success && typeof result.value === 'object') {
|
|
257
|
+
const value = result.value as Record<string, unknown>
|
|
258
|
+
// Should catch the stack overflow
|
|
259
|
+
expect(value.overflow).toBe(true)
|
|
260
|
+
}
|
|
261
|
+
}, 15000)
|
|
262
|
+
|
|
263
|
+
it('handles mutual recursion', async () => {
|
|
264
|
+
const result = await evaluate({
|
|
265
|
+
script: `
|
|
266
|
+
function a() { return b(); }
|
|
267
|
+
function b() { return a(); }
|
|
268
|
+
try {
|
|
269
|
+
a();
|
|
270
|
+
return { completed: true };
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return { overflow: true, error: e.message };
|
|
273
|
+
}
|
|
274
|
+
`,
|
|
275
|
+
timeout: 5000,
|
|
276
|
+
})
|
|
277
|
+
if (result.success && typeof result.value === 'object') {
|
|
278
|
+
const value = result.value as Record<string, unknown>
|
|
279
|
+
expect(value.overflow).toBe(true)
|
|
280
|
+
}
|
|
281
|
+
}, 15000)
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('network isolation', () => {
|
|
286
|
+
describe('fetch blocking when fetch: null', () => {
|
|
287
|
+
// Note: The `fetch: null` option should block network access.
|
|
288
|
+
// However, the actual blocking depends on the implementation.
|
|
289
|
+
// These tests verify the expected behavior.
|
|
290
|
+
|
|
291
|
+
it('verifies fetch behavior with network blocked', async () => {
|
|
292
|
+
const result = await evaluate({
|
|
293
|
+
script: `
|
|
294
|
+
return (async () => {
|
|
295
|
+
// Check what happens when we try to fetch
|
|
296
|
+
try {
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
setTimeout(() => controller.abort(), 500);
|
|
299
|
+
await fetch('https://httpstat.us/200', { signal: controller.signal });
|
|
300
|
+
return { fetchAllowed: true, networkBlocked: false };
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// Aborted or blocked
|
|
303
|
+
return { fetchAllowed: true, networkBlocked: true, error: e.message };
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
`,
|
|
307
|
+
fetch: null,
|
|
308
|
+
timeout: 3000,
|
|
309
|
+
})
|
|
310
|
+
// Test completes (doesn't hang)
|
|
311
|
+
expect(result).toBeDefined()
|
|
312
|
+
}, 10000)
|
|
313
|
+
|
|
314
|
+
it('handles localhost access with fetch: null', async () => {
|
|
315
|
+
const result = await evaluate({
|
|
316
|
+
script: `
|
|
317
|
+
return (async () => {
|
|
318
|
+
try {
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
setTimeout(() => controller.abort(), 100);
|
|
321
|
+
await fetch('http://127.0.0.1:8080', { signal: controller.signal });
|
|
322
|
+
return { fetched: true };
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return { blocked: true, error: e.message };
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
`,
|
|
328
|
+
fetch: null,
|
|
329
|
+
timeout: 2000,
|
|
330
|
+
})
|
|
331
|
+
// The request should either succeed (connection refused quickly),
|
|
332
|
+
// be blocked, or timeout/abort. All are valid behaviors.
|
|
333
|
+
// This test just verifies the sandbox doesn't hang on localhost requests.
|
|
334
|
+
expect(result).toBeDefined()
|
|
335
|
+
}, 5000)
|
|
336
|
+
|
|
337
|
+
// Skip private network tests as they can cause long timeouts
|
|
338
|
+
it.skip('blocks private network access (10.x.x.x)', async () => {
|
|
339
|
+
const result = await evaluate({
|
|
340
|
+
script: `
|
|
341
|
+
return (async () => {
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch('http://10.0.0.1:8080');
|
|
344
|
+
return { fetched: true };
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return { blocked: true, error: e.message };
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
`,
|
|
350
|
+
fetch: null,
|
|
351
|
+
timeout: 5000,
|
|
352
|
+
})
|
|
353
|
+
if (result.success && typeof result.value === 'object') {
|
|
354
|
+
const value = result.value as Record<string, unknown>
|
|
355
|
+
expect(value.blocked).toBe(true)
|
|
356
|
+
}
|
|
357
|
+
}, 10000)
|
|
358
|
+
|
|
359
|
+
it.skip('blocks private network access (192.168.x.x)', async () => {
|
|
360
|
+
const result = await evaluate({
|
|
361
|
+
script: `
|
|
362
|
+
return (async () => {
|
|
363
|
+
try {
|
|
364
|
+
const response = await fetch('http://192.168.1.1:8080');
|
|
365
|
+
return { fetched: true };
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return { blocked: true, error: e.message };
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
`,
|
|
371
|
+
fetch: null,
|
|
372
|
+
timeout: 5000,
|
|
373
|
+
})
|
|
374
|
+
if (result.success && typeof result.value === 'object') {
|
|
375
|
+
const value = result.value as Record<string, unknown>
|
|
376
|
+
expect(value.blocked).toBe(true)
|
|
377
|
+
}
|
|
378
|
+
}, 10000)
|
|
379
|
+
|
|
380
|
+
it.skip('blocks private network access (172.16.x.x)', async () => {
|
|
381
|
+
const result = await evaluate({
|
|
382
|
+
script: `
|
|
383
|
+
return (async () => {
|
|
384
|
+
try {
|
|
385
|
+
const response = await fetch('http://172.16.0.1:8080');
|
|
386
|
+
return { fetched: true };
|
|
387
|
+
} catch (e) {
|
|
388
|
+
return { blocked: true, error: e.message };
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
`,
|
|
392
|
+
fetch: null,
|
|
393
|
+
timeout: 5000,
|
|
394
|
+
})
|
|
395
|
+
if (result.success && typeof result.value === 'object') {
|
|
396
|
+
const value = result.value as Record<string, unknown>
|
|
397
|
+
expect(value.blocked).toBe(true)
|
|
398
|
+
}
|
|
399
|
+
}, 10000)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
describe('fetch with FetchConfig mode: block', () => {
|
|
403
|
+
it('blocks fetch requests with mode: block', async () => {
|
|
404
|
+
const result = await evaluate({
|
|
405
|
+
script: `
|
|
406
|
+
return (async () => {
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetch('https://example.com');
|
|
409
|
+
return { fetched: true, status: response.status };
|
|
410
|
+
} catch (e) {
|
|
411
|
+
return { blocked: true, error: e.message };
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
`,
|
|
415
|
+
fetch: { mode: 'block' },
|
|
416
|
+
timeout: 5000,
|
|
417
|
+
})
|
|
418
|
+
if (result.success && typeof result.value === 'object') {
|
|
419
|
+
const value = result.value as Record<string, unknown>
|
|
420
|
+
expect(value.blocked).toBe(true)
|
|
421
|
+
} else {
|
|
422
|
+
expect(result.error).toBeDefined()
|
|
423
|
+
}
|
|
424
|
+
}, 10000)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('fetch allowlist mode', () => {
|
|
428
|
+
it('blocks non-matching domains', async () => {
|
|
429
|
+
const result = await evaluate({
|
|
430
|
+
script: `
|
|
431
|
+
return (async () => {
|
|
432
|
+
try {
|
|
433
|
+
const response = await fetch('https://blocked.com/api');
|
|
434
|
+
return { fetched: true, status: response.status };
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return { blocked: true, error: e.message };
|
|
437
|
+
}
|
|
438
|
+
})();
|
|
439
|
+
`,
|
|
440
|
+
fetch: { mode: 'allowlist', allowedDomains: ['api.example.com'] },
|
|
441
|
+
timeout: 5000,
|
|
442
|
+
})
|
|
443
|
+
if (result.success && typeof result.value === 'object') {
|
|
444
|
+
const value = result.value as Record<string, unknown>
|
|
445
|
+
expect(value.blocked).toBe(true)
|
|
446
|
+
expect(value.error).toContain('not in allowlist')
|
|
447
|
+
} else {
|
|
448
|
+
expect(result.error).toContain('not in allowlist')
|
|
449
|
+
}
|
|
450
|
+
}, 10000)
|
|
451
|
+
|
|
452
|
+
it('allows matching exact domains', async () => {
|
|
453
|
+
const result = await evaluate({
|
|
454
|
+
script: `
|
|
455
|
+
return (async () => {
|
|
456
|
+
try {
|
|
457
|
+
const response = await fetch('https://api.example.com/data');
|
|
458
|
+
return { fetched: true, blocked: false };
|
|
459
|
+
} catch (e) {
|
|
460
|
+
const isAllowlistError = e.message.includes('not in allowlist');
|
|
461
|
+
// blocked: false means it wasn't blocked by allowlist (even if network failed)
|
|
462
|
+
return { blocked: isAllowlistError, error: e.message };
|
|
463
|
+
}
|
|
464
|
+
})();
|
|
465
|
+
`,
|
|
466
|
+
fetch: { mode: 'allowlist', allowedDomains: ['api.example.com'] },
|
|
467
|
+
timeout: 5000,
|
|
468
|
+
})
|
|
469
|
+
if (result.success && typeof result.value === 'object') {
|
|
470
|
+
const value = result.value as Record<string, unknown>
|
|
471
|
+
// Should NOT be blocked by allowlist (might fail for other reasons like DNS)
|
|
472
|
+
expect(value.blocked).toBe(false)
|
|
473
|
+
}
|
|
474
|
+
}, 10000)
|
|
475
|
+
|
|
476
|
+
it('supports wildcard patterns', async () => {
|
|
477
|
+
const result = await evaluate({
|
|
478
|
+
script: `
|
|
479
|
+
return (async () => {
|
|
480
|
+
try {
|
|
481
|
+
const response = await fetch('https://other.com/api');
|
|
482
|
+
return { fetched: true };
|
|
483
|
+
} catch (e) {
|
|
484
|
+
const isAllowlistError = e.message.includes('not in allowlist');
|
|
485
|
+
return { blocked: isAllowlistError, error: e.message };
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
`,
|
|
489
|
+
fetch: { mode: 'allowlist', allowedDomains: ['*.example.com'] },
|
|
490
|
+
timeout: 5000,
|
|
491
|
+
})
|
|
492
|
+
if (result.success && typeof result.value === 'object') {
|
|
493
|
+
const value = result.value as Record<string, unknown>
|
|
494
|
+
expect(value.blocked).toBe(true)
|
|
495
|
+
expect(value.error).toContain('not in allowlist')
|
|
496
|
+
}
|
|
497
|
+
}, 10000)
|
|
498
|
+
|
|
499
|
+
it('wildcard matches subdomains', async () => {
|
|
500
|
+
const result = await evaluate({
|
|
501
|
+
script: `
|
|
502
|
+
return (async () => {
|
|
503
|
+
try {
|
|
504
|
+
const response = await fetch('https://api.example.com/data');
|
|
505
|
+
return { fetched: true, blocked: false };
|
|
506
|
+
} catch (e) {
|
|
507
|
+
const isAllowlistError = e.message.includes('not in allowlist');
|
|
508
|
+
return { blocked: isAllowlistError, error: e.message };
|
|
509
|
+
}
|
|
510
|
+
})();
|
|
511
|
+
`,
|
|
512
|
+
fetch: { mode: 'allowlist', allowedDomains: ['*.example.com'] },
|
|
513
|
+
timeout: 5000,
|
|
514
|
+
})
|
|
515
|
+
if (result.success && typeof result.value === 'object') {
|
|
516
|
+
const value = result.value as Record<string, unknown>
|
|
517
|
+
// Should NOT be blocked by allowlist
|
|
518
|
+
expect(value.blocked).toBe(false)
|
|
519
|
+
}
|
|
520
|
+
}, 10000)
|
|
521
|
+
|
|
522
|
+
it('blocks localhost when not in allowlist', async () => {
|
|
523
|
+
const result = await evaluate({
|
|
524
|
+
script: `
|
|
525
|
+
return (async () => {
|
|
526
|
+
try {
|
|
527
|
+
const response = await fetch('http://localhost:8080/api');
|
|
528
|
+
return { fetched: true };
|
|
529
|
+
} catch (e) {
|
|
530
|
+
const isAllowlistError = e.message.includes('not in allowlist');
|
|
531
|
+
return { blocked: isAllowlistError, error: e.message };
|
|
532
|
+
}
|
|
533
|
+
})();
|
|
534
|
+
`,
|
|
535
|
+
fetch: { mode: 'allowlist', allowedDomains: ['api.example.com'] },
|
|
536
|
+
timeout: 5000,
|
|
537
|
+
})
|
|
538
|
+
if (result.success && typeof result.value === 'object') {
|
|
539
|
+
const value = result.value as Record<string, unknown>
|
|
540
|
+
expect(value.blocked).toBe(true)
|
|
541
|
+
}
|
|
542
|
+
}, 10000)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('backwards compatibility', () => {
|
|
546
|
+
it('fetch: null still blocks all network (backwards compat)', async () => {
|
|
547
|
+
const result = await evaluate({
|
|
548
|
+
script: `
|
|
549
|
+
return (async () => {
|
|
550
|
+
try {
|
|
551
|
+
const response = await fetch('https://example.com');
|
|
552
|
+
return { fetched: true };
|
|
553
|
+
} catch (e) {
|
|
554
|
+
return { blocked: true, error: e.message };
|
|
555
|
+
}
|
|
556
|
+
})();
|
|
557
|
+
`,
|
|
558
|
+
fetch: null,
|
|
559
|
+
timeout: 5000,
|
|
560
|
+
})
|
|
561
|
+
if (result.success && typeof result.value === 'object') {
|
|
562
|
+
const value = result.value as Record<string, unknown>
|
|
563
|
+
expect(value.blocked).toBe(true)
|
|
564
|
+
}
|
|
565
|
+
}, 10000)
|
|
566
|
+
|
|
567
|
+
it('mode: allow explicitly allows all network', async () => {
|
|
568
|
+
const result = await evaluate({
|
|
569
|
+
script: `
|
|
570
|
+
return (async () => {
|
|
571
|
+
try {
|
|
572
|
+
const response = await fetch('https://httpstat.us/200');
|
|
573
|
+
return { fetched: true };
|
|
574
|
+
} catch (e) {
|
|
575
|
+
const isAllowlistError = e.message.includes('not in allowlist');
|
|
576
|
+
const isBlockedError = e.message.includes('Network access blocked');
|
|
577
|
+
return {
|
|
578
|
+
allowlistBlocked: isAllowlistError,
|
|
579
|
+
networkBlocked: isBlockedError
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
`,
|
|
584
|
+
fetch: { mode: 'allow' },
|
|
585
|
+
timeout: 5000,
|
|
586
|
+
})
|
|
587
|
+
if (result.success && typeof result.value === 'object') {
|
|
588
|
+
const value = result.value as Record<string, unknown>
|
|
589
|
+
expect(value.allowlistBlocked).toBe(false)
|
|
590
|
+
expect(value.networkBlocked).toBe(false)
|
|
591
|
+
}
|
|
592
|
+
}, 10000)
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
describe('code injection', () => {
|
|
597
|
+
describe('template literal injection', () => {
|
|
598
|
+
it('handles malicious template literal safely', async () => {
|
|
599
|
+
const result = await evaluate({
|
|
600
|
+
script: `
|
|
601
|
+
const userInput = '\${process.env.SECRET}';
|
|
602
|
+
const template = \`Value: \${userInput}\`;
|
|
603
|
+
return template;
|
|
604
|
+
`,
|
|
605
|
+
})
|
|
606
|
+
// Should return the literal string, not evaluate the nested template
|
|
607
|
+
if (result.success) {
|
|
608
|
+
expect(result.value).toBe('Value: ${process.env.SECRET}')
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('handles nested template injection', async () => {
|
|
613
|
+
const result = await evaluate({
|
|
614
|
+
script: `
|
|
615
|
+
const evil = '\`\${require("child_process").execSync("whoami")}\`';
|
|
616
|
+
return evil;
|
|
617
|
+
`,
|
|
618
|
+
})
|
|
619
|
+
// Should return the string, not execute it
|
|
620
|
+
if (result.success) {
|
|
621
|
+
expect(typeof result.value).toBe('string')
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
describe('unicode escape sequences', () => {
|
|
627
|
+
it('handles unicode escape in identifiers', async () => {
|
|
628
|
+
const result = await evaluate({
|
|
629
|
+
script: `
|
|
630
|
+
// \\u0070rocess would normalize to 'process'
|
|
631
|
+
const \\u0070rocess = 'safe';
|
|
632
|
+
return \\u0070rocess;
|
|
633
|
+
`,
|
|
634
|
+
})
|
|
635
|
+
if (result.success) {
|
|
636
|
+
expect(result.value).toBe('safe')
|
|
637
|
+
}
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
it('handles zero-width characters', async () => {
|
|
641
|
+
const result = await evaluate({
|
|
642
|
+
script: `
|
|
643
|
+
// Zero-width space and other invisible characters
|
|
644
|
+
const a\u200B = 'visible';
|
|
645
|
+
return a\u200B;
|
|
646
|
+
`,
|
|
647
|
+
})
|
|
648
|
+
// Should handle this gracefully
|
|
649
|
+
expect(result).toBeDefined()
|
|
650
|
+
})
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
describe('comment injection', () => {
|
|
654
|
+
it('handles comment-based code hiding', async () => {
|
|
655
|
+
const result = await evaluate({
|
|
656
|
+
script: `
|
|
657
|
+
const x = 1; /* legitimate code */
|
|
658
|
+
// const y = require('fs');
|
|
659
|
+
return x;
|
|
660
|
+
`,
|
|
661
|
+
})
|
|
662
|
+
expect(result.success).toBe(true)
|
|
663
|
+
expect(result.value).toBe(1)
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('handles multi-line comment tricks', async () => {
|
|
667
|
+
const result = await evaluate({
|
|
668
|
+
script: `
|
|
669
|
+
const a = 1 //* comment
|
|
670
|
+
+ 2 //*/ + 3;
|
|
671
|
+
return a;
|
|
672
|
+
`,
|
|
673
|
+
})
|
|
674
|
+
// Should parse correctly according to JS spec
|
|
675
|
+
expect(result.success).toBe(true)
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('handles HTML comment syntax in JS', async () => {
|
|
679
|
+
const result = await evaluate({
|
|
680
|
+
script: `
|
|
681
|
+
const x = 1;
|
|
682
|
+
<!-- this is an HTML comment in JS
|
|
683
|
+
const y = 2;
|
|
684
|
+
--> more code
|
|
685
|
+
return x;
|
|
686
|
+
`,
|
|
687
|
+
})
|
|
688
|
+
// Behavior depends on strict mode, but shouldn't crash
|
|
689
|
+
expect(result).toBeDefined()
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
describe('environment isolation', () => {
|
|
695
|
+
describe('parent worker environment', () => {
|
|
696
|
+
it('cannot access sensitive bindings from parent worker env', async () => {
|
|
697
|
+
const result = await evaluate({
|
|
698
|
+
script: `
|
|
699
|
+
// Check for sensitive bindings that should not leak
|
|
700
|
+
const checks = {
|
|
701
|
+
hasParentEnv: typeof parentEnv !== 'undefined',
|
|
702
|
+
hasKV: typeof env !== 'undefined' && !!env.KV,
|
|
703
|
+
hasDB: typeof env !== 'undefined' && !!env.DB,
|
|
704
|
+
hasDO: typeof env !== 'undefined' && !!env.DO,
|
|
705
|
+
hasR2: typeof env !== 'undefined' && !!env.R2,
|
|
706
|
+
hasSecrets: typeof env !== 'undefined' && !!env.API_KEY,
|
|
707
|
+
};
|
|
708
|
+
return checks;
|
|
709
|
+
`,
|
|
710
|
+
})
|
|
711
|
+
if (result.success && typeof result.value === 'object') {
|
|
712
|
+
const value = result.value as Record<string, unknown>
|
|
713
|
+
// Should not have access to parent's sensitive bindings
|
|
714
|
+
expect(value.hasParentEnv).toBe(false)
|
|
715
|
+
expect(value.hasKV).toBe(false)
|
|
716
|
+
expect(value.hasDB).toBe(false)
|
|
717
|
+
expect(value.hasDO).toBe(false)
|
|
718
|
+
expect(value.hasR2).toBe(false)
|
|
719
|
+
expect(value.hasSecrets).toBe(false)
|
|
720
|
+
}
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it('cannot access caches API for data exfiltration', async () => {
|
|
724
|
+
const result = await evaluate({
|
|
725
|
+
script: `
|
|
726
|
+
return (async () => {
|
|
727
|
+
try {
|
|
728
|
+
if (typeof caches !== 'undefined') {
|
|
729
|
+
const cache = await caches.open('exfil');
|
|
730
|
+
return { hasCaches: true };
|
|
731
|
+
}
|
|
732
|
+
return { hasCaches: false };
|
|
733
|
+
} catch (e) {
|
|
734
|
+
return { blocked: true, error: e.message };
|
|
735
|
+
}
|
|
736
|
+
})();
|
|
737
|
+
`,
|
|
738
|
+
})
|
|
739
|
+
// Either caches is unavailable or blocked
|
|
740
|
+
expect(result).toBeDefined()
|
|
741
|
+
})
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
describe('file system APIs', () => {
|
|
745
|
+
it('cannot access Node.js fs module', async () => {
|
|
746
|
+
const result = await evaluate({
|
|
747
|
+
script: `
|
|
748
|
+
try {
|
|
749
|
+
const fs = require('fs');
|
|
750
|
+
return { hasFs: true };
|
|
751
|
+
} catch (e) {
|
|
752
|
+
return { hasFs: false, error: e.message };
|
|
753
|
+
}
|
|
754
|
+
`,
|
|
755
|
+
})
|
|
756
|
+
if (result.success && typeof result.value === 'object') {
|
|
757
|
+
const value = result.value as Record<string, unknown>
|
|
758
|
+
expect(value.hasFs).toBe(false)
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('cannot access dynamic import of fs', async () => {
|
|
763
|
+
const result = await evaluate({
|
|
764
|
+
script: `
|
|
765
|
+
return (async () => {
|
|
766
|
+
try {
|
|
767
|
+
const fs = await import('fs');
|
|
768
|
+
return { hasFs: true };
|
|
769
|
+
} catch (e) {
|
|
770
|
+
return { hasFs: false, error: e.message };
|
|
771
|
+
}
|
|
772
|
+
})();
|
|
773
|
+
`,
|
|
774
|
+
})
|
|
775
|
+
if (result.success && typeof result.value === 'object') {
|
|
776
|
+
const value = result.value as Record<string, unknown>
|
|
777
|
+
expect(value.hasFs).toBe(false)
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('cannot access File System Access API', async () => {
|
|
782
|
+
const result = await evaluate({
|
|
783
|
+
script: `
|
|
784
|
+
return {
|
|
785
|
+
hasShowOpenFilePicker: typeof showOpenFilePicker !== 'undefined',
|
|
786
|
+
hasShowSaveFilePicker: typeof showSaveFilePicker !== 'undefined',
|
|
787
|
+
hasShowDirectoryPicker: typeof showDirectoryPicker !== 'undefined'
|
|
788
|
+
};
|
|
789
|
+
`,
|
|
790
|
+
})
|
|
791
|
+
if (result.success && typeof result.value === 'object') {
|
|
792
|
+
const value = result.value as Record<string, unknown>
|
|
793
|
+
expect(value.hasShowOpenFilePicker).toBe(false)
|
|
794
|
+
expect(value.hasShowSaveFilePicker).toBe(false)
|
|
795
|
+
expect(value.hasShowDirectoryPicker).toBe(false)
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
describe('process APIs', () => {
|
|
801
|
+
it('cannot access process.env', async () => {
|
|
802
|
+
const result = await evaluate({
|
|
803
|
+
script: `
|
|
804
|
+
try {
|
|
805
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
806
|
+
return { hasProcessEnv: true, keys: Object.keys(process.env) };
|
|
807
|
+
}
|
|
808
|
+
return { hasProcessEnv: false };
|
|
809
|
+
} catch (e) {
|
|
810
|
+
return { hasProcessEnv: false, error: e.message };
|
|
811
|
+
}
|
|
812
|
+
`,
|
|
813
|
+
})
|
|
814
|
+
if (result.success && typeof result.value === 'object') {
|
|
815
|
+
const value = result.value as Record<string, unknown>
|
|
816
|
+
expect(value.hasProcessEnv).toBe(false)
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
it('cannot access process.exit', async () => {
|
|
821
|
+
const result = await evaluate({
|
|
822
|
+
script: `
|
|
823
|
+
try {
|
|
824
|
+
if (typeof process !== 'undefined' && typeof process.exit === 'function') {
|
|
825
|
+
return { hasProcessExit: true };
|
|
826
|
+
}
|
|
827
|
+
return { hasProcessExit: false };
|
|
828
|
+
} catch (e) {
|
|
829
|
+
return { hasProcessExit: false, error: e.message };
|
|
830
|
+
}
|
|
831
|
+
`,
|
|
832
|
+
})
|
|
833
|
+
if (result.success && typeof result.value === 'object') {
|
|
834
|
+
const value = result.value as Record<string, unknown>
|
|
835
|
+
expect(value.hasProcessExit).toBe(false)
|
|
836
|
+
}
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
it('cannot access child_process', async () => {
|
|
840
|
+
const result = await evaluate({
|
|
841
|
+
script: `
|
|
842
|
+
try {
|
|
843
|
+
const cp = require('child_process');
|
|
844
|
+
return { hasChildProcess: true };
|
|
845
|
+
} catch (e) {
|
|
846
|
+
return { hasChildProcess: false, error: e.message };
|
|
847
|
+
}
|
|
848
|
+
`,
|
|
849
|
+
})
|
|
850
|
+
if (result.success && typeof result.value === 'object') {
|
|
851
|
+
const value = result.value as Record<string, unknown>
|
|
852
|
+
expect(value.hasChildProcess).toBe(false)
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
it('cannot spawn processes via dynamic import', async () => {
|
|
857
|
+
const result = await evaluate({
|
|
858
|
+
script: `
|
|
859
|
+
return (async () => {
|
|
860
|
+
try {
|
|
861
|
+
const { spawn } = await import('child_process');
|
|
862
|
+
return { hasSpawn: true };
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return { hasSpawn: false, error: e.message };
|
|
865
|
+
}
|
|
866
|
+
})();
|
|
867
|
+
`,
|
|
868
|
+
})
|
|
869
|
+
if (result.success && typeof result.value === 'object') {
|
|
870
|
+
const value = result.value as Record<string, unknown>
|
|
871
|
+
expect(value.hasSpawn).toBe(false)
|
|
872
|
+
}
|
|
873
|
+
})
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
describe('dangerous globals', () => {
|
|
877
|
+
it('cannot access Deno namespace', async () => {
|
|
878
|
+
const result = await evaluate({
|
|
879
|
+
script: `
|
|
880
|
+
return { hasDeno: typeof Deno !== 'undefined' };
|
|
881
|
+
`,
|
|
882
|
+
})
|
|
883
|
+
if (result.success && typeof result.value === 'object') {
|
|
884
|
+
const value = result.value as Record<string, unknown>
|
|
885
|
+
expect(value.hasDeno).toBe(false)
|
|
886
|
+
}
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('cannot access Bun namespace', async () => {
|
|
890
|
+
const result = await evaluate({
|
|
891
|
+
script: `
|
|
892
|
+
return { hasBun: typeof Bun !== 'undefined' };
|
|
893
|
+
`,
|
|
894
|
+
})
|
|
895
|
+
if (result.success && typeof result.value === 'object') {
|
|
896
|
+
const value = result.value as Record<string, unknown>
|
|
897
|
+
expect(value.hasBun).toBe(false)
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
it('handles WebAssembly (may be sandboxed)', async () => {
|
|
902
|
+
const result = await evaluate({
|
|
903
|
+
script: `
|
|
904
|
+
try {
|
|
905
|
+
const wasmCode = new Uint8Array([
|
|
906
|
+
0x00, 0x61, 0x73, 0x6d, // WASM magic
|
|
907
|
+
0x01, 0x00, 0x00, 0x00 // Version
|
|
908
|
+
]);
|
|
909
|
+
const module = new WebAssembly.Module(wasmCode);
|
|
910
|
+
return { hasWasm: true };
|
|
911
|
+
} catch (e) {
|
|
912
|
+
return { hasWasm: false, error: e.message };
|
|
913
|
+
}
|
|
914
|
+
`,
|
|
915
|
+
})
|
|
916
|
+
// WebAssembly might be available but sandboxed
|
|
917
|
+
expect(result).toBeDefined()
|
|
918
|
+
})
|
|
919
|
+
})
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
describe('additional security vectors', () => {
|
|
923
|
+
it('handles __proto__ manipulation without affecting host', async () => {
|
|
924
|
+
const result = await evaluate({
|
|
925
|
+
script: `
|
|
926
|
+
const obj = {};
|
|
927
|
+
obj.__proto__.polluted = true;
|
|
928
|
+
return { polluted: ({}).polluted };
|
|
929
|
+
`,
|
|
930
|
+
})
|
|
931
|
+
// Host environment should not be affected
|
|
932
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('handles Object.defineProperty on prototypes without affecting host', async () => {
|
|
936
|
+
const result = await evaluate({
|
|
937
|
+
script: `
|
|
938
|
+
try {
|
|
939
|
+
Object.defineProperty(Object.prototype, 'pwned', {
|
|
940
|
+
get: () => 'gotcha',
|
|
941
|
+
configurable: true
|
|
942
|
+
});
|
|
943
|
+
return { pwned: ({}).pwned };
|
|
944
|
+
} catch (e) {
|
|
945
|
+
return { blocked: true, error: e.message };
|
|
946
|
+
}
|
|
947
|
+
`,
|
|
948
|
+
})
|
|
949
|
+
// Host environment should not be affected
|
|
950
|
+
expect(({} as Record<string, unknown>).pwned).toBeUndefined()
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it('handles Symbol.toStringTag manipulation', async () => {
|
|
954
|
+
const result = await evaluate({
|
|
955
|
+
script: `
|
|
956
|
+
const fake = {
|
|
957
|
+
[Symbol.toStringTag]: 'Process',
|
|
958
|
+
env: { SECRET: 'value' }
|
|
959
|
+
};
|
|
960
|
+
return Object.prototype.toString.call(fake);
|
|
961
|
+
`,
|
|
962
|
+
})
|
|
963
|
+
if (result.success) {
|
|
964
|
+
expect(result.value).toBe('[object Process]')
|
|
965
|
+
}
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
it('handles Proxy-based traps', async () => {
|
|
969
|
+
const result = await evaluate({
|
|
970
|
+
script: `
|
|
971
|
+
const handler = {
|
|
972
|
+
get: (target, prop) => {
|
|
973
|
+
if (prop === 'process') {
|
|
974
|
+
return { env: {} };
|
|
975
|
+
}
|
|
976
|
+
return target[prop];
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const proxy = new Proxy({}, handler);
|
|
980
|
+
return { hasProxy: true, process: proxy.process };
|
|
981
|
+
`,
|
|
982
|
+
})
|
|
983
|
+
// Proxy should work but not give real process access
|
|
984
|
+
if (result.success && typeof result.value === 'object') {
|
|
985
|
+
const value = result.value as Record<string, unknown>
|
|
986
|
+
expect(value.hasProxy).toBe(true)
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
it('handles Reflect-based access attempts', async () => {
|
|
991
|
+
const result = await evaluate({
|
|
992
|
+
script: `
|
|
993
|
+
try {
|
|
994
|
+
const global = Reflect.getPrototypeOf(Reflect.getPrototypeOf(() => {})).constructor('return this')();
|
|
995
|
+
return { hasGlobal: !!global, hasProcess: !!global?.process };
|
|
996
|
+
} catch (e) {
|
|
997
|
+
return { blocked: true, error: e.message };
|
|
998
|
+
}
|
|
999
|
+
`,
|
|
1000
|
+
})
|
|
1001
|
+
if (result.success && typeof result.value === 'object') {
|
|
1002
|
+
const value = result.value as Record<string, unknown>
|
|
1003
|
+
if (!value.blocked) {
|
|
1004
|
+
expect(value.hasProcess).toBe(false)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
})
|