circle-ir 4.14.1 → 4.15.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 (95) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +9 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/const-table-lookup.d.ts +29 -0
  5. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  6. package/dist/analysis/const-table-lookup.js +145 -0
  7. package/dist/analysis/const-table-lookup.js.map +1 -0
  8. package/dist/analysis/findings.d.ts.map +1 -1
  9. package/dist/analysis/findings.js +15 -4
  10. package/dist/analysis/findings.js.map +1 -1
  11. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  12. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  13. package/dist/analysis/go-strings-replacer.js +145 -0
  14. package/dist/analysis/go-strings-replacer.js.map +1 -0
  15. package/dist/analysis/interprocedural.d.ts.map +1 -1
  16. package/dist/analysis/interprocedural.js +53 -0
  17. package/dist/analysis/interprocedural.js.map +1 -1
  18. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  19. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  20. package/dist/analysis/java-canonical-prefix.js +145 -0
  21. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  22. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  23. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  24. package/dist/analysis/java-crlf-strip.js +195 -0
  25. package/dist/analysis/java-crlf-strip.js.map +1 -0
  26. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  27. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  29. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  30. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  31. package/dist/analysis/passes/cross-file-pass.js +43 -0
  32. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  33. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  34. package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
  35. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  36. package/dist/analysis/passes/insecure-cookie-pass.d.ts +11 -4
  37. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  38. package/dist/analysis/passes/insecure-cookie-pass.js +30 -15
  39. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  40. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  41. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  42. package/dist/analysis/passes/interprocedural-pass.js +31 -3
  43. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  44. package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
  45. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/language-sources-pass.js +894 -10
  47. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  48. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  49. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  50. package/dist/analysis/passes/param-source-flow-gate-pass.js +162 -6
  51. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  52. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  53. package/dist/analysis/passes/scan-secrets-pass.js +24 -3
  54. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  55. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  56. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/sink-filter-pass.js +255 -25
  58. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  59. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/taint-propagation-pass.js +268 -5
  61. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  62. package/dist/analysis/scalar-derivation.d.ts +25 -0
  63. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  64. package/dist/analysis/scalar-derivation.js +196 -0
  65. package/dist/analysis/scalar-derivation.js.map +1 -0
  66. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  67. package/dist/analysis/taint-matcher.js +172 -8
  68. package/dist/analysis/taint-matcher.js.map +1 -1
  69. package/dist/analysis/taint-propagation.d.ts +9 -0
  70. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  71. package/dist/analysis/taint-propagation.js +260 -11
  72. package/dist/analysis/taint-propagation.js.map +1 -1
  73. package/dist/analyzer.d.ts.map +1 -1
  74. package/dist/analyzer.js +8 -1
  75. package/dist/analyzer.js.map +1 -1
  76. package/dist/browser/circle-ir.js +3403 -1225
  77. package/dist/core/circle-ir-core.cjs +598 -26
  78. package/dist/core/circle-ir-core.js +598 -26
  79. package/dist/core/extractors/calls.js +19 -0
  80. package/dist/core/extractors/calls.js.map +1 -1
  81. package/dist/core/extractors/dfg.js +240 -6
  82. package/dist/core/extractors/dfg.js.map +1 -1
  83. package/dist/graph/code-graph.d.ts +9 -0
  84. package/dist/graph/code-graph.d.ts.map +1 -1
  85. package/dist/graph/code-graph.js +11 -0
  86. package/dist/graph/code-graph.js.map +1 -1
  87. package/dist/languages/plugins/go.d.ts.map +1 -1
  88. package/dist/languages/plugins/go.js +19 -0
  89. package/dist/languages/plugins/go.js.map +1 -1
  90. package/dist/resolution/cross-file.d.ts +7 -0
  91. package/dist/resolution/cross-file.d.ts.map +1 -1
  92. package/dist/resolution/cross-file.js +10 -1
  93. package/dist/resolution/cross-file.js.map +1 -1
  94. package/dist/wasm/web-tree-sitter.wasm +0 -0
  95. package/package.json +3 -3
@@ -8014,6 +8014,17 @@ function extractGoCallInfo(node) {
8014
8014
  if (operand.type === "identifier") {
8015
8015
  const resolved = resolveGoLocalReceiverType(opText, node);
8016
8016
  receiver = resolved !== null ? resolved : opText;
8017
+ } else if (operand.type === "selector_expression" && methodName === "Get") {
8018
+ const fieldNode = operand.childForFieldName("field");
8019
+ const inner = operand.childForFieldName("operand");
8020
+ const fieldName = fieldNode ? getNodeText(fieldNode) : "";
8021
+ const innerName = inner && inner.type === "identifier" ? getNodeText(inner) : null;
8022
+ const innerType = innerName !== null ? resolveGoLocalReceiverType(innerName, node) : null;
8023
+ if (innerType === "Request" && (fieldName === "PostForm" || fieldName === "Form")) {
8024
+ receiver = fieldName === "PostForm" ? "RequestPostForm" : "RequestForm";
8025
+ } else {
8026
+ receiver = opText;
8027
+ }
8017
8028
  } else {
8018
8029
  receiver = opText;
8019
8030
  }
@@ -9830,6 +9841,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9830
9841
  };
9831
9842
  defs.push(def);
9832
9843
  currentScope(scopeStack).set(name2, def.id);
9844
+ markDeclared(currentScope(scopeStack), name2);
9833
9845
  } else if (isJavaScript) {
9834
9846
  const destructDefs = extractDestructuringDefs(nameNode, defId);
9835
9847
  for (const def of destructDefs) {
@@ -9876,21 +9888,31 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9876
9888
  useId = condUses.nextId;
9877
9889
  }
9878
9890
  const consequence = stmt.childForFieldName("consequence");
9891
+ const alternative = stmt.childForFieldName("alternative");
9892
+ const topAlts = alternateDefs.get(currentScope(scopeStack));
9893
+ const before = /* @__PURE__ */ new Map();
9894
+ if (topAlts) for (const [name2, ids] of topAlts) before.set(name2, [...ids]);
9895
+ let armA = null;
9896
+ let armB = null;
9879
9897
  if (consequence) {
9880
- const result = processStatement2(consequence, defId, useId, scopeStack, isJavaScript);
9898
+ const result = processArm(consequence, defId, useId, scopeStack, isJavaScript);
9899
+ armA = result.popped;
9881
9900
  defs.push(...result.defs);
9882
9901
  uses.push(...result.uses);
9883
9902
  defId = result.nextDefId;
9884
9903
  useId = result.nextUseId;
9885
9904
  }
9886
- const alternative = stmt.childForFieldName("alternative");
9887
9905
  if (alternative) {
9888
- const result = processStatement2(alternative, defId, useId, scopeStack, isJavaScript);
9906
+ const result = processArm(alternative, defId, useId, scopeStack, isJavaScript);
9907
+ armB = result.popped;
9889
9908
  defs.push(...result.defs);
9890
9909
  uses.push(...result.uses);
9891
9910
  defId = result.nextDefId;
9892
9911
  useId = result.nextUseId;
9893
9912
  }
9913
+ if (armA && armB) {
9914
+ mergeIfElseDefs(armA, armB, before, scopeStack);
9915
+ }
9894
9916
  break;
9895
9917
  }
9896
9918
  case "for_statement":
@@ -9961,7 +9983,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9961
9983
  }
9962
9984
  const body2 = stmt.childForFieldName("body");
9963
9985
  if (body2) {
9964
- const result = processStatement2(body2, defId, useId, scopeStack, isJavaScript);
9986
+ const result = processArm(body2, defId, useId, scopeStack, isJavaScript);
9965
9987
  defs.push(...result.defs);
9966
9988
  uses.push(...result.uses);
9967
9989
  defId = result.nextDefId;
@@ -9989,7 +10011,8 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9989
10011
  defId = result.nextDefId;
9990
10012
  useId = result.nextUseId;
9991
10013
  }
9992
- scopeStack.pop();
10014
+ const poppedSwitch = scopeStack.pop();
10015
+ if (!isJavaScript) carryBranchDefsOut(poppedSwitch, scopeStack, false, true);
9993
10016
  }
9994
10017
  break;
9995
10018
  }
@@ -10001,7 +10024,10 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10001
10024
  uses.push(...result.uses);
10002
10025
  defId = result.nextDefId;
10003
10026
  useId = result.nextUseId;
10004
- scopeStack.pop();
10027
+ const popped = scopeStack.pop();
10028
+ const alwaysRuns = stmt.parent?.type === "block" || stmt.parent?.type === "synchronized_statement";
10029
+ lastPoppedBlock = popped;
10030
+ if (!isJavaScript) carryBranchDefsOut(popped, scopeStack, alwaysRuns);
10005
10031
  break;
10006
10032
  }
10007
10033
  default: {
@@ -10043,6 +10069,7 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
10043
10069
  };
10044
10070
  defs.push(def);
10045
10071
  currentScope(scopeStack).set(name2, def.id);
10072
+ alternateDefs.get(currentScope(scopeStack))?.delete(name2);
10046
10073
  } else if (left) {
10047
10074
  const leftUses = extractUses(left, useId, scopeStack, isJavaScript);
10048
10075
  uses.push(...leftUses.uses);
@@ -10126,6 +10153,10 @@ function extractUses(node, startId, scopeStack, isJavaScript) {
10126
10153
  line: n.startPosition.row + 1,
10127
10154
  def_id: reachingDef
10128
10155
  });
10156
+ for (const alt of findAlternateDefs(name2, scopeStack)) {
10157
+ if (alt === reachingDef) continue;
10158
+ uses.push({ id: id++, variable: name2, line: n.startPosition.row + 1, def_id: alt });
10159
+ }
10129
10160
  }
10130
10161
  });
10131
10162
  return { uses, nextId: id };
@@ -10378,6 +10409,134 @@ function extractDestructuringDefs(node, startId) {
10378
10409
  function currentScope(scopeStack) {
10379
10410
  return scopeStack[scopeStack.length - 1];
10380
10411
  }
10412
+ var lastPoppedBlock = null;
10413
+ function inAlwaysRunBlock(arm) {
10414
+ let n = arm.parent;
10415
+ while (n && (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || n.type === "while_statement" || n.type === "do_statement")) n = n.parent;
10416
+ return !!n && n.type === "block" && !!n.parent && (n.parent.type === "block" || n.parent.type === "synchronized_statement");
10417
+ }
10418
+ function inNestedBlock(arm) {
10419
+ let n = arm.parent;
10420
+ while (n && (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || n.type === "while_statement" || n.type === "do_statement")) n = n.parent;
10421
+ if (!n || n.type !== "block" || !n.parent) return false;
10422
+ switch (n.parent.type) {
10423
+ case "block":
10424
+ case "synchronized_statement":
10425
+ case "if_statement":
10426
+ case "for_statement":
10427
+ case "enhanced_for_statement":
10428
+ case "while_statement":
10429
+ case "do_statement":
10430
+ case "try_statement":
10431
+ case "try_with_resources_statement":
10432
+ case "catch_clause":
10433
+ case "finally_clause":
10434
+ case "labeled_statement":
10435
+ return true;
10436
+ default:
10437
+ return false;
10438
+ }
10439
+ }
10440
+ function processArm(node, defId, useId, scopeStack, isJavaScript) {
10441
+ const isBlock = node.type === "block" || node.type === "statement_block";
10442
+ if (isJavaScript || (isBlock ? !inAlwaysRunBlock(node) : !inNestedBlock(node))) {
10443
+ lastPoppedBlock = null;
10444
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10445
+ return { ...result2, popped: null };
10446
+ }
10447
+ if (isBlock) {
10448
+ lastPoppedBlock = null;
10449
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10450
+ return { ...result2, popped: lastPoppedBlock };
10451
+ }
10452
+ scopeStack.push(/* @__PURE__ */ new Map());
10453
+ const result = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10454
+ const popped = scopeStack.pop();
10455
+ carryBranchDefsOut(popped, scopeStack, false);
10456
+ return { ...result, popped };
10457
+ }
10458
+ function mergeIfElseDefs(armA, armB, before, scopeStack) {
10459
+ const top = scopeStack[scopeStack.length - 1];
10460
+ if (!top) return;
10461
+ const alts = alternateDefs.get(top);
10462
+ for (const [name2, idA] of armA) {
10463
+ const idB = armB.get(name2);
10464
+ if (idB === void 0) continue;
10465
+ if (declaredNames.get(armA)?.has(name2) || declaredNames.get(armB)?.has(name2)) continue;
10466
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10467
+ const prior = new Set(before.get(name2) ?? []);
10468
+ const arms = (alts?.get(name2) ?? []).filter((id) => !prior.has(id) && id !== idB);
10469
+ if (!arms.includes(idA)) arms.push(idA);
10470
+ top.set(name2, idB);
10471
+ let map = alts;
10472
+ if (!map) {
10473
+ map = /* @__PURE__ */ new Map();
10474
+ alternateDefs.set(top, map);
10475
+ }
10476
+ map.set(name2, arms);
10477
+ }
10478
+ }
10479
+ var declaredNames = /* @__PURE__ */ new WeakMap();
10480
+ var alternateDefs = /* @__PURE__ */ new WeakMap();
10481
+ function markDeclared(scope, name2) {
10482
+ let set = declaredNames.get(scope);
10483
+ if (!set) {
10484
+ set = /* @__PURE__ */ new Set();
10485
+ declaredNames.set(scope, set);
10486
+ }
10487
+ set.add(name2);
10488
+ alternateDefs.get(scope)?.delete(name2);
10489
+ }
10490
+ function carryBranchDefsOut(popped, scopeStack, alwaysRuns = false, altsOnly = false) {
10491
+ const declared = declaredNames.get(popped);
10492
+ const poppedAlts = alternateDefs.get(popped);
10493
+ const top = scopeStack[scopeStack.length - 1];
10494
+ if (!top) return;
10495
+ if (alwaysRuns) {
10496
+ for (const [name2, id] of popped) {
10497
+ if (declared?.has(name2)) continue;
10498
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10499
+ top.set(name2, id);
10500
+ let alts = alternateDefs.get(top);
10501
+ if (!alts) {
10502
+ alts = /* @__PURE__ */ new Map();
10503
+ alternateDefs.set(top, alts);
10504
+ }
10505
+ alts.delete(name2);
10506
+ const kept = poppedAlts?.get(name2);
10507
+ if (kept && kept.length > 0) alts.set(name2, [...kept]);
10508
+ }
10509
+ if (poppedAlts) {
10510
+ for (const [name2, ids] of poppedAlts) if (!popped.has(name2)) carry(name2, ids);
10511
+ }
10512
+ return;
10513
+ }
10514
+ function carry(name2, ids) {
10515
+ if (declared?.has(name2)) return;
10516
+ if (!scopeStack.some((scope) => scope.has(name2))) return;
10517
+ let alts = alternateDefs.get(top);
10518
+ if (!alts) {
10519
+ alts = /* @__PURE__ */ new Map();
10520
+ alternateDefs.set(top, alts);
10521
+ }
10522
+ const list = alts.get(name2) ?? [];
10523
+ for (const id of ids) if (!list.includes(id)) list.push(id);
10524
+ alts.set(name2, list);
10525
+ }
10526
+ if (!altsOnly) for (const [name2, id] of popped) carry(name2, [id]);
10527
+ if (poppedAlts) for (const [name2, ids] of poppedAlts) carry(name2, ids);
10528
+ }
10529
+ function findAlternateDefs(name2, scopeStack) {
10530
+ const out2 = [];
10531
+ for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10532
+ const alts = alternateDefs.get(scopeStack[i2])?.get(name2);
10533
+ if (alts) {
10534
+ for (const id of alts) if (!out2.includes(id)) out2.push(id);
10535
+ }
10536
+ if (scopeStack[i2].has(name2)) break;
10537
+ }
10538
+ return out2;
10539
+ }
10381
10540
  function findReachingDef(name2, scopeStack) {
10382
10541
  for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10383
10542
  const defId = scopeStack[i2].get(name2);
@@ -12382,8 +12541,15 @@ var DEFAULT_SINKS = [
12382
12541
  { method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
12383
12542
  { method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12384
12543
  { method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
12385
- // Wildfly/Undertow servlet resource manager
12386
- { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12544
+ // Wildfly/Undertow servlet resource manager.
12545
+ // ResourceManager / PathResourceManager / FileResourceManager /
12546
+ // ClassPathResourceManager are the interface and implementations WildFly
12547
+ // calls through (cognium-dev#622, CVE-2018-1047). Class-qualified only.
12548
+ { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12549
+ { method: "getResource", class: "ResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12550
+ { method: "getResource", class: "PathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12551
+ { method: "getResource", class: "FileResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12552
+ { method: "getResource", class: "ClassPathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12387
12553
  // Yamcs file system bucket
12388
12554
  { method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12389
12555
  // RocketMQ validators
@@ -14727,6 +14893,157 @@ function getDefaultConfig() {
14727
14893
  };
14728
14894
  }
14729
14895
 
14896
+ // src/analysis/java-crlf-strip.ts
14897
+ function decodeJavaLiteral(raw) {
14898
+ return raw.replace(/\\(?:u+([0-9a-fA-F]{4})|([0-3][0-7]{0,2}|[4-7][0-7]?)|([btnfrs"'\\]))/g, (_m, hex, oct, simple) => {
14899
+ if (hex !== void 0) return String.fromCharCode(parseInt(hex, 16));
14900
+ if (oct !== void 0) return String.fromCharCode(parseInt(oct, 8));
14901
+ switch (simple) {
14902
+ case "b":
14903
+ return "\b";
14904
+ case "t":
14905
+ return " ";
14906
+ case "n":
14907
+ return "\n";
14908
+ case "f":
14909
+ return "\f";
14910
+ case "r":
14911
+ return "\r";
14912
+ case "s":
14913
+ return " ";
14914
+ default:
14915
+ return simple;
14916
+ }
14917
+ });
14918
+ }
14919
+ function readAtom(s, i2) {
14920
+ const c = s[i2];
14921
+ if (c === "\\" && i2 + 1 < s.length) {
14922
+ const n = s[i2 + 1];
14923
+ return { atom: n === "r" ? "cr" : n === "n" ? "lf" : "other", end: i2 + 2 };
14924
+ }
14925
+ return { atom: c === "\r" ? "cr" : c === "\n" ? "lf" : "other", end: i2 + 1 };
14926
+ }
14927
+ function quantifierAllowsOne(q) {
14928
+ if (q === "") return true;
14929
+ const m = /^(?:(\*)|(\+)|(\?)|\{(\d+)(?:(,)(\d*))?\})[+?]?$/.exec(q);
14930
+ if (!m) return false;
14931
+ if (m[1] || m[2] || m[3]) return true;
14932
+ const min = Number(m[4]);
14933
+ const max = m[5] ? m[6] === "" ? Infinity : Number(m[6]) : min;
14934
+ return min <= 1 && max >= 1;
14935
+ }
14936
+ var NONE = { cr: false, lf: false };
14937
+ function alternativeCoverage(alt) {
14938
+ if (alt === "") return NONE;
14939
+ let cr = false;
14940
+ let lf = false;
14941
+ let i2;
14942
+ if (alt[0] === "[") {
14943
+ if (alt[1] === "^") return NONE;
14944
+ i2 = 1;
14945
+ let closed = false;
14946
+ while (i2 < alt.length) {
14947
+ if (alt[i2] === "]" && i2 > 1) {
14948
+ closed = true;
14949
+ i2++;
14950
+ break;
14951
+ }
14952
+ if (alt[i2] === "[" || alt[i2] === "&" && alt[i2 + 1] === "&") return NONE;
14953
+ const { atom, end } = readAtom(alt, i2);
14954
+ if (atom === "cr") cr = true;
14955
+ else if (atom === "lf") lf = true;
14956
+ i2 = end;
14957
+ }
14958
+ if (!closed) return NONE;
14959
+ } else {
14960
+ const { atom, end } = readAtom(alt, 0);
14961
+ if (atom === "other") return NONE;
14962
+ cr = atom === "cr";
14963
+ lf = atom === "lf";
14964
+ i2 = end;
14965
+ }
14966
+ return quantifierAllowsOne(alt.slice(i2)) ? { cr, lf } : NONE;
14967
+ }
14968
+ function splitAlternatives(s) {
14969
+ const alts = [];
14970
+ let depth = 0;
14971
+ let inClass = false;
14972
+ let cur = "";
14973
+ for (let i2 = 0; i2 < s.length; i2++) {
14974
+ const c = s[i2];
14975
+ if (c === "\\") {
14976
+ cur += c + (s[i2 + 1] ?? "");
14977
+ i2++;
14978
+ continue;
14979
+ }
14980
+ if (inClass) {
14981
+ if (c === "]") inClass = false;
14982
+ cur += c;
14983
+ continue;
14984
+ }
14985
+ if (c === "[") {
14986
+ inClass = true;
14987
+ cur += c;
14988
+ continue;
14989
+ }
14990
+ if (c === "(") depth++;
14991
+ else if (c === ")") depth--;
14992
+ if (c === "|" && depth === 0) {
14993
+ alts.push(cur);
14994
+ cur = "";
14995
+ continue;
14996
+ }
14997
+ cur += c;
14998
+ }
14999
+ alts.push(cur);
15000
+ return alts;
15001
+ }
15002
+ function unwrapGroup(s) {
15003
+ const open = /^\((?:\?:)?/.exec(s);
15004
+ if (!open) return null;
15005
+ let depth = 0;
15006
+ let inClass = false;
15007
+ for (let i2 = 0; i2 < s.length; i2++) {
15008
+ const c = s[i2];
15009
+ if (c === "\\") {
15010
+ i2++;
15011
+ continue;
15012
+ }
15013
+ if (inClass) {
15014
+ if (c === "]") inClass = false;
15015
+ continue;
15016
+ }
15017
+ if (c === "[") {
15018
+ inClass = true;
15019
+ continue;
15020
+ }
15021
+ if (c === "(") depth++;
15022
+ else if (c === ")" && --depth === 0) {
15023
+ return { body: s.slice(open[0].length, i2), quantifier: s.slice(i2 + 1) };
15024
+ }
15025
+ }
15026
+ return null;
15027
+ }
15028
+ function javaReplaceCrLfCoverage(method, rawFrom) {
15029
+ const value = decodeJavaLiteral(rawFrom);
15030
+ if (method === "replace") return { cr: value === "\r", lf: value === "\n" };
15031
+ let body2 = value;
15032
+ const group = unwrapGroup(value);
15033
+ if (group) {
15034
+ if (!quantifierAllowsOne(group.quantifier)) return NONE;
15035
+ body2 = group.body;
15036
+ }
15037
+ let cr = false;
15038
+ let lf = false;
15039
+ for (const alt of splitAlternatives(body2)) {
15040
+ const c = alternativeCoverage(alt);
15041
+ if (c.cr) cr = true;
15042
+ if (c.lf) lf = true;
15043
+ }
15044
+ return { cr, lf };
15045
+ }
15046
+
14730
15047
  // src/analysis/arg-type-resolver.ts
14731
15048
  function findEnclosingType(callInMethod, types) {
14732
15049
  if (!callInMethod) return null;
@@ -14827,7 +15144,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
14827
15144
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14828
15145
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
14829
15146
  const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14830
- const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
15147
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines, language);
14831
15148
  return { sources, sinks: gatedSinks, sanitizers };
14832
15149
  }
14833
15150
  function sinkPatternAppliesTo(pattern, language) {
@@ -15254,7 +15571,8 @@ function isInterproceduralTaintableType(typeName, language) {
15254
15571
  }
15255
15572
  function isParameterizedQueryCall(call, pattern) {
15256
15573
  if (pattern.type !== "sql_injection") return false;
15257
- const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
15574
+ const positions = pattern.arg_positions ?? [];
15575
+ const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
15258
15576
  const queryArg = call.arguments.find((a) => a.position === queryPos);
15259
15577
  if (queryArg) {
15260
15578
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
@@ -15845,6 +16163,25 @@ function isGoFprintToNonResponseWriter(call, pattern, language, sourceLines) {
15845
16163
  }
15846
16164
  return true;
15847
16165
  }
16166
+ var JAVA_OUTBOUND_STREAM_OWNERS = "HttpURLConnection|HttpsURLConnection|URLConnection|JarURLConnection|Socket|SSLSocket|Process";
16167
+ function isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines) {
16168
+ if (language !== "java" || pattern.type !== "xss") return false;
16169
+ const receiver = (call.receiver ?? "").replace(/\s+/g, "");
16170
+ if (!receiver.endsWith(".getOutputStream()")) return false;
16171
+ if (/\.openConnection\(\)\)?\.getOutputStream\(\)$/.test(receiver)) return true;
16172
+ const m = receiver.match(/^\(?([A-Za-z_]\w*)\)?\.getOutputStream\(\)$/);
16173
+ if (!m || !sourceLines) return false;
16174
+ const declRe = new RegExp(
16175
+ `\\b(?:[\\w.]+\\.)?(?:${JAVA_OUTBOUND_STREAM_OWNERS})\\s+${m[1]}\\s*[=;,)]`
16176
+ );
16177
+ const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]]*|var)\\s+${m[1]}\\s*[=;,)]`);
16178
+ for (let i2 = call.location.line - 1; i2 >= 0; i2--) {
16179
+ const line = (sourceLines[i2] ?? "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/, "");
16180
+ if (declRe.test(line)) return true;
16181
+ if (anyDeclRe.test(line)) return false;
16182
+ }
16183
+ return false;
16184
+ }
15848
16185
  function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
15849
16186
  if (language !== "go") return false;
15850
16187
  if (pattern.type !== "deserialization") return false;
@@ -15994,6 +16331,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15994
16331
  if (isGoFprintToNonResponseWriter(call, pattern, language, sourceLines)) {
15995
16332
  continue;
15996
16333
  }
16334
+ if (isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines)) {
16335
+ continue;
16336
+ }
15997
16337
  if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
15998
16338
  continue;
15999
16339
  }
@@ -16401,6 +16741,13 @@ function receiverMightBeClassImpl(receiver, className) {
16401
16741
  if (receiver === className) {
16402
16742
  return true;
16403
16743
  }
16744
+ const inlineRequire = receiver.match(/^require\(\s*['"`](?:node:)?([\w./-]+)['"`]\s*\)$/);
16745
+ if (inlineRequire && inlineRequire[1] === className) {
16746
+ return true;
16747
+ }
16748
+ if (className === "ClassLoader" && /(?:^|\.)(?:getContextClassLoader|getClassLoader|getSystemClassLoader)\(\s*\)$/.test(receiver.replace(/\s+/g, ""))) {
16749
+ return true;
16750
+ }
16404
16751
  if (receiver.endsWith(")")) {
16405
16752
  const ctorMatch = receiver.match(/^(\w+)\(/);
16406
16753
  if (ctorMatch) {
@@ -16689,8 +17036,9 @@ function calculateSinkConfidence(call, pattern) {
16689
17036
  }
16690
17037
  return Math.min(confidence, 1);
16691
17038
  }
16692
- function findSanitizers(calls, types, patterns, sourceLines) {
17039
+ function findSanitizers(calls, types, patterns, sourceLines, language) {
16693
17040
  const sanitizers = [];
17041
+ const javaReplace = (call) => language === "java" && sourceLines && (call.method_name === "replace" || call.method_name === "replaceAll") ? javaReplaceChainCredit(javaChainTextAt(sourceLines, call.location.line, call.location.column)) : null;
16694
17042
  const sanitizerMethods = /* @__PURE__ */ new Set();
16695
17043
  for (const type of types) {
16696
17044
  for (const method of type.methods) {
@@ -16715,10 +17063,15 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16715
17063
  if (inside.length !== 1) continue;
16716
17064
  const innerCall = inside[0];
16717
17065
  let matched;
16718
- for (const pattern of patterns) {
16719
- if (matchesSanitizerPattern(innerCall, pattern)) {
16720
- matched = pattern;
16721
- break;
17066
+ const replaceCredit = javaReplace(innerCall);
17067
+ if (replaceCredit) {
17068
+ matched = replaceCredit.length > 0 ? { method: innerCall.method_name, removes: replaceCredit } : void 0;
17069
+ } else {
17070
+ for (const pattern of patterns) {
17071
+ if (matchesSanitizerPattern(innerCall, pattern)) {
17072
+ matched = pattern;
17073
+ break;
17074
+ }
16722
17075
  }
16723
17076
  }
16724
17077
  if (!matched || !matched.removes || matched.removes.length === 0) continue;
@@ -16770,6 +17123,18 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16770
17123
  });
16771
17124
  continue;
16772
17125
  }
17126
+ const replaceCredit = javaReplace(call);
17127
+ if (replaceCredit) {
17128
+ if (replaceCredit.length > 0) {
17129
+ sanitizers.push({
17130
+ type: "method_call",
17131
+ method: formatSanitizerMethod(call),
17132
+ line: call.location.line,
17133
+ sanitizes: replaceCredit
17134
+ });
17135
+ }
17136
+ continue;
17137
+ }
16773
17138
  for (const pattern of patterns) {
16774
17139
  if (matchesSanitizerPattern(call, pattern)) {
16775
17140
  sanitizers.push({
@@ -16799,6 +17164,52 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16799
17164
  }
16800
17165
  return sanitizers;
16801
17166
  }
17167
+ var JAVA_LIT = String.raw`(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')`;
17168
+ var JAVA_REPLACE_ARGS_RE = new RegExp(String.raw`\.(replace|replaceAll)\(\s*${JAVA_LIT}\s*,\s*${JAVA_LIT}\s*\)`, "g");
17169
+ function javaChainTextAt(sourceLines, line, column) {
17170
+ const text = [(sourceLines[line - 1] ?? "").slice(column), ...sourceLines.slice(line, line + 7)].join("\n");
17171
+ let depth = 0;
17172
+ let i2 = 0;
17173
+ for (; i2 < text.length; i2++) {
17174
+ const c = text[i2];
17175
+ if (c === '"' || c === "'") {
17176
+ for (i2++; i2 < text.length && text[i2] !== c; i2++) if (text[i2] === "\\") i2++;
17177
+ continue;
17178
+ }
17179
+ if (c === "(" || c === "[") {
17180
+ depth++;
17181
+ continue;
17182
+ }
17183
+ if (c === ")" || c === "]") {
17184
+ if (depth === 0) break;
17185
+ depth--;
17186
+ continue;
17187
+ }
17188
+ if (depth === 0 && !/[\w$.\s]/.test(c)) break;
17189
+ }
17190
+ return text.slice(0, i2);
17191
+ }
17192
+ function javaReplaceChainCredit(chainText) {
17193
+ let cr = false;
17194
+ let lf = false;
17195
+ let lt = false;
17196
+ let gt = false;
17197
+ for (const m of chainText.matchAll(JAVA_REPLACE_ARGS_RE)) {
17198
+ const kind = m[1];
17199
+ const rawFrom = m[2] ?? m[3] ?? "";
17200
+ const from = rawFrom.replace(/\\\\/g, "\\");
17201
+ const to = m[4] ?? m[5] ?? "";
17202
+ const cov = javaReplaceCrLfCoverage(kind, rawFrom);
17203
+ if (cov.cr) cr = true;
17204
+ if (cov.lf) lf = true;
17205
+ if (from === "<" && to.startsWith("&")) lt = true;
17206
+ if (from === ">" && to.startsWith("&")) gt = true;
17207
+ }
17208
+ const out2 = [];
17209
+ if (cr && lf) out2.push("log_injection");
17210
+ if (lt && gt) out2.push("xss");
17211
+ return out2;
17212
+ }
16802
17213
  function matchesSanitizerPattern(call, pattern) {
16803
17214
  if (pattern.method) {
16804
17215
  if (call.method_name !== pattern.method) {
@@ -16839,6 +17250,15 @@ function formatSanitizerMethod(call) {
16839
17250
  // src/graph/code-graph.ts
16840
17251
  var CodeGraph = class {
16841
17252
  ir;
17253
+ /**
17254
+ * Defs whose value cannot carry the content of the values they read, so
17255
+ * taint does not flow into them. Set by `analyze()`; empty for a bare graph.
17256
+ * - an int or bool measure or test (`len(x)`, `x.length`,
17257
+ * `re.MatchString(x)`) (#568)
17258
+ * - a lookup in a constant table the file never writes (`T[req.query.k]`)
17259
+ * (#667)
17260
+ */
17261
+ taintBarrierDefIds = /* @__PURE__ */ new Set();
16842
17262
  constructor(ir) {
16843
17263
  this.ir = ir;
16844
17264
  }
@@ -17093,6 +17513,7 @@ var CodeGraph = class {
17093
17513
  for (const [fromDef, chains] of this.chainsByFromDef) {
17094
17514
  if (!result.has(fromDef)) continue;
17095
17515
  for (const chain of chains) {
17516
+ if (this.taintBarrierDefIds.has(chain.to_def)) continue;
17096
17517
  if (!result.has(chain.to_def)) {
17097
17518
  result.add(chain.to_def);
17098
17519
  changed = true;
@@ -17178,6 +17599,10 @@ function buildSanitizersByLine(sanitizers) {
17178
17599
  }
17179
17600
  return out2;
17180
17601
  }
17602
+ function templateInterpolationCount(expression) {
17603
+ if (!expression.trimStart().startsWith("`")) return 0;
17604
+ return (expression.match(/\$\{/g) ?? []).length;
17605
+ }
17181
17606
  function concatOutsideStrings(expression) {
17182
17607
  return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
17183
17608
  }
@@ -17213,31 +17638,38 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17213
17638
  const taintedVars = [];
17214
17639
  const flows = [];
17215
17640
  const javaConcat = graph.ir.meta.language === "java";
17641
+ const jsLike = graph.ir.meta.language === "javascript" || graph.ir.meta.language === "typescript";
17216
17642
  const reachableSinks = /* @__PURE__ */ new Map();
17217
17643
  const defsByLine = graph.defsByLine;
17218
17644
  const usesByLine = graph.usesByLine;
17219
17645
  const callsByLine = graph.callsByLine;
17220
17646
  const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
17221
17647
  const defById = graph.defById;
17222
- const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
17223
- const initialTaint = rawInitialTaint.filter((tv) => {
17648
+ const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine, graph.chainsByFromDef, jsLike);
17649
+ const filteredInitialTaint = rawInitialTaint.filter((tv) => {
17224
17650
  if (tv.line === tv.sourceLine) return true;
17651
+ if (graph.taintBarrierDefIds.has(tv.defId)) return false;
17225
17652
  const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
17226
17653
  return !sanCheck.sanitized;
17227
17654
  });
17655
+ const initialTaint = [
17656
+ ...filteredInitialTaint.filter((tv) => tv.sourceType !== "interprocedural_param"),
17657
+ ...filteredInitialTaint.filter((tv) => tv.sourceType === "interprocedural_param")
17658
+ ];
17228
17659
  taintedVars.push(...initialTaint);
17229
17660
  const propagatedTaint = propagateThroughChains(
17230
17661
  initialTaint,
17231
17662
  graph.chainsByFromDef,
17232
17663
  defById,
17233
- sanitizersByLine
17664
+ sanitizersByLine,
17665
+ graph.taintBarrierDefIds
17234
17666
  );
17235
17667
  taintedVars.push(...propagatedTaint);
17236
17668
  const allTaintedDefIds = /* @__PURE__ */ new Set();
17237
17669
  const taintByDefId = /* @__PURE__ */ new Map();
17238
17670
  for (const tv of taintedVars) {
17239
17671
  allTaintedDefIds.add(tv.defId);
17240
- taintByDefId.set(tv.defId, tv);
17672
+ if (!yieldsToRealSource(taintByDefId.get(tv.defId), tv)) taintByDefId.set(tv.defId, tv);
17241
17673
  }
17242
17674
  for (const sink of sinks) {
17243
17675
  const usesAtSink = usesByLine.get(sink.line) ?? [];
@@ -17250,7 +17682,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17250
17682
  }
17251
17683
  }
17252
17684
  const expr = typeof arg.expression === "string" ? arg.expression : "";
17253
- const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
17685
+ const compoundConcat = !!arg.variable && expr !== arg.variable && (javaConcat && concatOutsideStrings(expr) || jsLike && templateInterpolationCount(expr) > 1);
17254
17686
  const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17255
17687
  {
17256
17688
  for (const use of candidateUses) {
@@ -17263,11 +17695,26 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17263
17695
  }
17264
17696
  const taintInfo = taintByDefId.get(use.def_id);
17265
17697
  if (taintInfo) {
17698
+ let sanMap = sanitizersByLine;
17699
+ if (identifierOutsideSanitizerCalls(
17700
+ use.variable,
17701
+ callsAtSink,
17702
+ sanitizersByLine.get(sink.line) ?? [],
17703
+ sink.type
17704
+ )) {
17705
+ const stripped = new Map(sanitizersByLine);
17706
+ const kept = (sanitizersByLine.get(sink.line) ?? []).filter(
17707
+ (s) => !sanitizerCoversSink(s, sink.type)
17708
+ );
17709
+ if (kept.length === 0) stripped.delete(sink.line);
17710
+ else stripped.set(sink.line, kept);
17711
+ sanMap = stripped;
17712
+ }
17266
17713
  const isSanitized = checkSanitized(
17267
17714
  taintInfo.line,
17268
17715
  sink.line,
17269
17716
  sink.type,
17270
- sanitizersByLine,
17717
+ sanMap,
17271
17718
  {
17272
17719
  startDefId: use.def_id,
17273
17720
  chainsByToDef: graph.chainsByToDef,
@@ -17308,12 +17755,46 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17308
17755
  }
17309
17756
  return { taintedVars, flows: deduped, reachableSinks };
17310
17757
  }
17311
- function findInitialTaint(sources, callsByLine, defsByLine) {
17758
+ var PARAM_SCOPED_SOURCE_RE = /^(?:GraphQL \w+\.\w+ resolver args|Express handler destructured request) /;
17759
+ function isCompleteConditionHeader(code) {
17760
+ if (!code) return false;
17761
+ const m = /^(?:\}\s*)?(?:else\s+)?(?:if|while)\s*\(/.exec(code);
17762
+ if (!m) return false;
17763
+ const text = code.slice(m[0].length - 1).replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17764
+ let depth = 0;
17765
+ for (const ch of text) {
17766
+ if (ch === "(") depth++;
17767
+ else if (ch === ")" && --depth === 0) return true;
17768
+ }
17769
+ return false;
17770
+ }
17771
+ function hasSecondStatement(code) {
17772
+ if (!code || /^for\b/.test(code)) return false;
17773
+ const text = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17774
+ return /;\s*(?!\/\/|\/\*|#|\}|\)|$)\S/.test(text);
17775
+ }
17776
+ function findInitialTaint(sources, callsByLine, defsByLine, chainsByFromDef, jsLike = false) {
17312
17777
  const tainted = [];
17313
17778
  for (const source of sources) {
17314
17779
  const defsOnLine = defsByLine.get(source.line) ?? [];
17780
+ let seedable = null;
17781
+ if (source.variable && hasSecondStatement(source.code)) {
17782
+ const own = defsOnLine.filter((d) => d.variable === source.variable);
17783
+ if (own.length > 0) {
17784
+ seedable = new Set(own.map((d) => d.id));
17785
+ for (const d of own) {
17786
+ for (const c of chainsByFromDef.get(d.id) ?? []) {
17787
+ if (defsOnLine.some((o) => o.id === c.to_def)) seedable.add(c.to_def);
17788
+ }
17789
+ }
17790
+ }
17791
+ }
17315
17792
  for (const def of defsOnLine) {
17316
- if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
17793
+ if (seedable && !seedable.has(def.id)) continue;
17794
+ if ((source.type === "interprocedural_param" || PARAM_SCOPED_SOURCE_RE.test(source.location ?? "")) && source.variable && def.kind === "param" && def.variable !== source.variable) {
17795
+ continue;
17796
+ }
17797
+ if (source.type === "interprocedural_param" && def.kind !== "param") {
17317
17798
  continue;
17318
17799
  }
17319
17800
  tainted.push({
@@ -17325,7 +17806,8 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17325
17806
  confidence: source.confidence
17326
17807
  });
17327
17808
  }
17328
- const defsNextLine = defsByLine.get(source.line + 1) ?? [];
17809
+ const closesOwnStatement = jsLike && defsOnLine.length > 0 && /;\s*(?:\/\/.*)?$/.test(source.code ?? "");
17810
+ const defsNextLine = isCompleteConditionHeader(source.code) ? [] : (defsByLine.get(source.line + 1) ?? []).filter((d) => !(closesOwnStatement && d.kind === "local"));
17329
17811
  for (const def of defsNextLine) {
17330
17812
  const callsOnSourceLine = callsByLine.get(source.line) ?? [];
17331
17813
  if (callsOnSourceLine.length > 0) {
@@ -17359,12 +17841,15 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17359
17841
  }
17360
17842
  return tainted;
17361
17843
  }
17362
- function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
17844
+ function yieldsToRealSource(existing, next) {
17845
+ return existing !== void 0 && next.sourceType === "interprocedural_param" && existing.sourceType !== "interprocedural_param";
17846
+ }
17847
+ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine, taintBarrierDefIds) {
17363
17848
  const propagated = [];
17364
17849
  const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
17365
17850
  const taintInfoByDefId = /* @__PURE__ */ new Map();
17366
17851
  for (const t of initialTaint) {
17367
- taintInfoByDefId.set(t.defId, t);
17852
+ if (!yieldsToRealSource(taintInfoByDefId.get(t.defId), t)) taintInfoByDefId.set(t.defId, t);
17368
17853
  }
17369
17854
  const queue = [...initialTaint.map((t) => t.defId)];
17370
17855
  const visited = new Set(queue);
@@ -17375,6 +17860,7 @@ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitize
17375
17860
  const outgoingChains = chainsByFromDef.get(currentDefId) ?? [];
17376
17861
  for (const chain of outgoingChains) {
17377
17862
  if (visited.has(chain.to_def)) continue;
17863
+ if (taintBarrierDefIds.has(chain.to_def)) continue;
17378
17864
  const targetDef = defById.get(chain.to_def);
17379
17865
  if (!targetDef) continue;
17380
17866
  const sanitizeCheck = checkSanitized(
@@ -17421,6 +17907,92 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
17421
17907
  "crlf",
17422
17908
  "mass_assignment"
17423
17909
  ]);
17910
+ function readBalancedArg(text, openParen) {
17911
+ let depth = 0;
17912
+ let quote = null;
17913
+ let escaped = false;
17914
+ for (let i2 = openParen; i2 < text.length; i2++) {
17915
+ const c = text[i2];
17916
+ if (quote) {
17917
+ if (escaped) {
17918
+ escaped = false;
17919
+ continue;
17920
+ }
17921
+ if (c === "\\") {
17922
+ escaped = true;
17923
+ continue;
17924
+ }
17925
+ if (c === quote) quote = null;
17926
+ continue;
17927
+ }
17928
+ if (c === '"' || c === "'") {
17929
+ quote = c;
17930
+ continue;
17931
+ }
17932
+ if (c === "(") depth++;
17933
+ else if (c === ")") {
17934
+ depth--;
17935
+ if (depth === 0) return text.slice(openParen + 1, i2);
17936
+ }
17937
+ }
17938
+ return null;
17939
+ }
17940
+ function sanitizerCallRefs(sanitizers, sinkType) {
17941
+ const refs = [];
17942
+ for (const san of sanitizers) {
17943
+ if (!sanitizerCoversSink(san, sinkType)) continue;
17944
+ const m = /(?:(\w+)\.)?(\w+)\(\)/.exec(san.method);
17945
+ if (!m?.[2]) continue;
17946
+ refs.push({ receiver: m[1], methodName: m[2] });
17947
+ }
17948
+ return refs;
17949
+ }
17950
+ function maskSanitizerCalls(expr, refs) {
17951
+ let out2 = expr;
17952
+ for (const ref of refs) {
17953
+ const needles = ref.receiver ? [`${ref.receiver}.${ref.methodName}`, ref.methodName] : [ref.methodName];
17954
+ for (const needle of needles) {
17955
+ const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
17956
+ for (let guard = 0; guard < 8; guard++) {
17957
+ const m = re.exec(out2);
17958
+ if (!m) break;
17959
+ const open = m.index + m[0].length - 1;
17960
+ const arg = readBalancedArg(out2, open);
17961
+ if (arg === null) break;
17962
+ out2 = out2.slice(0, m.index) + " " + out2.slice(open + arg.length + 2);
17963
+ }
17964
+ }
17965
+ }
17966
+ return out2;
17967
+ }
17968
+ function maskLengthOf(expr) {
17969
+ return expr.replace(/\b(?:len|strlen|count)\s*\(\s*[\w$.]+\s*\)/g, " 0 ").replace(/[\w$.]+\s*\.\s*(?:length|size|count)\s*(?:\(\s*\))?(?![\w$(])/g, " 0 ");
17970
+ }
17971
+ function blankStringLiterals(expr) {
17972
+ return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
17973
+ }
17974
+ function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType) {
17975
+ if (!/^[A-Za-z_$][\w$]*$/.test(variable)) return false;
17976
+ const refs = sanitizerCallRefs(sanitizers, sinkType);
17977
+ if (refs.length === 0) return false;
17978
+ const re = new RegExp(
17979
+ `(?:^|[^A-Za-z0-9_$])${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`
17980
+ );
17981
+ for (const call of calls) {
17982
+ if (refs.some((r) => r.methodName === call.method_name)) continue;
17983
+ const masked = call.arguments.map((arg) => {
17984
+ const expr = arg.expression ?? "";
17985
+ return { expr, masked: expr ? maskSanitizerCalls(expr, refs) : expr };
17986
+ });
17987
+ if (!masked.some((m) => m.masked !== m.expr)) continue;
17988
+ for (const m of masked) {
17989
+ if (!m.masked) continue;
17990
+ const text = m.masked === m.expr ? maskLengthOf(m.masked) : m.masked;
17991
+ if (re.test(blankStringLiterals(text))) return true;
17992
+ }
17993
+ }
17994
+ return false;
17995
+ }
17424
17996
  function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
17425
17997
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
17426
17998
  const sanitizersAtTarget = sanitizersByLine.get(toLine);