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.
Files changed (100) hide show
  1. package/README.md +90 -3
  2. package/dist/capnweb-bundle.d.ts +10 -0
  3. package/dist/capnweb-bundle.d.ts.map +1 -0
  4. package/dist/capnweb-bundle.js +2596 -0
  5. package/dist/capnweb-bundle.js.map +1 -0
  6. package/dist/evaluate.d.ts +1 -1
  7. package/dist/evaluate.d.ts.map +1 -1
  8. package/dist/evaluate.js +186 -7
  9. package/dist/evaluate.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/miniflare-pool.d.ts +109 -0
  15. package/dist/miniflare-pool.d.ts.map +1 -0
  16. package/dist/miniflare-pool.js +308 -0
  17. package/dist/miniflare-pool.js.map +1 -0
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +42 -10
  20. package/dist/node.js.map +1 -1
  21. package/dist/shared.d.ts +66 -0
  22. package/dist/shared.d.ts.map +1 -0
  23. package/dist/shared.js +169 -0
  24. package/dist/shared.js.map +1 -0
  25. package/dist/type-guards.d.ts +21 -0
  26. package/dist/type-guards.d.ts.map +1 -0
  27. package/dist/type-guards.js +216 -0
  28. package/dist/type-guards.js.map +1 -0
  29. package/dist/types.d.ts +17 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/validation.d.ts +26 -0
  32. package/dist/validation.d.ts.map +1 -0
  33. package/dist/validation.js +104 -0
  34. package/dist/validation.js.map +1 -0
  35. package/dist/worker-template/code-transforms.d.ts +9 -0
  36. package/dist/worker-template/code-transforms.d.ts.map +1 -0
  37. package/dist/worker-template/code-transforms.js +28 -0
  38. package/dist/worker-template/code-transforms.js.map +1 -0
  39. package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
  40. package/dist/worker-template/core.d.ts.map +1 -0
  41. package/dist/worker-template/core.js +502 -0
  42. package/dist/worker-template/core.js.map +1 -0
  43. package/dist/worker-template/helpers.d.ts +14 -0
  44. package/dist/worker-template/helpers.d.ts.map +1 -0
  45. package/dist/worker-template/helpers.js +79 -0
  46. package/dist/worker-template/helpers.js.map +1 -0
  47. package/dist/worker-template/index.d.ts +14 -0
  48. package/dist/worker-template/index.d.ts.map +1 -0
  49. package/dist/worker-template/index.js +19 -0
  50. package/dist/worker-template/index.js.map +1 -0
  51. package/dist/worker-template/sdk-generator.d.ts +17 -0
  52. package/dist/worker-template/sdk-generator.d.ts.map +1 -0
  53. package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
  54. package/dist/worker-template/sdk-generator.js.map +1 -0
  55. package/dist/worker-template/test-generator.d.ts +16 -0
  56. package/dist/worker-template/test-generator.d.ts.map +1 -0
  57. package/dist/worker-template/test-generator.js +357 -0
  58. package/dist/worker-template/test-generator.js.map +1 -0
  59. package/dist/worker-template.d.ts +2 -2
  60. package/dist/worker-template.d.ts.map +1 -1
  61. package/dist/worker-template.js +64 -31
  62. package/dist/worker-template.js.map +1 -1
  63. package/example/package.json +7 -3
  64. package/example/src/index.ts +194 -40
  65. package/example/wrangler.jsonc +18 -2
  66. package/package.json +1 -3
  67. package/src/capnweb-bundle.ts +2596 -0
  68. package/src/evaluate.ts +216 -7
  69. package/src/index.ts +3 -1
  70. package/src/miniflare-pool.ts +395 -0
  71. package/src/node.ts +56 -11
  72. package/src/shared.ts +186 -0
  73. package/src/type-guards.ts +323 -0
  74. package/src/types.ts +18 -2
  75. package/src/validation.ts +120 -0
  76. package/src/worker-template/code-transforms.ts +32 -0
  77. package/src/worker-template/core.ts +557 -0
  78. package/src/worker-template/helpers.ts +90 -0
  79. package/src/worker-template/index.ts +23 -0
  80. package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
  81. package/src/worker-template/test-generator.ts +358 -0
  82. package/test/miniflare-pool.test.ts +246 -0
  83. package/test/node.test.ts +467 -0
  84. package/test/security.test.ts +1009 -0
  85. package/test/shared.test.ts +105 -0
  86. package/test/type-guards.test.ts +303 -0
  87. package/test/validation.test.ts +240 -0
  88. package/test/worker-template.test.ts +21 -19
  89. package/src/evaluate.js +0 -187
  90. package/src/index.js +0 -10
  91. package/src/node.d.ts +0 -17
  92. package/src/node.d.ts.map +0 -1
  93. package/src/node.js +0 -168
  94. package/src/node.js.map +0 -1
  95. package/src/types.d.ts +0 -172
  96. package/src/types.d.ts.map +0 -1
  97. package/src/types.js +0 -4
  98. package/src/types.js.map +0 -1
  99. package/src/worker-template.d.ts.map +0 -1
  100. 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
+ })