circle-ir 4.14.2 → 4.16.0

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 (168) hide show
  1. package/configs/sinks/golang.json +4 -4
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +11 -7
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/const-table-lookup.d.ts +29 -0
  6. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  7. package/dist/analysis/const-table-lookup.js +145 -0
  8. package/dist/analysis/const-table-lookup.js.map +1 -0
  9. package/dist/analysis/constant-caller-gate.d.ts +39 -0
  10. package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
  11. package/dist/analysis/constant-caller-gate.js +298 -0
  12. package/dist/analysis/constant-caller-gate.js.map +1 -0
  13. package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
  14. package/dist/analysis/constant-propagation/evaluator.js +3 -1
  15. package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
  16. package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
  17. package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/patterns.js +33 -0
  19. package/dist/analysis/constant-propagation/patterns.js.map +1 -1
  20. package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
  21. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  22. package/dist/analysis/constant-propagation/propagator.js +396 -49
  23. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  24. package/dist/analysis/constant-propagation/types.d.ts +9 -0
  25. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  26. package/dist/analysis/findings.d.ts.map +1 -1
  27. package/dist/analysis/findings.js +149 -4
  28. package/dist/analysis/findings.js.map +1 -1
  29. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  30. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  31. package/dist/analysis/go-strings-replacer.js +145 -0
  32. package/dist/analysis/go-strings-replacer.js.map +1 -0
  33. package/dist/analysis/interprocedural.d.ts.map +1 -1
  34. package/dist/analysis/interprocedural.js +148 -0
  35. package/dist/analysis/interprocedural.js.map +1 -1
  36. package/dist/analysis/java-array-element-keys.d.ts +14 -0
  37. package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
  38. package/dist/analysis/java-array-element-keys.js +36 -0
  39. package/dist/analysis/java-array-element-keys.js.map +1 -0
  40. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  41. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  42. package/dist/analysis/java-canonical-prefix.js +145 -0
  43. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  44. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  45. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  46. package/dist/analysis/java-crlf-strip.js +195 -0
  47. package/dist/analysis/java-crlf-strip.js.map +1 -0
  48. package/dist/analysis/js-crlf-strip.d.ts +26 -0
  49. package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
  50. package/dist/analysis/js-crlf-strip.js +244 -0
  51. package/dist/analysis/js-crlf-strip.js.map +1 -0
  52. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  53. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  54. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  55. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  56. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/cross-file-pass.js +43 -0
  58. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  59. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
  60. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  61. package/dist/analysis/passes/csharp-sanitizer-credit.js +127 -12
  62. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  63. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  64. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  65. package/dist/analysis/passes/interprocedural-pass.js +20 -1
  66. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  67. package/dist/analysis/passes/language-sources-pass.d.ts +76 -2
  68. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  69. package/dist/analysis/passes/language-sources-pass.js +2045 -156
  70. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  71. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  72. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  73. package/dist/analysis/passes/param-source-flow-gate-pass.js +214 -6
  74. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  75. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
  76. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
  77. package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
  78. package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
  79. package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
  80. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  81. package/dist/analysis/passes/scan-secrets-pass.js +7 -1
  82. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  83. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  84. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  85. package/dist/analysis/passes/sink-filter-pass.js +509 -27
  86. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  87. package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
  88. package/dist/analysis/passes/taint-matcher-pass.js +2 -2
  89. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
  90. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  91. package/dist/analysis/passes/taint-propagation-pass.js +1606 -60
  92. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  93. package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
  94. package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
  95. package/dist/analysis/passes/weak-hash-pass.js +191 -4
  96. package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
  97. package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
  98. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  99. package/dist/analysis/passes/weak-random-pass.js +30 -0
  100. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  101. package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
  102. package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
  103. package/dist/analysis/project-callee-escape-gate.js +243 -0
  104. package/dist/analysis/project-callee-escape-gate.js.map +1 -0
  105. package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
  106. package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
  107. package/dist/analysis/rust-literal-guard-rebind.js +197 -0
  108. package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
  109. package/dist/analysis/rust-literal-guard.d.ts +33 -0
  110. package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
  111. package/dist/analysis/rust-literal-guard.js +422 -0
  112. package/dist/analysis/rust-literal-guard.js.map +1 -0
  113. package/dist/analysis/scalar-derivation.d.ts +33 -0
  114. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  115. package/dist/analysis/scalar-derivation.js +368 -0
  116. package/dist/analysis/scalar-derivation.js.map +1 -0
  117. package/dist/analysis/taint-matcher.d.ts +2 -2
  118. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  119. package/dist/analysis/taint-matcher.js +695 -34
  120. package/dist/analysis/taint-matcher.js.map +1 -1
  121. package/dist/analysis/taint-propagation.d.ts +26 -1
  122. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  123. package/dist/analysis/taint-propagation.js +375 -24
  124. package/dist/analysis/taint-propagation.js.map +1 -1
  125. package/dist/analyzer.d.ts +16 -2
  126. package/dist/analyzer.d.ts.map +1 -1
  127. package/dist/analyzer.js +19 -2
  128. package/dist/analyzer.js.map +1 -1
  129. package/dist/browser/circle-ir.js +25539 -19200
  130. package/dist/core/circle-ir-core.cjs +3945 -2126
  131. package/dist/core/circle-ir-core.js +3945 -2126
  132. package/dist/core/extractors/calls.d.ts +12 -1
  133. package/dist/core/extractors/calls.d.ts.map +1 -1
  134. package/dist/core/extractors/calls.js +332 -22
  135. package/dist/core/extractors/calls.js.map +1 -1
  136. package/dist/core/extractors/dfg.js +336 -10
  137. package/dist/core/extractors/dfg.js.map +1 -1
  138. package/dist/core/extractors/types.d.ts +5 -1
  139. package/dist/core/extractors/types.d.ts.map +1 -1
  140. package/dist/core/extractors/types.js +62 -0
  141. package/dist/core/extractors/types.js.map +1 -1
  142. package/dist/graph/analysis-pass.d.ts +6 -1
  143. package/dist/graph/analysis-pass.d.ts.map +1 -1
  144. package/dist/graph/analysis-pass.js +2 -1
  145. package/dist/graph/analysis-pass.js.map +1 -1
  146. package/dist/graph/code-graph.d.ts +9 -0
  147. package/dist/graph/code-graph.d.ts.map +1 -1
  148. package/dist/graph/code-graph.js +11 -0
  149. package/dist/graph/code-graph.js.map +1 -1
  150. package/dist/navigation/navigation-index.d.ts +134 -14
  151. package/dist/navigation/navigation-index.d.ts.map +1 -1
  152. package/dist/navigation/navigation-index.js +1158 -104
  153. package/dist/navigation/navigation-index.js.map +1 -1
  154. package/dist/navigation/types.d.ts +59 -2
  155. package/dist/navigation/types.d.ts.map +1 -1
  156. package/dist/resolution/cross-file.d.ts +16 -0
  157. package/dist/resolution/cross-file.d.ts.map +1 -1
  158. package/dist/resolution/cross-file.js +110 -42
  159. package/dist/resolution/cross-file.js.map +1 -1
  160. package/dist/resolution/type-hierarchy.d.ts +3 -0
  161. package/dist/resolution/type-hierarchy.d.ts.map +1 -1
  162. package/dist/resolution/type-hierarchy.js +45 -1
  163. package/dist/resolution/type-hierarchy.js.map +1 -1
  164. package/dist/types/index.d.ts +33 -0
  165. package/dist/types/index.d.ts.map +1 -1
  166. package/dist/types/index.js.map +1 -1
  167. package/dist/wasm/web-tree-sitter.wasm +0 -0
  168. package/package.json +3 -3
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { isKnown, createUnknown, getNodeText, getNodeLine } from './ast-utils.js';
8
8
  import { ExpressionEvaluator } from './evaluator.js';
9
- import { TAINT_PATTERN_REGEX, SANITIZER_METHODS, PROPAGATOR_METHODS, ANTI_SANITIZER_METHODS } from './patterns.js';
9
+ import { TAINT_PATTERN_REGEX, SANITIZER_METHODS, PROPAGATOR_METHODS, ANTI_SANITIZER_METHODS, HTML_CONTEXT_ENCODER_METHODS, HTML_CONTEXT_ENCODER_UNCOVERED_SINKS, } from './patterns.js';
10
+ import { javaArrayElementStorageKey } from '../java-array-element-keys.js';
10
11
  /**
11
12
  * Constant Propagator for taint analysis.
12
13
  *
@@ -23,6 +24,8 @@ export class ConstantPropagator {
23
24
  taintedCollections = new Map();
24
25
  // Track variables explicitly assigned from sanitizer calls
25
26
  sanitizedVars = new Set();
27
+ /** #589: per sanitized variable, the sink types its context-specific encoder does NOT clean. */
28
+ sanitizedVarUncovered = new Map();
26
29
  source = '';
27
30
  evaluator;
28
31
  // Track the expression node that defined each variable (for refinement)
@@ -33,10 +36,23 @@ export class ConstantPropagator {
33
36
  methodReturnsConstant = new Set();
34
37
  // Track which methods always return sanitized values (inter-procedural analysis)
35
38
  methodReturnsSanitized = new Set();
39
+ /** #581: same-file helpers named like a class-less sanitizer but not actually sanitizing. */
40
+ localNonSanitizing = new Set();
41
+ /**
42
+ * #589: same-file helpers whose every reachable return is a direct call to a
43
+ * context-specific HTML encoder (`return StringEscapeUtils.escapeHtml(v);`).
44
+ * Such a helper cleans `xss` like the encoder it wraps, but not `trust_boundary`.
45
+ */
46
+ methodReturnsHtmlEncoderOnly = new Set();
47
+ methodNotHtmlEncoderOnly = new Set();
36
48
  // Track which methods return a specific parameter (index) - for taint propagation
37
49
  methodReturnsParameter = new Map();
38
50
  // Track which methods return safe (non-tainted) values even with tainted input
39
51
  methodReturnsSafeValue = new Set();
52
+ /** Go locals (method-scoped) that were ever assigned a value const-prop cannot prove clean (#751). */
53
+ goUnknownVars = new Set();
54
+ /** Every method-scoped name handleGoAssignment wrote; these get no unscoped alias in the result. */
55
+ goScopedNames = new Set();
40
56
  // Additional taint patterns (for test harnesses to inject custom patterns)
41
57
  additionalTaintPatterns = [];
42
58
  // Track list elements by index for precise list taint tracking
@@ -103,13 +119,19 @@ export class ConstantPropagator {
103
119
  this.definitionNodes.clear();
104
120
  this.inConditionalBranch = false;
105
121
  this.methodReturnsConstant.clear();
122
+ this.goUnknownVars.clear();
123
+ this.goScopedNames.clear();
106
124
  this.methodReturnsSanitized.clear();
125
+ this.localNonSanitizing.clear();
126
+ this.methodReturnsHtmlEncoderOnly.clear();
127
+ this.methodNotHtmlEncoderOnly.clear();
107
128
  this.methodReturnsParameter.clear();
108
129
  this.methodReturnsSafeValue.clear();
109
130
  this.listElements.clear();
110
131
  this.loopVariables.clear();
111
132
  this.taintedArrayElements.clear();
112
133
  this.sanitizedVars.clear();
134
+ this.sanitizedVarUncovered.clear();
113
135
  this.currentMethod = null;
114
136
  this.conditionalTaints.clear();
115
137
  this.conditionStack = [];
@@ -162,6 +184,11 @@ export class ConstantPropagator {
162
184
  resultTainted.add(unscoped);
163
185
  }
164
186
  }
187
+ // #589 review: do NOT copy sanitizedVarUncovered onto the bare name.
188
+ // The unscoped sanitizedVars alias stays for legacy readers, but uncovered
189
+ // sink types are decided from the scoped `method:var` key only — otherwise
190
+ // a sibling method's local of the same name inherits the encoder set.
191
+ const resultScopes = new Map(this.sanitizedVarUncovered);
165
192
  for (const name of this.sanitizedVars) {
166
193
  if (name.includes(':')) {
167
194
  const unscoped = name.substring(name.indexOf(':') + 1);
@@ -171,6 +198,11 @@ export class ConstantPropagator {
171
198
  // Add unscoped symbols for backward compatibility with tests
172
199
  // The scoped versions take priority in filterCleanVariableSinks
173
200
  for (const [name, value] of this.symbols) {
201
+ // cognium-dev#751: a Go local is only meaningful in its own function. An
202
+ // unscoped alias would let `query := "SELECT 1"` in one function make
203
+ // every other function's `query` look clean to the veto.
204
+ if (this.goScopedNames.has(name))
205
+ continue;
174
206
  if (name.includes(':')) {
175
207
  const unscoped = name.substring(name.indexOf(':') + 1);
176
208
  // Only add if not already present (scoped version wins on conflict)
@@ -186,10 +218,12 @@ export class ConstantPropagator {
186
218
  taintedCollections: new Map(this.taintedCollections),
187
219
  taintedArrayElements: new Map(this.taintedArrayElements),
188
220
  sanitizedVars: resultSanitized,
221
+ sanitizedVarUncovered: resultScopes,
189
222
  conditionalTaints: new Map(this.conditionalTaints),
190
223
  lineConditions: new Map(this.lineConditions),
191
224
  synchronizedLines: new Set(this.synchronizedLines),
192
225
  instanceFieldTaint: new Map(this.instanceFieldTaint),
226
+ constantReturningMethods: new Set(this.methodReturnsConstant),
193
227
  };
194
228
  }
195
229
  /**
@@ -259,6 +293,7 @@ export class ConstantPropagator {
259
293
  let allReturnsConstant = true;
260
294
  let allReturnsSanitized = true;
261
295
  let allReturnsSafe = true; // Track if all returns are non-tainted
296
+ let allReturnsHtmlEncoder = true; // #589: every return is a direct HTML-encoder call
262
297
  let returnedParamIndex = -1;
263
298
  let hasReachableReturn = false;
264
299
  for (const ret of returns) {
@@ -267,13 +302,23 @@ export class ConstantPropagator {
267
302
  continue;
268
303
  }
269
304
  hasReachableReturn = true;
270
- const valueNode = ret.children.find(c => c.type !== 'return' && c.type !== ';' && c.type !== 'comment');
305
+ const valueNode = this.returnExpressionValueNode(ret);
271
306
  if (!valueNode) {
272
307
  allReturnsConstant = false;
273
308
  allReturnsSanitized = false;
309
+ allReturnsHtmlEncoder = false;
274
310
  allReturnsSafe = false; // void return or unknown - not safe
275
311
  continue;
276
312
  }
313
+ // #589: `return <Encoder>.<encode>(<the only parameter>)`.
314
+ const encoderArgs = valueNode.type === 'method_invocation'
315
+ ? valueNode.childForFieldName('arguments')?.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',')
316
+ : undefined;
317
+ if (!(valueNode.type === 'method_invocation' &&
318
+ HTML_CONTEXT_ENCODER_METHODS.has(getNodeText(valueNode.childForFieldName('name') ?? valueNode, this.source)) &&
319
+ params.length === 1 && encoderArgs?.length === 1 && getNodeText(encoderArgs[0], this.source) === params[0])) {
320
+ allReturnsHtmlEncoder = false;
321
+ }
277
322
  const value = tempPropagator.evaluateExpression(valueNode);
278
323
  if (!isKnown(value)) {
279
324
  allReturnsConstant = false;
@@ -310,12 +355,25 @@ export class ConstantPropagator {
310
355
  }
311
356
  }
312
357
  if (hasReachableReturn) {
358
+ // #589: an encoder-only helper is recorded only when no other method of
359
+ // this name (an overload, an annotated sanitizer) returns anything else.
360
+ if (allReturnsHtmlEncoder && allReturnsSanitized && !this.methodNotHtmlEncoderOnly.has(methodName) &&
361
+ (!this.methodReturnsSanitized.has(methodName) || this.methodReturnsHtmlEncoderOnly.has(methodName))) {
362
+ this.methodReturnsHtmlEncoderOnly.add(methodName);
363
+ }
364
+ else {
365
+ this.methodNotHtmlEncoderOnly.add(methodName);
366
+ this.methodReturnsHtmlEncoderOnly.delete(methodName);
367
+ }
313
368
  if (allReturnsConstant) {
314
369
  this.methodReturnsConstant.add(methodName);
315
370
  }
316
371
  if (allReturnsSanitized) {
317
372
  this.methodReturnsSanitized.add(methodName);
318
373
  }
374
+ if (hasReachableReturn && SANITIZER_METHODS.has(methodName) && !allReturnsSanitized) {
375
+ this.localNonSanitizing.add(methodName);
376
+ }
319
377
  if (returnedParamIndex >= 0) {
320
378
  this.methodReturnsParameter.set(methodName, returnedParamIndex);
321
379
  }
@@ -431,6 +489,18 @@ export class ConstantPropagator {
431
489
  }
432
490
  return null;
433
491
  }
492
+ /** Return the expression node from a `return_statement` (Java, Go, Python, …). */
493
+ returnExpressionValueNode(ret) {
494
+ const byField = ret.childForFieldName('result');
495
+ if (byField)
496
+ return byField;
497
+ const candidates = ret.children.filter((c) => c.type !== 'return' && c.type !== ';' && c.type !== 'comment');
498
+ let valueNode = candidates[0];
499
+ if (valueNode?.type === 'expression_list') {
500
+ valueNode = valueNode.children.find((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
501
+ }
502
+ return valueNode;
503
+ }
434
504
  getMethodParameters(method) {
435
505
  const params = [];
436
506
  const paramsNode = method.childForFieldName('parameters');
@@ -443,6 +513,20 @@ export class ConstantPropagator {
443
513
  params.push(getNodeText(nameNode, this.source));
444
514
  }
445
515
  }
516
+ else if (child.type === 'parameter_declaration') {
517
+ const nameNode = child.childForFieldName('name');
518
+ if (nameNode) {
519
+ if (nameNode.type === 'identifier') {
520
+ params.push(getNodeText(nameNode, this.source));
521
+ }
522
+ else {
523
+ for (const id of nameNode.children) {
524
+ if (id.type === 'identifier')
525
+ params.push(getNodeText(id, this.source));
526
+ }
527
+ }
528
+ }
529
+ }
446
530
  }
447
531
  return params;
448
532
  }
@@ -451,7 +535,7 @@ export class ConstantPropagator {
451
535
  const nameNode = node.childForFieldName('name');
452
536
  if (nameNode) {
453
537
  const methodName = getNodeText(nameNode, this.source);
454
- if (SANITIZER_METHODS.has(methodName)) {
538
+ if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
455
539
  return true;
456
540
  }
457
541
  }
@@ -832,6 +916,7 @@ export class ConstantPropagator {
832
916
  }
833
917
  switch (node.type) {
834
918
  case 'method_declaration':
919
+ case 'function_declaration':
835
920
  case 'constructor_declaration':
836
921
  this.handleMethodDeclaration(node);
837
922
  return true; // Don't visit children directly, handleMethodDeclaration does it
@@ -859,6 +944,10 @@ export class ConstantPropagator {
859
944
  case 'assignment_expression':
860
945
  this.handleAssignment(node);
861
946
  return false;
947
+ case 'short_var_declaration':
948
+ case 'assignment_statement':
949
+ this.handleGoAssignment(node);
950
+ return false;
862
951
  case 'update_expression':
863
952
  this.handleUpdateExpression(node);
864
953
  return false;
@@ -884,6 +973,10 @@ export class ConstantPropagator {
884
973
  case 'synchronized_statement':
885
974
  this.handleSynchronizedStatement(node);
886
975
  return true;
976
+ case 'try_statement':
977
+ case 'try_with_resources_statement':
978
+ this.handleTryStatement(node);
979
+ return true;
887
980
  default:
888
981
  return false;
889
982
  }
@@ -1093,6 +1186,59 @@ export class ConstantPropagator {
1093
1186
  this.collectLoopVariableNames(child, names);
1094
1187
  }
1095
1188
  }
1189
+ /**
1190
+ * cognium-dev#753 — try/catch must not let a catch-arm literal assignment
1191
+ * erase taint established on the normal-completion path through the try body
1192
+ * (`try { label = format(picked); } catch (…) { label = ""; }` then
1193
+ * `exec(tool + label)`).
1194
+ */
1195
+ handleTryStatement(node) {
1196
+ const body = node.childForFieldName('body');
1197
+ const catches = node.children.filter((c) => c.type === 'catch_clause');
1198
+ const finallyClause = node.children.find((c) => c.type === 'finally_clause');
1199
+ const taintedBefore = new Set(this.tainted);
1200
+ const symbolsBefore = new Map(this.symbols);
1201
+ if (body)
1202
+ this.visit(body);
1203
+ const taintedAfterTry = new Set(this.tainted);
1204
+ const symbolsAfterTry = new Map(this.symbols);
1205
+ this.tainted = new Set(taintedBefore);
1206
+ this.symbols = new Map(symbolsBefore);
1207
+ for (const c of catches)
1208
+ this.visit(c);
1209
+ const taintedAfterCatch = new Set(this.tainted);
1210
+ const symbolsAfterCatch = new Map(this.symbols);
1211
+ this.tainted = new Set([...taintedBefore, ...taintedAfterTry, ...taintedAfterCatch]);
1212
+ this.symbols = new Map(symbolsBefore);
1213
+ const keys = new Set([...symbolsAfterTry.keys(), ...symbolsAfterCatch.keys()]);
1214
+ for (const key of keys) {
1215
+ const merged = this.mergeTryCatchSymbol(symbolsAfterTry.get(key), symbolsAfterCatch.get(key));
1216
+ if (merged)
1217
+ this.symbols.set(key, merged);
1218
+ }
1219
+ if (finallyClause)
1220
+ this.visit(finallyClause);
1221
+ }
1222
+ /** Prefer unknown / non-literal over a definite literal when paths disagree. */
1223
+ mergeTryCatchSymbol(tryVal, catchVal) {
1224
+ if (!tryVal && !catchVal)
1225
+ return undefined;
1226
+ if (!tryVal)
1227
+ return catchVal;
1228
+ if (!catchVal)
1229
+ return tryVal;
1230
+ if (!isKnown(tryVal) || tryVal.type === 'unknown')
1231
+ return tryVal;
1232
+ if (!isKnown(catchVal) || catchVal.type === 'unknown')
1233
+ return catchVal;
1234
+ if (tryVal.type !== catchVal.type) {
1235
+ return createUnknown(Math.max(tryVal.sourceLine ?? 0, catchVal.sourceLine ?? 0));
1236
+ }
1237
+ if (tryVal.type === 'string' && catchVal.type === 'string' && tryVal.value !== catchVal.value) {
1238
+ return createUnknown(Math.max(tryVal.sourceLine ?? 0, catchVal.sourceLine ?? 0));
1239
+ }
1240
+ return tryVal;
1241
+ }
1096
1242
  /**
1097
1243
  * Handle synchronized statements.
1098
1244
  * Operations inside synchronized blocks are atomic, so field strong updates are safe.
@@ -1170,6 +1316,7 @@ export class ConstantPropagator {
1170
1316
  if (isTainted) {
1171
1317
  this.tainted.add(scopedName);
1172
1318
  this.sanitizedVars.delete(scopedName); // No longer sanitized if receiving tainted value
1319
+ this.sanitizedVarUncovered.delete(scopedName);
1173
1320
  this.definitionNodes.set(scopedName, valueNode);
1174
1321
  this.symbols.set(scopedName, createUnknown(line));
1175
1322
  }
@@ -1182,15 +1329,23 @@ export class ConstantPropagator {
1182
1329
  this.definitionNodes.delete(scopedName);
1183
1330
  const value = this.evaluateExpression(valueNode);
1184
1331
  this.symbols.set(scopedName, value);
1185
- // Track if this variable was explicitly assigned from a sanitizer call
1332
+ // Track if this variable was explicitly assigned from a sanitizer call.
1333
+ // A constant / non-sanitizer overwrite must clear any prior encoder
1334
+ // scope (#589 review: `data = escapeHtml(x); data = "safe"`).
1186
1335
  if (this.isSanitizerMethodCall(valueNode)) {
1187
1336
  this.sanitizedVars.add(scopedName);
1337
+ this.recordSanitizerScope(scopedName, valueNode);
1338
+ }
1339
+ else {
1340
+ this.sanitizedVars.delete(scopedName);
1341
+ this.sanitizedVarUncovered.delete(scopedName);
1188
1342
  }
1189
1343
  // Check if this is an anti-sanitizer call that reintroduces taint
1190
1344
  // e.g., URLDecoder.decode(sanitizedVar) produces tainted output
1191
1345
  if (this.antiSanitizerReintroducesTaint(valueNode)) {
1192
1346
  this.tainted.add(scopedName);
1193
1347
  this.sanitizedVars.delete(scopedName);
1348
+ this.sanitizedVarUncovered.delete(scopedName);
1194
1349
  }
1195
1350
  }
1196
1351
  }
@@ -1201,6 +1356,101 @@ export class ConstantPropagator {
1201
1356
  }
1202
1357
  }
1203
1358
  }
1359
+ /**
1360
+ * Go `:=` / `=` (cognium-dev#751).
1361
+ *
1362
+ * Const-prop does not model Go request sources, so a right-hand side it does
1363
+ * not see as tainted is usually just *unknown* (`r.URL.Query().Get("f")`,
1364
+ * `"SELECT " + f`, `r.FormValue("x")`). Recording such a variable as a clean
1365
+ * symbol makes the const-prop veto drop real flows downstream, so only a
1366
+ * right-hand side that is provably clean — a constant, or a call to a local
1367
+ * helper that always returns a constant — clears the variable. Anything else stops being tracked here, which is what Go
1368
+ * assignments got before this handler existed: the DFG decides.
1369
+ */
1370
+ handleGoAssignment(node) {
1371
+ const left = node.childForFieldName('left');
1372
+ const right = node.childForFieldName('right');
1373
+ if (!left || !right)
1374
+ return;
1375
+ const names = [];
1376
+ if (left.type === 'identifier') {
1377
+ names.push(getNodeText(left, this.source));
1378
+ }
1379
+ else if (left.type === 'expression_list') {
1380
+ for (const c of left.children) {
1381
+ if (c.type === 'identifier')
1382
+ names.push(getNodeText(c, this.source));
1383
+ }
1384
+ }
1385
+ if (names.length === 0)
1386
+ return;
1387
+ // The Go grammar wraps every right-hand side in an `expression_list`, so
1388
+ // pair each name with its own expression (`a, b := x, y`); one expression
1389
+ // for several names (`v, err := f()`) applies to all of them.
1390
+ const rhs = right.type === 'expression_list'
1391
+ ? right.children.filter((c) => c.type !== ',' && c.type !== 'comment')
1392
+ : [right];
1393
+ const line = getNodeLine(node);
1394
+ for (let i = 0; i < names.length; i++) {
1395
+ const varName = names[i];
1396
+ if (varName === '_')
1397
+ continue;
1398
+ const value = rhs.length === names.length ? rhs[i] : rhs.length === 1 ? rhs[0] : undefined;
1399
+ const scopedName = this.getScopedName(varName);
1400
+ this.goScopedNames.add(scopedName);
1401
+ if (value && this.isTaintedExpression(value)) {
1402
+ this.goUnknownVars.add(scopedName);
1403
+ this.tainted.add(scopedName);
1404
+ this.sanitizedVars.delete(scopedName);
1405
+ this.sanitizedVarUncovered.delete(scopedName);
1406
+ this.definitionNodes.set(scopedName, value);
1407
+ this.symbols.set(scopedName, createUnknown(line));
1408
+ continue;
1409
+ }
1410
+ if (!value || !this.isGoKnownCleanRhs(value) || this.goUnknownVars.has(scopedName)) {
1411
+ // Unknown value: forget what we knew, and do not trust a later clean
1412
+ // assignment either, since const-prop's symbols are not flow-sensitive
1413
+ // and a sink between the two would be vetoed. In a conditional branch
1414
+ // the other path may still carry the old value, so keep any taint.
1415
+ this.goUnknownVars.add(scopedName);
1416
+ this.symbols.delete(scopedName);
1417
+ this.sanitizedVars.delete(scopedName);
1418
+ this.sanitizedVarUncovered.delete(scopedName);
1419
+ if (!this.inConditionalBranch) {
1420
+ this.tainted.delete(scopedName);
1421
+ this.definitionNodes.delete(scopedName);
1422
+ }
1423
+ continue;
1424
+ }
1425
+ // Provably clean, but only on this path: a branch must not clear taint
1426
+ // or make an untracked variable look clean at the join.
1427
+ if (this.inConditionalBranch) {
1428
+ this.goUnknownVars.add(scopedName);
1429
+ this.symbols.delete(scopedName);
1430
+ continue;
1431
+ }
1432
+ this.tainted.delete(scopedName);
1433
+ this.definitionNodes.delete(scopedName);
1434
+ this.symbols.set(scopedName, this.evaluateExpression(value));
1435
+ this.sanitizedVars.delete(scopedName);
1436
+ this.sanitizedVarUncovered.delete(scopedName);
1437
+ }
1438
+ }
1439
+ /**
1440
+ * A Go right-hand side whose value is clean regardless of its inputs: a
1441
+ * compile-time constant, or a call to a local helper whose every reachable
1442
+ * return is a constant (#751). Sanitizer-style calls are deliberately not
1443
+ * included: clearing here vetoes every sink type, and Go sanitizers are
1444
+ * sink-specific (`strconv.Atoi` bounds nothing for a `make` size, #349).
1445
+ */
1446
+ isGoKnownCleanRhs(right) {
1447
+ if (isKnown(this.evaluateExpression(right)))
1448
+ return true;
1449
+ if (right.type !== 'call_expression')
1450
+ return false;
1451
+ const callee = this.extractCallName(right);
1452
+ return callee !== null && this.methodReturnsConstant.has(callee);
1453
+ }
1204
1454
  handleAssignment(node) {
1205
1455
  const left = node.childForFieldName('left');
1206
1456
  const right = node.childForFieldName('right');
@@ -1225,6 +1475,7 @@ export class ConstantPropagator {
1225
1475
  if (this.isTaintedExpression(right)) {
1226
1476
  this.tainted.add(scopedName);
1227
1477
  this.sanitizedVars.delete(scopedName); // No longer sanitized if receiving tainted value
1478
+ this.sanitizedVarUncovered.delete(scopedName);
1228
1479
  this.definitionNodes.set(scopedName, right);
1229
1480
  this.symbols.set(scopedName, createUnknown(line));
1230
1481
  // Track constructor field assignments: this.field = taintedParam
@@ -1269,9 +1520,15 @@ export class ConstantPropagator {
1269
1520
  this.definitionNodes.delete(scopedName);
1270
1521
  const value = this.evaluateExpression(right);
1271
1522
  this.symbols.set(scopedName, value);
1272
- // Track if this variable was explicitly assigned from a sanitizer call
1523
+ // Track if this variable was explicitly assigned from a sanitizer call.
1524
+ // Constant / non-sanitizer overwrite clears prior encoder scope (#589).
1273
1525
  if (this.isSanitizerMethodCall(right)) {
1274
1526
  this.sanitizedVars.add(scopedName);
1527
+ this.recordSanitizerScope(scopedName, right);
1528
+ }
1529
+ else {
1530
+ this.sanitizedVars.delete(scopedName);
1531
+ this.sanitizedVarUncovered.delete(scopedName);
1275
1532
  }
1276
1533
  }
1277
1534
  // Check if this is an anti-sanitizer call that reintroduces taint
@@ -1279,17 +1536,24 @@ export class ConstantPropagator {
1279
1536
  if (this.antiSanitizerReintroducesTaint(right)) {
1280
1537
  this.tainted.add(scopedName);
1281
1538
  this.sanitizedVars.delete(scopedName);
1539
+ this.sanitizedVarUncovered.delete(scopedName);
1282
1540
  }
1283
1541
  }
1284
1542
  }
1285
- handleArrayElementAssignment(left, right, node) {
1286
- // Extract array name and index from array[index]
1287
- const arrayNode = left.childForFieldName('array') || left.child(0);
1288
- const indexNode = left.childForFieldName('index') || left.child(2);
1289
- if (!arrayNode)
1543
+ handleArrayElementAssignment(left, right, _node) {
1544
+ const chain = this.parseArrayAccessChain(left);
1545
+ if (!chain)
1290
1546
  return;
1291
- const arrayName = getNodeText(arrayNode, this.source);
1292
- // Determine the index key (numeric or '*' for unknown)
1547
+ const { arrayName, indexKey } = this.arrayElementStorageKey(chain);
1548
+ const isTainted = this.isTaintedExpression(right);
1549
+ this.setArrayElementTaint(arrayName, indexKey, isTainted);
1550
+ }
1551
+ /** cognium-dev#476: `grid[0][0]` and deeper element paths. */
1552
+ parseArrayAccessChain(node) {
1553
+ if (node.type !== 'array_access' && node.type !== 'subscript_expression') {
1554
+ return null;
1555
+ }
1556
+ const indexNode = node.childForFieldName('index') || node.child(2);
1293
1557
  let indexKey = '*';
1294
1558
  if (indexNode) {
1295
1559
  const indexValue = this.evaluateExpression(indexNode);
@@ -1297,24 +1561,54 @@ export class ConstantPropagator {
1297
1561
  indexKey = String(indexValue.value);
1298
1562
  }
1299
1563
  }
1300
- const isTainted = this.isTaintedExpression(right);
1564
+ const arrayNode = node.childForFieldName('array') || node.child(0);
1565
+ if (!arrayNode)
1566
+ return null;
1567
+ if (arrayNode.type === 'identifier') {
1568
+ return { base: getNodeText(arrayNode, this.source), indices: [indexKey] };
1569
+ }
1570
+ if (arrayNode.type === 'array_access' || arrayNode.type === 'subscript_expression') {
1571
+ const inner = this.parseArrayAccessChain(arrayNode);
1572
+ if (!inner)
1573
+ return null;
1574
+ return { base: inner.base, indices: [...inner.indices, indexKey] };
1575
+ }
1576
+ return null;
1577
+ }
1578
+ arrayElementStorageKey(chain) {
1579
+ return javaArrayElementStorageKey(chain.base, chain.indices);
1580
+ }
1581
+ setArrayElementTaint(arrayName, indexKey, isTainted) {
1301
1582
  if (isTainted) {
1302
- // Mark this array element as tainted
1303
1583
  if (!this.taintedArrayElements.has(arrayName)) {
1304
1584
  this.taintedArrayElements.set(arrayName, new Set());
1305
1585
  }
1306
1586
  this.taintedArrayElements.get(arrayName).add(indexKey);
1307
1587
  }
1308
1588
  else {
1309
- // Mark this array element as clean (remove from tainted set)
1310
1589
  const taintedIndices = this.taintedArrayElements.get(arrayName);
1311
1590
  if (taintedIndices) {
1312
1591
  taintedIndices.delete(indexKey);
1313
- // If we're assigning to a specific index and '*' is in the set,
1314
- // we can't remove '*' because other indices might still be tainted
1315
1592
  }
1316
1593
  }
1317
1594
  }
1595
+ isArrayElementAccessTainted(chain) {
1596
+ if (this.tainted.has(chain.base)) {
1597
+ return true;
1598
+ }
1599
+ const { arrayName, indexKey } = this.arrayElementStorageKey(chain);
1600
+ const taintedIndices = this.taintedArrayElements.get(arrayName);
1601
+ if (!taintedIndices) {
1602
+ return false;
1603
+ }
1604
+ if (taintedIndices.has('*')) {
1605
+ return true;
1606
+ }
1607
+ if (indexKey === '*') {
1608
+ return taintedIndices.size > 0;
1609
+ }
1610
+ return taintedIndices.has(indexKey);
1611
+ }
1318
1612
  handleUpdateExpression(node) {
1319
1613
  // Handle x++, ++x, x--, --x
1320
1614
  // The operand is a positional child (identifier), not a named field
@@ -1456,10 +1750,12 @@ export class ConstantPropagator {
1456
1750
  if (guardedVar) {
1457
1751
  this.tainted.delete(guardedVar);
1458
1752
  this.sanitizedVars.add(guardedVar);
1753
+ this.sanitizedVarUncovered.delete(guardedVar);
1459
1754
  const scoped = this.getScopedName(guardedVar);
1460
1755
  if (scoped !== guardedVar) {
1461
1756
  this.tainted.delete(scoped);
1462
1757
  this.sanitizedVars.add(scoped);
1758
+ this.sanitizedVarUncovered.delete(scoped);
1463
1759
  }
1464
1760
  }
1465
1761
  }
@@ -1682,6 +1978,7 @@ export class ConstantPropagator {
1682
1978
  for (const child of node.children) {
1683
1979
  if (child.type === 'method_invocation') {
1684
1980
  this.checkCollectionTaint(child);
1981
+ this.handleSystemArrayCopy(child);
1685
1982
  }
1686
1983
  }
1687
1984
  for (const child of node.children) {
@@ -1772,11 +2069,27 @@ export class ConstantPropagator {
1772
2069
  * Handles Java (`method_invocation`), Go/JS/TS (`call_expression`), and
1773
2070
  * Python (`call`) AST shapes.
1774
2071
  */
2072
+ /**
2073
+ * #589: remember which sink types the sanitizer behind `scopedName` does NOT
2074
+ * clean. Only context-specific HTML/markup encoders carry an entry; any other
2075
+ * sanitizer clears a previously recorded one (the variable was reassigned).
2076
+ */
2077
+ recordSanitizerScope(scopedName, node) {
2078
+ const methodName = this.extractCallName(node);
2079
+ if (methodName && ((HTML_CONTEXT_ENCODER_METHODS.has(methodName) && !this.methodReturnsSanitized.has(methodName)) ||
2080
+ this.methodReturnsHtmlEncoderOnly.has(methodName))) {
2081
+ this.sanitizedVarUncovered.set(scopedName, HTML_CONTEXT_ENCODER_UNCOVERED_SINKS);
2082
+ }
2083
+ else {
2084
+ this.sanitizedVarUncovered.delete(scopedName);
2085
+ }
2086
+ }
1775
2087
  isSanitizerMethodCall(node) {
1776
2088
  const methodName = this.extractCallName(node);
1777
2089
  if (!methodName)
1778
2090
  return false;
1779
- return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
2091
+ return (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName))
2092
+ || this.methodReturnsSanitized.has(methodName);
1780
2093
  }
1781
2094
  /**
1782
2095
  * Extract the trailing method/function name from any call node shape:
@@ -2013,7 +2326,7 @@ export class ConstantPropagator {
2013
2326
  const objectNode = node.childForFieldName('object');
2014
2327
  if (nameNode) {
2015
2328
  const methodName = getNodeText(nameNode, this.source);
2016
- if (SANITIZER_METHODS.has(methodName)) {
2329
+ if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
2017
2330
  return false;
2018
2331
  }
2019
2332
  if (this.methodReturnsConstant.has(methodName)) {
@@ -2177,43 +2490,50 @@ export class ConstantPropagator {
2177
2490
  }
2178
2491
  }
2179
2492
  }
2180
- if (node.type === 'array_access' || node.type === 'subscript_expression') {
2181
- const arrayNode = node.childForFieldName('array') || node.child(0);
2182
- const indexNode = node.childForFieldName('index') || node.child(2);
2183
- if (arrayNode) {
2184
- const arrayName = getNodeText(arrayNode, this.source);
2185
- // Check if the whole array is tainted
2186
- if (arrayNode.type === 'identifier' && this.tainted.has(arrayName)) {
2187
- return true;
2493
+ if (node.type === 'call_expression' || node.type === 'call') {
2494
+ const methodName = this.extractCallName(node);
2495
+ if (methodName) {
2496
+ if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
2497
+ return false;
2188
2498
  }
2189
- // Check element-level taint tracking
2190
- const taintedIndices = this.taintedArrayElements.get(arrayName);
2191
- if (taintedIndices) {
2192
- // If '*' is in the set, the whole array has tainted elements
2193
- if (taintedIndices.has('*')) {
2194
- return true;
2195
- }
2196
- // Check specific index
2197
- if (indexNode) {
2198
- const indexValue = this.evaluateExpression(indexNode);
2199
- if (isKnown(indexValue) && (indexValue.type === 'int' || indexValue.type === 'string')) {
2200
- const indexKey = String(indexValue.value);
2201
- if (taintedIndices.has(indexKey)) {
2202
- return true;
2203
- }
2204
- // Specific index is NOT tainted - return false for this access
2205
- return false;
2206
- }
2207
- // Unknown index - check if ANY element is tainted
2208
- if (taintedIndices.size > 0) {
2499
+ if (this.methodReturnsConstant.has(methodName)) {
2500
+ return false;
2501
+ }
2502
+ if (this.methodReturnsSanitized.has(methodName)) {
2503
+ return false;
2504
+ }
2505
+ if (this.methodReturnsSafeValue.has(methodName)) {
2506
+ return false;
2507
+ }
2508
+ const returnedParamIdx = this.methodReturnsParameter.get(methodName);
2509
+ if (returnedParamIdx !== undefined && returnedParamIdx >= 0) {
2510
+ const argsNode = node.childForFieldName('arguments');
2511
+ if (argsNode) {
2512
+ const args = argsNode.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
2513
+ if (args.length > returnedParamIdx && this.isTaintedExpression(args[returnedParamIdx])) {
2209
2514
  return true;
2210
2515
  }
2211
2516
  }
2212
2517
  }
2213
- // Recursively check if arrayNode itself is tainted
2214
- if (this.isTaintedExpression(arrayNode)) {
2518
+ }
2519
+ }
2520
+ if (node.type === 'array_access' || node.type === 'subscript_expression') {
2521
+ const chain = this.parseArrayAccessChain(node);
2522
+ if (chain) {
2523
+ if (this.isArrayElementAccessTainted(chain)) {
2215
2524
  return true;
2216
2525
  }
2526
+ if (chain.indices.length === 1 && chain.indices[0] !== '*') {
2527
+ // Known 1-D index with no element taint recorded — not tainted.
2528
+ const taintedIndices = this.taintedArrayElements.get(chain.base);
2529
+ if (taintedIndices && !this.tainted.has(chain.base)) {
2530
+ return false;
2531
+ }
2532
+ }
2533
+ }
2534
+ const arrayNode = node.childForFieldName('array') || node.child(0);
2535
+ if (arrayNode && this.isTaintedExpression(arrayNode)) {
2536
+ return true;
2217
2537
  }
2218
2538
  }
2219
2539
  if (node.type === 'ternary_expression') {
@@ -2318,6 +2638,33 @@ export class ConstantPropagator {
2318
2638
  // No verdict at this node — let the wrapper descend into children.
2319
2639
  return undefined;
2320
2640
  }
2641
+ /** cognium-dev#476: copy tainted array element indices across `System.arraycopy`. */
2642
+ handleSystemArrayCopy(node) {
2643
+ const objectNode = node.childForFieldName('object');
2644
+ const nameNode = node.childForFieldName('name');
2645
+ const argsNode = node.childForFieldName('arguments');
2646
+ if (!nameNode || !argsNode)
2647
+ return;
2648
+ if (getNodeText(nameNode, this.source) !== 'arraycopy')
2649
+ return;
2650
+ if (!objectNode || getNodeText(objectNode, this.source) !== 'System')
2651
+ return;
2652
+ const args = argsNode.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
2653
+ if (args.length < 3)
2654
+ return;
2655
+ const srcName = getNodeText(args[0], this.source);
2656
+ const dstName = getNodeText(args[2], this.source);
2657
+ const srcTainted = this.taintedArrayElements.get(srcName);
2658
+ if (!srcTainted || srcTainted.size === 0)
2659
+ return;
2660
+ if (!this.taintedArrayElements.has(dstName)) {
2661
+ this.taintedArrayElements.set(dstName, new Set());
2662
+ }
2663
+ const dstSet = this.taintedArrayElements.get(dstName);
2664
+ for (const idx of srcTainted) {
2665
+ dstSet.add(idx);
2666
+ }
2667
+ }
2321
2668
  checkCollectionTaint(node) {
2322
2669
  const objectNode = node.childForFieldName('object');
2323
2670
  const nameNode = node.childForFieldName('name');