@mondaydotcomorg/atp-server 0.19.5 → 0.19.6

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.
@@ -2,8 +2,12 @@
2
2
  * AST Provenance Tracking Runtime for isolated-vm
3
3
  * This code is injected into the isolate and runs INSIDE the sandbox
4
4
  * It must be plain JavaScript with no imports
5
+ *
6
+ * This runtime provides comprehensive taint tracking by:
7
+ * 1. Deep-tainting all primitives from tool results
8
+ * 2. Intercepting native methods with re-entry protection
9
+ * 3. Supporting cross-execution tracking via hints
5
10
  */
6
- // TODO: need to create atp.internal with internal functions like has to reduce complexity
7
11
  export const AST_TRACKING_RUNTIME = `
8
12
  // Pure JavaScript SHA-256 implementation for digest computation
9
13
  function sha256(str) {
@@ -98,18 +102,26 @@ function sha256(str) {
98
102
  return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');
99
103
  }
100
104
 
105
+ // Re-entry guard to prevent infinite recursion in wrapped methods
106
+ let __inTrackingCall = false;
107
+
101
108
  const __astTracker = {
102
109
  metadata: new Map(),
103
110
  nextId: 0,
104
111
  hints: new Map(globalThis.__provenance_hints || []),
105
112
  hintValues: new Map(globalThis.__provenance_hint_values || []),
113
+ // Track tainted objects via WeakMap for O(1) lookup
114
+ taintedObjects: new WeakMap(),
106
115
 
107
- // SHA-256 digest computation to match server-side
108
116
  computeDigest(value) {
117
+ if (__inTrackingCall) return null;
109
118
  try {
119
+ __inTrackingCall = true;
110
120
  const str = JSON.stringify(value);
121
+ __inTrackingCall = false;
111
122
  return sha256(str);
112
123
  } catch (e) {
124
+ __inTrackingCall = false;
113
125
  return null;
114
126
  }
115
127
  },
@@ -117,7 +129,7 @@ const __astTracker = {
117
129
  getId(value) {
118
130
  if (typeof value === 'object' && value !== null) {
119
131
  if (!value.__prov_id__) {
120
- const id = 'tracked_' + this.nextId++;
132
+ const id = 'tracked_' + Date.now() + '_' + (Math.random().toString(36).substring(2, 8));
121
133
  try {
122
134
  Object.defineProperty(value, '__prov_id__', {
123
135
  value: id,
@@ -135,20 +147,87 @@ const __astTracker = {
135
147
  return 'primitive_' + Date.now() + '_' + Math.random();
136
148
  },
137
149
 
150
+ // Register a primitive value as tainted
151
+ registerTaintedPrimitive(value, metadata) {
152
+ // Accept any non-empty string or any valid number (including 0)
153
+ const isValidString = typeof value === 'string' && value !== '';
154
+ const isValidNumber = typeof value === 'number' && !Number.isNaN(value);
155
+
156
+ if (isValidString || isValidNumber) {
157
+ const taintedKey = 'tainted:' + String(value);
158
+ this.metadata.set(taintedKey, {
159
+ ...metadata,
160
+ readers: metadata.readers || { type: 'restricted', readers: [] },
161
+ dependencies: metadata.dependencies || metadata.deps || []
162
+ });
163
+ }
164
+ },
165
+
166
+ // Deep taint an object - register all nested primitives
167
+ deepTaintObject(obj, metadata, visited = new WeakSet()) {
168
+ if (!obj || typeof obj !== 'object') return;
169
+ if (visited.has(obj)) return;
170
+ visited.add(obj);
171
+
172
+ // Mark object in WeakMap for fast lookup
173
+ this.taintedObjects.set(obj, metadata);
174
+
175
+ // Set __prov_id__ and register in metadata
176
+ const id = this.getId(obj);
177
+ this.metadata.set(id, metadata);
178
+
179
+ // Register all primitive properties
180
+ const keys = Object.keys(obj);
181
+ for (let i = 0; i < keys.length; i++) {
182
+ const key = keys[i];
183
+ const value = obj[key];
184
+
185
+ if (typeof value === 'string' || typeof value === 'number') {
186
+ this.registerTaintedPrimitive(value, metadata);
187
+ // Also store by object:key:value for additional lookup
188
+ const primitiveKey = id + ':' + key + ':' + String(value);
189
+ this.metadata.set(primitiveKey, metadata);
190
+ } else if (Array.isArray(value)) {
191
+ // Taint array and its elements
192
+ this.deepTaintObject(value, metadata, visited);
193
+ for (let j = 0; j < value.length; j++) {
194
+ const elem = value[j];
195
+ if (typeof elem === 'string' || typeof elem === 'number') {
196
+ this.registerTaintedPrimitive(elem, metadata);
197
+ } else if (typeof elem === 'object' && elem !== null) {
198
+ this.deepTaintObject(elem, metadata, visited);
199
+ }
200
+ }
201
+ } else if (typeof value === 'object' && value !== null) {
202
+ this.deepTaintObject(value, metadata, visited);
203
+ }
204
+ }
205
+ },
206
+
138
207
  track(value, source, deps) {
139
208
  try {
140
209
  const id = this.getId(value);
141
- this.metadata.set(id, { id, source, deps: deps || [] });
142
- console.log('[__track] Stored metadata:', id, 'source:', source.type, 'metadataSize:', this.metadata.size);
210
+ const metadata = {
211
+ id,
212
+ source,
213
+ deps: deps || [],
214
+ readers: source.readers || { type: 'restricted', readers: ['tool:' + (source.tool || source.operation || 'unknown')] },
215
+ dependencies: []
216
+ };
217
+ this.metadata.set(id, metadata);
218
+
219
+ // Deep taint all nested primitives
220
+ if (value && typeof value === 'object') {
221
+ this.deepTaintObject(value, metadata);
222
+ }
223
+
143
224
  return value;
144
225
  } catch (error) {
145
- console.error('[__track] Error:', error);
146
226
  return value;
147
227
  }
148
228
  },
149
229
 
150
230
  trackBinary(left, right, operator) {
151
- // Perform the actual operation
152
231
  let result;
153
232
  switch (operator) {
154
233
  case '+': result = left + right; break;
@@ -169,88 +248,13 @@ const __astTracker = {
169
248
  default: result = left;
170
249
  }
171
250
 
172
- // Check if either operand has provenance
173
- let hasToolSource = false;
174
- let toolMetadata = null;
175
-
176
- // Helper to check primitive provenance
177
- const checkPrimitive = (value) => {
178
- if (typeof value !== 'string' && typeof value !== 'number') {
179
- return null;
180
- }
181
-
182
- // Check tainted key first
183
- const taintedKey = 'tainted:' + String(value);
184
- const taintedMeta = this.metadata.get(taintedKey);
185
- if (taintedMeta && taintedMeta.source && taintedMeta.source.type === 'tool') {
186
- return taintedMeta;
187
- }
188
-
189
- // Check hint-based tracking
190
- const digest = this.computeDigest(value);
191
- const hintMeta = this.hints.get(digest);
192
- if (hintMeta && hintMeta.source && hintMeta.source.type === 'tool') {
193
- return hintMeta;
194
- }
195
-
196
- // Check primitive map (id:key:value format)
197
- for (const [key, meta] of this.metadata.entries()) {
198
- if (!key.startsWith('tainted:') && key.includes(':')) {
199
- const parts = key.split(':');
200
- if (parts.length >= 3) {
201
- const primitiveValue = parts.slice(2).join(':');
202
- if (primitiveValue === String(value) && meta.source && meta.source.type === 'tool') {
203
- return meta;
204
- }
205
- }
206
- }
207
- }
208
-
209
- return null;
210
- };
211
-
212
- // Check left operand
213
- if (typeof left === 'object' && left && left.__prov_id__) {
214
- const leftMeta = this.metadata.get(left.__prov_id__);
215
- if (leftMeta && leftMeta.source && leftMeta.source.type === 'tool') {
216
- hasToolSource = true;
217
- toolMetadata = leftMeta;
218
- }
219
- } else {
220
- const primMeta = checkPrimitive(left);
221
- if (primMeta) {
222
- hasToolSource = true;
223
- toolMetadata = primMeta;
224
- }
225
- }
226
-
227
- // Check right operand
228
- if (!hasToolSource) {
229
- if (typeof right === 'object' && right && right.__prov_id__) {
230
- const rightMeta = this.metadata.get(right.__prov_id__);
231
- if (rightMeta && rightMeta.source && rightMeta.source.type === 'tool') {
232
- hasToolSource = true;
233
- toolMetadata = rightMeta;
234
- }
235
- } else {
236
- const primMeta = checkPrimitive(right);
237
- if (primMeta) {
238
- hasToolSource = true;
239
- toolMetadata = primMeta;
240
- }
241
- }
242
- }
251
+ // Propagate taint from operands to result
252
+ const leftMeta = this.checkProvenance(left);
253
+ const rightMeta = this.checkProvenance(right);
254
+ const toolMetadata = leftMeta || rightMeta;
243
255
 
244
- // If result is a string and has tool-sourced operand, mark it as tainted
245
- if (hasToolSource && toolMetadata && (typeof result === 'string' || typeof result === 'number')) {
246
- const taintedKey = 'tainted:' + String(result);
247
- // Ensure metadata has all required fields, preserving readers from source
248
- const fullMetadata = {
249
- ...toolMetadata,
250
- readers: toolMetadata.readers || { type: 'restricted', readers: [] },
251
- dependencies: toolMetadata.dependencies || toolMetadata.deps || []
252
- };
253
- this.metadata.set(taintedKey, fullMetadata);
256
+ if (toolMetadata && (typeof result === 'string' || typeof result === 'number')) {
257
+ this.registerTaintedPrimitive(result, toolMetadata);
254
258
  }
255
259
 
256
260
  return result;
@@ -261,28 +265,27 @@ const __astTracker = {
261
265
  },
262
266
 
263
267
  async trackMethod(object, method, args) {
264
- // Recursively wrap tainted primitives in arguments before calling the method
268
+ // Wrap tainted primitives in arguments before calling API methods
269
+ const self = this;
265
270
  function wrapTaintedInArgs(val, visited = new WeakSet()) {
266
271
  if (val === null || val === undefined) return val;
267
272
 
268
- // Check if this value has provenance
269
- const prov = this.checkProvenance(val);
273
+ const prov = self.checkProvenance(val);
270
274
  if (prov && (typeof val === 'string' || typeof val === 'number')) {
271
- // Wrap tainted primitive
272
275
  return { __tainted_value: val, __prov_meta: prov };
273
276
  }
274
277
 
275
- // Recursively process objects/arrays
276
278
  if (typeof val === 'object') {
277
279
  if (visited.has(val)) return val;
278
280
  visited.add(val);
279
281
 
280
282
  if (Array.isArray(val)) {
281
- return val.map(item => wrapTaintedInArgs.call(this, item, visited));
283
+ return val.map(item => wrapTaintedInArgs(item, visited));
282
284
  } else {
283
285
  const wrapped = {};
284
- for (const [key, v] of Object.entries(val)) {
285
- wrapped[key] = wrapTaintedInArgs.call(this, v, visited);
286
+ const keys = Object.keys(val);
287
+ for (let i = 0; i < keys.length; i++) {
288
+ wrapped[keys[i]] = wrapTaintedInArgs(val[keys[i]], visited);
286
289
  }
287
290
  return wrapped;
288
291
  }
@@ -291,72 +294,38 @@ const __astTracker = {
291
294
  return val;
292
295
  }
293
296
 
294
- // Wrap arguments
295
- const wrappedArgs = args.map(arg => wrapTaintedInArgs.call(this, arg));
297
+ const wrappedArgs = args.map(arg => wrapTaintedInArgs(arg));
296
298
 
297
- // Call the method with wrapped arguments
298
299
  if (typeof object === 'object' && object !== null && method in object) {
299
300
  const result = await object[method](...wrappedArgs);
300
301
 
301
- // Track the result
302
302
  if (result && typeof result === 'object') {
303
303
  const id = this.getId(result);
304
304
 
305
- // Extract authorized readers from common param patterns (email, userId, username, user)
306
- // Match server-side logic in sandbox-builder.ts (lines 459-470)
307
305
  let authorizedReaders = [];
308
306
  for (const arg of args) {
309
307
  if (arg && typeof arg === 'object') {
310
- // Check for user identifier fields (email, user, userId only - no generic 'id')
311
308
  const value = arg.email || arg.user || arg.userId;
312
309
  if (typeof value === 'string' && value.length > 0) {
313
310
  authorizedReaders.push(value);
314
- break; // Only take first identifier
311
+ break;
315
312
  }
316
313
  }
317
314
  }
318
315
 
319
- // If no email found, use tool-scoped authorization (matches server logic line 467-470)
320
316
  if (authorizedReaders.length === 0) {
321
317
  authorizedReaders = ['tool:' + method];
322
318
  }
323
319
 
324
- // Tool data should be restricted by default to prevent exfiltration
325
320
  const metadata = {
326
321
  id,
327
- source: {
328
- type: 'tool',
329
- operation: method,
330
- toolName: method,
331
- timestamp: Date.now()
332
- },
322
+ source: { type: 'tool', operation: method, toolName: method, timestamp: Date.now() },
333
323
  readers: { type: 'restricted', readers: authorizedReaders },
334
- deps: [this.getId(object), ...args.map(a => this.getId(a))],
324
+ deps: [],
335
325
  dependencies: []
336
326
  };
337
327
  this.metadata.set(id, metadata);
338
-
339
- // Track primitive properties for token emission
340
- for (const key in result) {
341
- if (Object.prototype.hasOwnProperty.call(result, key)) {
342
- const value = result[key];
343
- if (typeof value === 'string' || typeof value === 'number') {
344
- // Check if this primitive matches any hints
345
- const digest = this.computeDigest(value);
346
- const hintMeta = this.hints.get(digest);
347
-
348
- const primitiveKey = id + ':' + key + ':' + String(value);
349
- // Use hint metadata if available, otherwise use result metadata
350
- this.metadata.set(primitiveKey, hintMeta || metadata);
351
-
352
- // Also store by digest for cross-execution matching
353
- if (hintMeta) {
354
- const taintedKey = 'tainted:' + String(value);
355
- this.metadata.set(taintedKey, hintMeta);
356
- }
357
- }
358
- }
359
- }
328
+ this.deepTaintObject(result, metadata);
360
329
  }
361
330
 
362
331
  return result;
@@ -366,81 +335,55 @@ const __astTracker = {
366
335
 
367
336
  trackTemplate(expressions, quasis) {
368
337
  let result = '';
369
- let hasToolSource = false;
370
338
  let toolMetadata = null;
371
339
 
372
- // Helper to check primitive provenance
373
- const checkPrimitive = (value) => {
374
- if (typeof value !== 'string' && typeof value !== 'number') {
375
- return null;
376
- }
377
-
378
- // Check tainted key first
379
- const taintedKey = 'tainted:' + String(value);
380
- const taintedMeta = this.metadata.get(taintedKey);
381
- if (taintedMeta && taintedMeta.source && taintedMeta.source.type === 'tool') {
382
- return taintedMeta;
383
- }
384
-
385
- // Check hint-based tracking
386
- const digest = this.computeDigest(value);
387
- const hintMeta = this.hints.get(digest);
388
- if (hintMeta && hintMeta.source && hintMeta.source.type === 'tool') {
389
- return hintMeta;
390
- }
391
-
392
- // Check primitive map (id:key:value format)
393
- for (const [key, meta] of this.metadata.entries()) {
394
- if (!key.startsWith('tainted:') && key.includes(':')) {
395
- const parts = key.split(':');
396
- if (parts.length >= 3) {
397
- const primitiveValue = parts.slice(2).join(':');
398
- if (primitiveValue === String(value) && meta.source && meta.source.type === 'tool') {
399
- return meta;
400
- }
401
- }
402
- }
403
- }
404
-
405
- return null;
406
- };
407
-
408
340
  for (let i = 0; i < quasis.length; i++) {
409
341
  result += quasis[i] || '';
410
342
  if (i < expressions.length) {
411
343
  const expr = expressions[i];
412
344
  result += String(expr);
413
345
 
414
- // Check if expression has provenance
415
- if (!hasToolSource) {
416
- // Check object provenance
417
- if (typeof expr === 'object' && expr && expr.__prov_id__) {
418
- const exprMeta = this.metadata.get(expr.__prov_id__);
419
- if (exprMeta && exprMeta.source && exprMeta.source.type === 'tool') {
420
- hasToolSource = true;
421
- toolMetadata = exprMeta;
422
- }
423
- } else {
424
- const primMeta = checkPrimitive(expr);
425
- if (primMeta) {
426
- hasToolSource = true;
427
- toolMetadata = primMeta;
428
- }
429
- }
346
+ if (!toolMetadata) {
347
+ toolMetadata = this.checkProvenance(expr);
430
348
  }
431
349
  }
432
350
  }
433
351
 
434
- // If template contains tool-sourced data, mark result as tainted
435
- if (hasToolSource && toolMetadata) {
436
- const taintedKey = 'tainted:' + result;
437
- // Ensure metadata has all required fields, preserving readers from source
438
- const fullMetadata = {
439
- ...toolMetadata,
440
- readers: toolMetadata.readers || { type: 'restricted', readers: [] },
441
- dependencies: toolMetadata.dependencies || toolMetadata.deps || []
442
- };
443
- this.metadata.set(taintedKey, fullMetadata);
352
+ if (toolMetadata) {
353
+ this.registerTaintedPrimitive(result, toolMetadata);
354
+ }
355
+
356
+ return result;
357
+ },
358
+
359
+ // Propagate taint from source to result for native method calls
360
+ propagateTaint(source, args, result) {
361
+ if (__inTrackingCall) return result;
362
+
363
+ __inTrackingCall = true;
364
+ try {
365
+ // Check if source or any arg is tainted
366
+ let metadata = this.checkProvenanceInternal(source);
367
+
368
+ if (!metadata && args) {
369
+ for (let i = 0; i < args.length; i++) {
370
+ metadata = this.checkProvenanceInternal(args[i]);
371
+ if (metadata) break;
372
+ }
373
+ }
374
+
375
+ // Propagate taint to result
376
+ if (metadata) {
377
+ if (typeof result === 'string' || typeof result === 'number') {
378
+ this.registerTaintedPrimitive(result, metadata);
379
+ } else if (Array.isArray(result)) {
380
+ this.deepTaintObject(result, metadata);
381
+ } else if (typeof result === 'object' && result !== null) {
382
+ this.deepTaintObject(result, metadata);
383
+ }
384
+ }
385
+ } finally {
386
+ __inTrackingCall = false;
444
387
  }
445
388
 
446
389
  return result;
@@ -455,13 +398,16 @@ const __astTracker = {
455
398
  return Array.from(this.metadata.entries());
456
399
  },
457
400
 
458
- // Check if a value or any nested value has tool-sourced provenance
459
- checkProvenance(value) {
460
- if (value === null || value === undefined) {
461
- return null;
401
+ // Internal provenance check (no re-entry guard - caller must set it)
402
+ checkProvenanceInternal(value) {
403
+ if (value === null || value === undefined) return null;
404
+
405
+ // Fast path: check WeakMap for objects
406
+ if (typeof value === 'object' && this.taintedObjects.has(value)) {
407
+ return this.taintedObjects.get(value);
462
408
  }
463
409
 
464
- // Check if it's an object with __prov_id__
410
+ // Check object __prov_id__
465
411
  if (typeof value === 'object' && value.__prov_id__) {
466
412
  const meta = this.metadata.get(value.__prov_id__);
467
413
  if (meta && meta.source && meta.source.type === 'tool') {
@@ -469,7 +415,7 @@ const __astTracker = {
469
415
  }
470
416
  }
471
417
 
472
- // Check if it's a primitive with tainted metadata
418
+ // Check primitive taint
473
419
  if (typeof value === 'string' || typeof value === 'number') {
474
420
  const taintedKey = 'tainted:' + String(value);
475
421
  const taintedMeta = this.metadata.get(taintedKey);
@@ -477,8 +423,10 @@ const __astTracker = {
477
423
  return taintedMeta;
478
424
  }
479
425
 
480
- // Check primitive map
426
+ // Check primitive map (object:key:value format)
481
427
  for (const [key, meta] of this.metadata.entries()) {
428
+ // Skip non-string keys (could be Symbols)
429
+ if (typeof key !== 'string') continue;
482
430
  if (!key.startsWith('tainted:') && key.includes(':')) {
483
431
  const parts = key.split(':');
484
432
  if (parts.length >= 3) {
@@ -490,31 +438,362 @@ const __astTracker = {
490
438
  }
491
439
  }
492
440
 
493
- // Check hints
441
+ // Check hints (cross-execution)
494
442
  const digest = this.computeDigest(value);
495
- const hintMeta = this.hints.get(digest);
496
- if (hintMeta && hintMeta.source && hintMeta.source.type === 'tool') {
497
- return hintMeta;
443
+ if (digest) {
444
+ const hintMeta = this.hints.get(digest);
445
+ if (hintMeta && hintMeta.source && hintMeta.source.type === 'tool') {
446
+ return hintMeta;
447
+ }
448
+ }
449
+
450
+ // Check substring containment in hint values
451
+ if (typeof value === 'string' && this.hintValues && this.hintValues.size > 0) {
452
+ for (const [hintValue, meta] of this.hintValues.entries()) {
453
+ if (value.includes(hintValue) && meta.source && meta.source.type === 'tool') {
454
+ return meta;
455
+ }
456
+ }
498
457
  }
499
458
  }
500
459
 
501
- // For objects/arrays, recursively check all values
460
+ // Recursively check object properties
502
461
  if (typeof value === 'object') {
503
- for (const key in value) {
504
- if (Object.prototype.hasOwnProperty.call(value, key)) {
505
- const nestedMeta = this.checkProvenance(value[key]);
506
- if (nestedMeta) {
507
- return nestedMeta;
508
- }
509
- }
462
+ const keys = Object.keys(value);
463
+ for (let i = 0; i < keys.length; i++) {
464
+ const nestedMeta = this.checkProvenanceInternal(value[keys[i]]);
465
+ if (nestedMeta) return nestedMeta;
510
466
  }
511
467
  }
512
468
 
513
469
  return null;
470
+ },
471
+
472
+ // Public provenance check with re-entry guard
473
+ checkProvenance(value) {
474
+ if (__inTrackingCall) return null;
475
+
476
+ __inTrackingCall = true;
477
+ try {
478
+ return this.checkProvenanceInternal(value);
479
+ } finally {
480
+ __inTrackingCall = false;
481
+ }
514
482
  }
515
483
  };
516
484
 
517
- // Expose tracking functions globally
485
+ // ============================================================================
486
+ // NATIVE METHOD INTERCEPTION
487
+ // We store originals and wrap methods with taint propagation
488
+ // ============================================================================
489
+
490
+ const __originals = {
491
+ String_toUpperCase: String.prototype.toUpperCase,
492
+ String_toLowerCase: String.prototype.toLowerCase,
493
+ String_slice: String.prototype.slice,
494
+ String_substring: String.prototype.substring,
495
+ String_substr: String.prototype.substr,
496
+ String_trim: String.prototype.trim,
497
+ String_trimStart: String.prototype.trimStart,
498
+ String_trimEnd: String.prototype.trimEnd,
499
+ String_replace: String.prototype.replace,
500
+ String_replaceAll: String.prototype.replaceAll,
501
+ String_split: String.prototype.split,
502
+ String_charAt: String.prototype.charAt,
503
+ String_concat: String.prototype.concat,
504
+ String_padStart: String.prototype.padStart,
505
+ String_padEnd: String.prototype.padEnd,
506
+ String_repeat: String.prototype.repeat,
507
+ String_normalize: String.prototype.normalize,
508
+
509
+ Array_map: Array.prototype.map,
510
+ Array_filter: Array.prototype.filter,
511
+ Array_reduce: Array.prototype.reduce,
512
+ Array_reduceRight: Array.prototype.reduceRight,
513
+ Array_join: Array.prototype.join,
514
+ Array_slice: Array.prototype.slice,
515
+ Array_concat: Array.prototype.concat,
516
+ Array_flat: Array.prototype.flat,
517
+ Array_flatMap: Array.prototype.flatMap,
518
+ Array_find: Array.prototype.find,
519
+ Array_every: Array.prototype.every,
520
+ Array_some: Array.prototype.some,
521
+
522
+ Number_toFixed: Number.prototype.toFixed,
523
+ Number_toPrecision: Number.prototype.toPrecision,
524
+ Number_toExponential: Number.prototype.toExponential,
525
+
526
+ JSON_stringify: JSON.stringify,
527
+ JSON_parse: JSON.parse,
528
+
529
+ String_ctor: String,
530
+ Number_ctor: Number,
531
+ parseInt_fn: parseInt,
532
+ parseFloat_fn: parseFloat,
533
+ };
534
+
535
+ // String method wrappers - use this.valueOf() to get primitive for provenance check
536
+ String.prototype.toUpperCase = function() {
537
+ const result = __originals.String_toUpperCase.call(this);
538
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
539
+ return __astTracker.propagateTaint(primitiveThis, [], result);
540
+ };
541
+
542
+ String.prototype.toLowerCase = function() {
543
+ const result = __originals.String_toLowerCase.call(this);
544
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
545
+ return __astTracker.propagateTaint(primitiveThis, [], result);
546
+ };
547
+
548
+ String.prototype.slice = function(start, end) {
549
+ const result = __originals.String_slice.call(this, start, end);
550
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
551
+ return __astTracker.propagateTaint(primitiveThis, [], result);
552
+ };
553
+
554
+ String.prototype.substring = function(start, end) {
555
+ const result = __originals.String_substring.call(this, start, end);
556
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
557
+ return __astTracker.propagateTaint(primitiveThis, [], result);
558
+ };
559
+
560
+ String.prototype.substr = function(start, length) {
561
+ const result = __originals.String_substr.call(this, start, length);
562
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
563
+ return __astTracker.propagateTaint(primitiveThis, [], result);
564
+ };
565
+
566
+ String.prototype.trim = function() {
567
+ const result = __originals.String_trim.call(this);
568
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
569
+ return __astTracker.propagateTaint(primitiveThis, [], result);
570
+ };
571
+
572
+ String.prototype.trimStart = function() {
573
+ const result = __originals.String_trimStart.call(this);
574
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
575
+ return __astTracker.propagateTaint(primitiveThis, [], result);
576
+ };
577
+
578
+ String.prototype.trimEnd = function() {
579
+ const result = __originals.String_trimEnd.call(this);
580
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
581
+ return __astTracker.propagateTaint(primitiveThis, [], result);
582
+ };
583
+
584
+ String.prototype.replace = function(searchValue, replaceValue) {
585
+ const result = __originals.String_replace.call(this, searchValue, replaceValue);
586
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
587
+ return __astTracker.propagateTaint(primitiveThis, [], result);
588
+ };
589
+
590
+ String.prototype.replaceAll = function(searchValue, replaceValue) {
591
+ const result = __originals.String_replaceAll.call(this, searchValue, replaceValue);
592
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
593
+ return __astTracker.propagateTaint(primitiveThis, [], result);
594
+ };
595
+
596
+ String.prototype.split = function(separator, limit) {
597
+ const result = __originals.String_split.call(this, separator, limit);
598
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
599
+ return __astTracker.propagateTaint(primitiveThis, [], result);
600
+ };
601
+
602
+ String.prototype.charAt = function(index) {
603
+ const result = __originals.String_charAt.call(this, index);
604
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
605
+ return __astTracker.propagateTaint(primitiveThis, [], result);
606
+ };
607
+
608
+ String.prototype.concat = function(...args) {
609
+ const result = __originals.String_concat.apply(this, args);
610
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
611
+ return __astTracker.propagateTaint(primitiveThis, args, result);
612
+ };
613
+
614
+ String.prototype.padStart = function(targetLength, padString) {
615
+ const result = __originals.String_padStart.call(this, targetLength, padString);
616
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
617
+ return __astTracker.propagateTaint(primitiveThis, [], result);
618
+ };
619
+
620
+ String.prototype.padEnd = function(targetLength, padString) {
621
+ const result = __originals.String_padEnd.call(this, targetLength, padString);
622
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
623
+ return __astTracker.propagateTaint(primitiveThis, [], result);
624
+ };
625
+
626
+ String.prototype.repeat = function(count) {
627
+ const result = __originals.String_repeat.call(this, count);
628
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
629
+ return __astTracker.propagateTaint(primitiveThis, [], result);
630
+ };
631
+
632
+ String.prototype.normalize = function(form) {
633
+ const result = __originals.String_normalize.call(this, form);
634
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
635
+ return __astTracker.propagateTaint(primitiveThis, [], result);
636
+ };
637
+
638
+ // Array method wrappers
639
+ Array.prototype.map = function(callback, thisArg) {
640
+ const result = __originals.Array_map.call(this, callback, thisArg);
641
+ return __astTracker.propagateTaint(this, [], result);
642
+ };
643
+
644
+ Array.prototype.filter = function(callback, thisArg) {
645
+ const result = __originals.Array_filter.call(this, callback, thisArg);
646
+ return __astTracker.propagateTaint(this, [], result);
647
+ };
648
+
649
+ Array.prototype.reduce = function(callback, initialValue) {
650
+ const result = arguments.length > 1
651
+ ? __originals.Array_reduce.call(this, callback, initialValue)
652
+ : __originals.Array_reduce.call(this, callback);
653
+ return __astTracker.propagateTaint(this, [], result);
654
+ };
655
+
656
+ Array.prototype.reduceRight = function(callback, initialValue) {
657
+ const result = arguments.length > 1
658
+ ? __originals.Array_reduceRight.call(this, callback, initialValue)
659
+ : __originals.Array_reduceRight.call(this, callback);
660
+ return __astTracker.propagateTaint(this, [], result);
661
+ };
662
+
663
+ Array.prototype.join = function(separator) {
664
+ const result = __originals.Array_join.call(this, separator);
665
+ return __astTracker.propagateTaint(this, [], result);
666
+ };
667
+
668
+ Array.prototype.slice = function(start, end) {
669
+ const result = __originals.Array_slice.call(this, start, end);
670
+ return __astTracker.propagateTaint(this, [], result);
671
+ };
672
+
673
+ Array.prototype.concat = function(...args) {
674
+ const result = __originals.Array_concat.apply(this, args);
675
+ return __astTracker.propagateTaint(this, args, result);
676
+ };
677
+
678
+ Array.prototype.flat = function(depth) {
679
+ const result = __originals.Array_flat.call(this, depth);
680
+ return __astTracker.propagateTaint(this, [], result);
681
+ };
682
+
683
+ Array.prototype.flatMap = function(callback, thisArg) {
684
+ const result = __originals.Array_flatMap.call(this, callback, thisArg);
685
+ return __astTracker.propagateTaint(this, [], result);
686
+ };
687
+
688
+ Array.prototype.find = function(callback, thisArg) {
689
+ const result = __originals.Array_find.call(this, callback, thisArg);
690
+ return __astTracker.propagateTaint(this, [], result);
691
+ };
692
+
693
+ Array.prototype.every = function(callback, thisArg) {
694
+ return __originals.Array_every.call(this, callback, thisArg);
695
+ };
696
+
697
+ Array.prototype.some = function(callback, thisArg) {
698
+ return __originals.Array_some.call(this, callback, thisArg);
699
+ };
700
+
701
+ // Number method wrappers - use this.valueOf() to get primitive for provenance check
702
+ Number.prototype.toFixed = function(digits) {
703
+ const result = __originals.Number_toFixed.call(this, digits);
704
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
705
+ return __astTracker.propagateTaint(primitiveThis, [], result);
706
+ };
707
+
708
+ Number.prototype.toPrecision = function(precision) {
709
+ const result = __originals.Number_toPrecision.call(this, precision);
710
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
711
+ return __astTracker.propagateTaint(primitiveThis, [], result);
712
+ };
713
+
714
+ Number.prototype.toExponential = function(fractionDigits) {
715
+ const result = __originals.Number_toExponential.call(this, fractionDigits);
716
+ const primitiveThis = typeof this === 'object' ? this.valueOf() : this;
717
+ return __astTracker.propagateTaint(primitiveThis, [], result);
718
+ };
719
+
720
+ // JSON wrappers
721
+ JSON.stringify = function(value, replacer, space) {
722
+ const result = __originals.JSON_stringify(value, replacer, space);
723
+ return __astTracker.propagateTaint(value, [], result);
724
+ };
725
+
726
+ JSON.parse = function(text, reviver) {
727
+ const result = __originals.JSON_parse(text, reviver);
728
+ // If input was tainted, taint the parsed object
729
+ if (!__inTrackingCall) {
730
+ __inTrackingCall = true;
731
+ try {
732
+ const inputMeta = __astTracker.checkProvenanceInternal(text);
733
+ if (inputMeta && result && typeof result === 'object') {
734
+ __astTracker.deepTaintObject(result, inputMeta);
735
+ }
736
+ } finally {
737
+ __inTrackingCall = false;
738
+ }
739
+ }
740
+ return result;
741
+ };
742
+
743
+ // Global function wrappers
744
+ const __OrigString = __originals.String_ctor;
745
+ globalThis.String = function(value) {
746
+ if (new.target) {
747
+ return new __OrigString(value);
748
+ }
749
+ const result = __OrigString(value);
750
+ return __astTracker.propagateTaint(value, [], result);
751
+ };
752
+ Object.setPrototypeOf(globalThis.String, __OrigString);
753
+ globalThis.String.prototype = __OrigString.prototype;
754
+ globalThis.String.fromCharCode = __OrigString.fromCharCode;
755
+ globalThis.String.fromCodePoint = __OrigString.fromCodePoint;
756
+ globalThis.String.raw = __OrigString.raw;
757
+
758
+ const __OrigNumber = __originals.Number_ctor;
759
+ globalThis.Number = function(value) {
760
+ if (new.target) {
761
+ return new __OrigNumber(value);
762
+ }
763
+ const result = __OrigNumber(value);
764
+ return __astTracker.propagateTaint(value, [], result);
765
+ };
766
+ Object.setPrototypeOf(globalThis.Number, __OrigNumber);
767
+ globalThis.Number.prototype = __OrigNumber.prototype;
768
+ globalThis.Number.isNaN = __OrigNumber.isNaN;
769
+ globalThis.Number.isFinite = __OrigNumber.isFinite;
770
+ globalThis.Number.isInteger = __OrigNumber.isInteger;
771
+ globalThis.Number.isSafeInteger = __OrigNumber.isSafeInteger;
772
+ globalThis.Number.parseFloat = __OrigNumber.parseFloat;
773
+ globalThis.Number.parseInt = __OrigNumber.parseInt;
774
+ globalThis.Number.MAX_VALUE = __OrigNumber.MAX_VALUE;
775
+ globalThis.Number.MIN_VALUE = __OrigNumber.MIN_VALUE;
776
+ globalThis.Number.NaN = __OrigNumber.NaN;
777
+ globalThis.Number.NEGATIVE_INFINITY = __OrigNumber.NEGATIVE_INFINITY;
778
+ globalThis.Number.POSITIVE_INFINITY = __OrigNumber.POSITIVE_INFINITY;
779
+ globalThis.Number.MAX_SAFE_INTEGER = __OrigNumber.MAX_SAFE_INTEGER;
780
+ globalThis.Number.MIN_SAFE_INTEGER = __OrigNumber.MIN_SAFE_INTEGER;
781
+ globalThis.Number.EPSILON = __OrigNumber.EPSILON;
782
+
783
+ globalThis.parseInt = function(string, radix) {
784
+ const result = __originals.parseInt_fn(string, radix);
785
+ return __astTracker.propagateTaint(string, [], result);
786
+ };
787
+
788
+ globalThis.parseFloat = function(string) {
789
+ const result = __originals.parseFloat_fn(string);
790
+ return __astTracker.propagateTaint(string, [], result);
791
+ };
792
+
793
+ // ============================================================================
794
+ // GLOBAL TRACKING FUNCTIONS
795
+ // ============================================================================
796
+
518
797
  globalThis.__track = (v, s, d) => __astTracker.track(v, s, d);
519
798
  globalThis.__track_binary = (l, r, o) => __astTracker.trackBinary(l, r, o);
520
799
  globalThis.__track_assign = (n, v) => __astTracker.trackAssign(n, v);
@@ -524,35 +803,30 @@ globalThis.__get_provenance = (v) => __astTracker.getMetadata(v);
524
803
  globalThis.__get_all_metadata = () => __astTracker.getAllMetadata();
525
804
  globalThis.__check_provenance = (v) => __astTracker.checkProvenance(v);
526
805
 
527
- // Mark a string literal as tainted (for cross-execution tracking)
528
806
  globalThis.__mark_tainted = (value) => {
529
- // Check if this value matches a hint by exact digest
530
- const digest = __astTracker.computeDigest(value);
531
- const hintMeta = __astTracker.hints.get(digest);
532
- if (hintMeta) {
533
- const taintedKey = 'tainted:' + String(value);
534
- __astTracker.metadata.set(taintedKey, hintMeta);
535
- return value;
536
- }
537
-
538
- // ALSO check if this value CONTAINS any hint values (substring match)
539
- // This enables cross-execution tracking for template literals/concatenation
540
- if (typeof value === 'string' && __astTracker.hintValues && __astTracker.hintValues.size > 0) {
541
- for (const [hintValue, metadata] of __astTracker.hintValues.entries()) {
542
- if (value.includes(hintValue)) {
543
- const taintedKey = 'tainted:' + String(value);
544
- // Ensure metadata has all required fields, use restricted readers for tool data
545
- const fullMetadata = {
546
- ...metadata,
547
- readers: metadata.readers || { type: 'restricted', readers: [] },
548
- dependencies: metadata.dependencies || metadata.deps || []
549
- };
550
- __astTracker.metadata.set(taintedKey, fullMetadata);
807
+ if (__inTrackingCall) return value;
808
+ __inTrackingCall = true;
809
+ try {
810
+ const digest = __astTracker.computeDigest(value);
811
+ if (digest) {
812
+ const hintMeta = __astTracker.hints.get(digest);
813
+ if (hintMeta) {
814
+ __astTracker.registerTaintedPrimitive(value, hintMeta);
551
815
  return value;
552
816
  }
553
817
  }
818
+
819
+ if (typeof value === 'string' && __astTracker.hintValues && __astTracker.hintValues.size > 0) {
820
+ for (const [hintValue, metadata] of __astTracker.hintValues.entries()) {
821
+ if (value.includes(hintValue)) {
822
+ __astTracker.registerTaintedPrimitive(value, metadata);
823
+ return value;
824
+ }
825
+ }
826
+ }
827
+ } finally {
828
+ __inTrackingCall = false;
554
829
  }
555
-
556
830
  return value;
557
831
  };
558
832
  `;