circle-ir 4.14.2 → 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 (78) hide show
  1. package/dist/analysis/const-table-lookup.d.ts +29 -0
  2. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  3. package/dist/analysis/const-table-lookup.js +145 -0
  4. package/dist/analysis/const-table-lookup.js.map +1 -0
  5. package/dist/analysis/findings.d.ts.map +1 -1
  6. package/dist/analysis/findings.js +15 -4
  7. package/dist/analysis/findings.js.map +1 -1
  8. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  9. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  10. package/dist/analysis/go-strings-replacer.js +145 -0
  11. package/dist/analysis/go-strings-replacer.js.map +1 -0
  12. package/dist/analysis/interprocedural.d.ts.map +1 -1
  13. package/dist/analysis/interprocedural.js +53 -0
  14. package/dist/analysis/interprocedural.js.map +1 -1
  15. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  16. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  17. package/dist/analysis/java-canonical-prefix.js +145 -0
  18. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  19. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  20. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  21. package/dist/analysis/java-crlf-strip.js +195 -0
  22. package/dist/analysis/java-crlf-strip.js.map +1 -0
  23. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  24. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  25. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  26. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  27. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/cross-file-pass.js +43 -0
  29. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  30. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  31. package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
  32. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  33. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  34. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/interprocedural-pass.js +20 -1
  36. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  37. package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
  38. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  39. package/dist/analysis/passes/language-sources-pass.js +842 -10
  40. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  41. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  42. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  43. package/dist/analysis/passes/param-source-flow-gate-pass.js +162 -6
  44. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  45. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/sink-filter-pass.js +207 -1
  47. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  48. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  49. package/dist/analysis/passes/taint-propagation-pass.js +257 -2
  50. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  51. package/dist/analysis/scalar-derivation.d.ts +25 -0
  52. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  53. package/dist/analysis/scalar-derivation.js +196 -0
  54. package/dist/analysis/scalar-derivation.js.map +1 -0
  55. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  56. package/dist/analysis/taint-matcher.js +167 -6
  57. package/dist/analysis/taint-matcher.js.map +1 -1
  58. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  59. package/dist/analysis/taint-propagation.js +149 -19
  60. package/dist/analysis/taint-propagation.js.map +1 -1
  61. package/dist/analyzer.d.ts.map +1 -1
  62. package/dist/analyzer.js +7 -0
  63. package/dist/analyzer.js.map +1 -1
  64. package/dist/browser/circle-ir.js +3140 -1186
  65. package/dist/core/circle-ir-core.cjs +485 -27
  66. package/dist/core/circle-ir-core.js +485 -27
  67. package/dist/core/extractors/dfg.js +240 -6
  68. package/dist/core/extractors/dfg.js.map +1 -1
  69. package/dist/graph/code-graph.d.ts +9 -0
  70. package/dist/graph/code-graph.d.ts.map +1 -1
  71. package/dist/graph/code-graph.js +11 -0
  72. package/dist/graph/code-graph.js.map +1 -1
  73. package/dist/resolution/cross-file.d.ts +7 -0
  74. package/dist/resolution/cross-file.d.ts.map +1 -1
  75. package/dist/resolution/cross-file.js +10 -1
  76. package/dist/resolution/cross-file.js.map +1 -1
  77. package/dist/wasm/web-tree-sitter.wasm +0 -0
  78. package/package.json +3 -3
@@ -9907,6 +9907,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9907
9907
  };
9908
9908
  defs.push(def);
9909
9909
  currentScope(scopeStack).set(name2, def.id);
9910
+ markDeclared(currentScope(scopeStack), name2);
9910
9911
  } else if (isJavaScript) {
9911
9912
  const destructDefs = extractDestructuringDefs(nameNode, defId);
9912
9913
  for (const def of destructDefs) {
@@ -9953,21 +9954,31 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9953
9954
  useId = condUses.nextId;
9954
9955
  }
9955
9956
  const consequence = stmt.childForFieldName("consequence");
9957
+ const alternative = stmt.childForFieldName("alternative");
9958
+ const topAlts = alternateDefs.get(currentScope(scopeStack));
9959
+ const before = /* @__PURE__ */ new Map();
9960
+ if (topAlts) for (const [name2, ids] of topAlts) before.set(name2, [...ids]);
9961
+ let armA = null;
9962
+ let armB = null;
9956
9963
  if (consequence) {
9957
- const result = processStatement2(consequence, defId, useId, scopeStack, isJavaScript);
9964
+ const result = processArm(consequence, defId, useId, scopeStack, isJavaScript);
9965
+ armA = result.popped;
9958
9966
  defs.push(...result.defs);
9959
9967
  uses.push(...result.uses);
9960
9968
  defId = result.nextDefId;
9961
9969
  useId = result.nextUseId;
9962
9970
  }
9963
- const alternative = stmt.childForFieldName("alternative");
9964
9971
  if (alternative) {
9965
- const result = processStatement2(alternative, defId, useId, scopeStack, isJavaScript);
9972
+ const result = processArm(alternative, defId, useId, scopeStack, isJavaScript);
9973
+ armB = result.popped;
9966
9974
  defs.push(...result.defs);
9967
9975
  uses.push(...result.uses);
9968
9976
  defId = result.nextDefId;
9969
9977
  useId = result.nextUseId;
9970
9978
  }
9979
+ if (armA && armB) {
9980
+ mergeIfElseDefs(armA, armB, before, scopeStack);
9981
+ }
9971
9982
  break;
9972
9983
  }
9973
9984
  case "for_statement":
@@ -10038,7 +10049,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10038
10049
  }
10039
10050
  const body2 = stmt.childForFieldName("body");
10040
10051
  if (body2) {
10041
- const result = processStatement2(body2, defId, useId, scopeStack, isJavaScript);
10052
+ const result = processArm(body2, defId, useId, scopeStack, isJavaScript);
10042
10053
  defs.push(...result.defs);
10043
10054
  uses.push(...result.uses);
10044
10055
  defId = result.nextDefId;
@@ -10066,7 +10077,8 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10066
10077
  defId = result.nextDefId;
10067
10078
  useId = result.nextUseId;
10068
10079
  }
10069
- scopeStack.pop();
10080
+ const poppedSwitch = scopeStack.pop();
10081
+ if (!isJavaScript) carryBranchDefsOut(poppedSwitch, scopeStack, false, true);
10070
10082
  }
10071
10083
  break;
10072
10084
  }
@@ -10078,7 +10090,10 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10078
10090
  uses.push(...result.uses);
10079
10091
  defId = result.nextDefId;
10080
10092
  useId = result.nextUseId;
10081
- scopeStack.pop();
10093
+ const popped = scopeStack.pop();
10094
+ const alwaysRuns = stmt.parent?.type === "block" || stmt.parent?.type === "synchronized_statement";
10095
+ lastPoppedBlock = popped;
10096
+ if (!isJavaScript) carryBranchDefsOut(popped, scopeStack, alwaysRuns);
10082
10097
  break;
10083
10098
  }
10084
10099
  default: {
@@ -10120,6 +10135,7 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
10120
10135
  };
10121
10136
  defs.push(def);
10122
10137
  currentScope(scopeStack).set(name2, def.id);
10138
+ alternateDefs.get(currentScope(scopeStack))?.delete(name2);
10123
10139
  } else if (left) {
10124
10140
  const leftUses = extractUses(left, useId, scopeStack, isJavaScript);
10125
10141
  uses.push(...leftUses.uses);
@@ -10203,6 +10219,10 @@ function extractUses(node, startId, scopeStack, isJavaScript) {
10203
10219
  line: n.startPosition.row + 1,
10204
10220
  def_id: reachingDef
10205
10221
  });
10222
+ for (const alt of findAlternateDefs(name2, scopeStack)) {
10223
+ if (alt === reachingDef) continue;
10224
+ uses.push({ id: id++, variable: name2, line: n.startPosition.row + 1, def_id: alt });
10225
+ }
10206
10226
  }
10207
10227
  });
10208
10228
  return { uses, nextId: id };
@@ -10455,6 +10475,134 @@ function extractDestructuringDefs(node, startId) {
10455
10475
  function currentScope(scopeStack) {
10456
10476
  return scopeStack[scopeStack.length - 1];
10457
10477
  }
10478
+ var lastPoppedBlock = null;
10479
+ function inAlwaysRunBlock(arm) {
10480
+ let n = arm.parent;
10481
+ 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;
10482
+ return !!n && n.type === "block" && !!n.parent && (n.parent.type === "block" || n.parent.type === "synchronized_statement");
10483
+ }
10484
+ function inNestedBlock(arm) {
10485
+ let n = arm.parent;
10486
+ 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;
10487
+ if (!n || n.type !== "block" || !n.parent) return false;
10488
+ switch (n.parent.type) {
10489
+ case "block":
10490
+ case "synchronized_statement":
10491
+ case "if_statement":
10492
+ case "for_statement":
10493
+ case "enhanced_for_statement":
10494
+ case "while_statement":
10495
+ case "do_statement":
10496
+ case "try_statement":
10497
+ case "try_with_resources_statement":
10498
+ case "catch_clause":
10499
+ case "finally_clause":
10500
+ case "labeled_statement":
10501
+ return true;
10502
+ default:
10503
+ return false;
10504
+ }
10505
+ }
10506
+ function processArm(node, defId, useId, scopeStack, isJavaScript) {
10507
+ const isBlock = node.type === "block" || node.type === "statement_block";
10508
+ if (isJavaScript || (isBlock ? !inAlwaysRunBlock(node) : !inNestedBlock(node))) {
10509
+ lastPoppedBlock = null;
10510
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10511
+ return { ...result2, popped: null };
10512
+ }
10513
+ if (isBlock) {
10514
+ lastPoppedBlock = null;
10515
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10516
+ return { ...result2, popped: lastPoppedBlock };
10517
+ }
10518
+ scopeStack.push(/* @__PURE__ */ new Map());
10519
+ const result = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10520
+ const popped = scopeStack.pop();
10521
+ carryBranchDefsOut(popped, scopeStack, false);
10522
+ return { ...result, popped };
10523
+ }
10524
+ function mergeIfElseDefs(armA, armB, before, scopeStack) {
10525
+ const top = scopeStack[scopeStack.length - 1];
10526
+ if (!top) return;
10527
+ const alts = alternateDefs.get(top);
10528
+ for (const [name2, idA] of armA) {
10529
+ const idB = armB.get(name2);
10530
+ if (idB === void 0) continue;
10531
+ if (declaredNames.get(armA)?.has(name2) || declaredNames.get(armB)?.has(name2)) continue;
10532
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10533
+ const prior = new Set(before.get(name2) ?? []);
10534
+ const arms = (alts?.get(name2) ?? []).filter((id) => !prior.has(id) && id !== idB);
10535
+ if (!arms.includes(idA)) arms.push(idA);
10536
+ top.set(name2, idB);
10537
+ let map = alts;
10538
+ if (!map) {
10539
+ map = /* @__PURE__ */ new Map();
10540
+ alternateDefs.set(top, map);
10541
+ }
10542
+ map.set(name2, arms);
10543
+ }
10544
+ }
10545
+ var declaredNames = /* @__PURE__ */ new WeakMap();
10546
+ var alternateDefs = /* @__PURE__ */ new WeakMap();
10547
+ function markDeclared(scope, name2) {
10548
+ let set = declaredNames.get(scope);
10549
+ if (!set) {
10550
+ set = /* @__PURE__ */ new Set();
10551
+ declaredNames.set(scope, set);
10552
+ }
10553
+ set.add(name2);
10554
+ alternateDefs.get(scope)?.delete(name2);
10555
+ }
10556
+ function carryBranchDefsOut(popped, scopeStack, alwaysRuns = false, altsOnly = false) {
10557
+ const declared = declaredNames.get(popped);
10558
+ const poppedAlts = alternateDefs.get(popped);
10559
+ const top = scopeStack[scopeStack.length - 1];
10560
+ if (!top) return;
10561
+ if (alwaysRuns) {
10562
+ for (const [name2, id] of popped) {
10563
+ if (declared?.has(name2)) continue;
10564
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10565
+ top.set(name2, id);
10566
+ let alts = alternateDefs.get(top);
10567
+ if (!alts) {
10568
+ alts = /* @__PURE__ */ new Map();
10569
+ alternateDefs.set(top, alts);
10570
+ }
10571
+ alts.delete(name2);
10572
+ const kept = poppedAlts?.get(name2);
10573
+ if (kept && kept.length > 0) alts.set(name2, [...kept]);
10574
+ }
10575
+ if (poppedAlts) {
10576
+ for (const [name2, ids] of poppedAlts) if (!popped.has(name2)) carry(name2, ids);
10577
+ }
10578
+ return;
10579
+ }
10580
+ function carry(name2, ids) {
10581
+ if (declared?.has(name2)) return;
10582
+ if (!scopeStack.some((scope) => scope.has(name2))) return;
10583
+ let alts = alternateDefs.get(top);
10584
+ if (!alts) {
10585
+ alts = /* @__PURE__ */ new Map();
10586
+ alternateDefs.set(top, alts);
10587
+ }
10588
+ const list = alts.get(name2) ?? [];
10589
+ for (const id of ids) if (!list.includes(id)) list.push(id);
10590
+ alts.set(name2, list);
10591
+ }
10592
+ if (!altsOnly) for (const [name2, id] of popped) carry(name2, [id]);
10593
+ if (poppedAlts) for (const [name2, ids] of poppedAlts) carry(name2, ids);
10594
+ }
10595
+ function findAlternateDefs(name2, scopeStack) {
10596
+ const out2 = [];
10597
+ for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10598
+ const alts = alternateDefs.get(scopeStack[i2])?.get(name2);
10599
+ if (alts) {
10600
+ for (const id of alts) if (!out2.includes(id)) out2.push(id);
10601
+ }
10602
+ if (scopeStack[i2].has(name2)) break;
10603
+ }
10604
+ return out2;
10605
+ }
10458
10606
  function findReachingDef(name2, scopeStack) {
10459
10607
  for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10460
10608
  const defId = scopeStack[i2].get(name2);
@@ -14811,6 +14959,157 @@ function getDefaultConfig() {
14811
14959
  };
14812
14960
  }
14813
14961
 
14962
+ // src/analysis/java-crlf-strip.ts
14963
+ function decodeJavaLiteral(raw) {
14964
+ 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) => {
14965
+ if (hex !== void 0) return String.fromCharCode(parseInt(hex, 16));
14966
+ if (oct !== void 0) return String.fromCharCode(parseInt(oct, 8));
14967
+ switch (simple) {
14968
+ case "b":
14969
+ return "\b";
14970
+ case "t":
14971
+ return " ";
14972
+ case "n":
14973
+ return "\n";
14974
+ case "f":
14975
+ return "\f";
14976
+ case "r":
14977
+ return "\r";
14978
+ case "s":
14979
+ return " ";
14980
+ default:
14981
+ return simple;
14982
+ }
14983
+ });
14984
+ }
14985
+ function readAtom(s, i2) {
14986
+ const c = s[i2];
14987
+ if (c === "\\" && i2 + 1 < s.length) {
14988
+ const n = s[i2 + 1];
14989
+ return { atom: n === "r" ? "cr" : n === "n" ? "lf" : "other", end: i2 + 2 };
14990
+ }
14991
+ return { atom: c === "\r" ? "cr" : c === "\n" ? "lf" : "other", end: i2 + 1 };
14992
+ }
14993
+ function quantifierAllowsOne(q) {
14994
+ if (q === "") return true;
14995
+ const m = /^(?:(\*)|(\+)|(\?)|\{(\d+)(?:(,)(\d*))?\})[+?]?$/.exec(q);
14996
+ if (!m) return false;
14997
+ if (m[1] || m[2] || m[3]) return true;
14998
+ const min = Number(m[4]);
14999
+ const max = m[5] ? m[6] === "" ? Infinity : Number(m[6]) : min;
15000
+ return min <= 1 && max >= 1;
15001
+ }
15002
+ var NONE = { cr: false, lf: false };
15003
+ function alternativeCoverage(alt) {
15004
+ if (alt === "") return NONE;
15005
+ let cr = false;
15006
+ let lf = false;
15007
+ let i2;
15008
+ if (alt[0] === "[") {
15009
+ if (alt[1] === "^") return NONE;
15010
+ i2 = 1;
15011
+ let closed = false;
15012
+ while (i2 < alt.length) {
15013
+ if (alt[i2] === "]" && i2 > 1) {
15014
+ closed = true;
15015
+ i2++;
15016
+ break;
15017
+ }
15018
+ if (alt[i2] === "[" || alt[i2] === "&" && alt[i2 + 1] === "&") return NONE;
15019
+ const { atom, end } = readAtom(alt, i2);
15020
+ if (atom === "cr") cr = true;
15021
+ else if (atom === "lf") lf = true;
15022
+ i2 = end;
15023
+ }
15024
+ if (!closed) return NONE;
15025
+ } else {
15026
+ const { atom, end } = readAtom(alt, 0);
15027
+ if (atom === "other") return NONE;
15028
+ cr = atom === "cr";
15029
+ lf = atom === "lf";
15030
+ i2 = end;
15031
+ }
15032
+ return quantifierAllowsOne(alt.slice(i2)) ? { cr, lf } : NONE;
15033
+ }
15034
+ function splitAlternatives(s) {
15035
+ const alts = [];
15036
+ let depth = 0;
15037
+ let inClass = false;
15038
+ let cur = "";
15039
+ for (let i2 = 0; i2 < s.length; i2++) {
15040
+ const c = s[i2];
15041
+ if (c === "\\") {
15042
+ cur += c + (s[i2 + 1] ?? "");
15043
+ i2++;
15044
+ continue;
15045
+ }
15046
+ if (inClass) {
15047
+ if (c === "]") inClass = false;
15048
+ cur += c;
15049
+ continue;
15050
+ }
15051
+ if (c === "[") {
15052
+ inClass = true;
15053
+ cur += c;
15054
+ continue;
15055
+ }
15056
+ if (c === "(") depth++;
15057
+ else if (c === ")") depth--;
15058
+ if (c === "|" && depth === 0) {
15059
+ alts.push(cur);
15060
+ cur = "";
15061
+ continue;
15062
+ }
15063
+ cur += c;
15064
+ }
15065
+ alts.push(cur);
15066
+ return alts;
15067
+ }
15068
+ function unwrapGroup(s) {
15069
+ const open = /^\((?:\?:)?/.exec(s);
15070
+ if (!open) return null;
15071
+ let depth = 0;
15072
+ let inClass = false;
15073
+ for (let i2 = 0; i2 < s.length; i2++) {
15074
+ const c = s[i2];
15075
+ if (c === "\\") {
15076
+ i2++;
15077
+ continue;
15078
+ }
15079
+ if (inClass) {
15080
+ if (c === "]") inClass = false;
15081
+ continue;
15082
+ }
15083
+ if (c === "[") {
15084
+ inClass = true;
15085
+ continue;
15086
+ }
15087
+ if (c === "(") depth++;
15088
+ else if (c === ")" && --depth === 0) {
15089
+ return { body: s.slice(open[0].length, i2), quantifier: s.slice(i2 + 1) };
15090
+ }
15091
+ }
15092
+ return null;
15093
+ }
15094
+ function javaReplaceCrLfCoverage(method, rawFrom) {
15095
+ const value = decodeJavaLiteral(rawFrom);
15096
+ if (method === "replace") return { cr: value === "\r", lf: value === "\n" };
15097
+ let body2 = value;
15098
+ const group = unwrapGroup(value);
15099
+ if (group) {
15100
+ if (!quantifierAllowsOne(group.quantifier)) return NONE;
15101
+ body2 = group.body;
15102
+ }
15103
+ let cr = false;
15104
+ let lf = false;
15105
+ for (const alt of splitAlternatives(body2)) {
15106
+ const c = alternativeCoverage(alt);
15107
+ if (c.cr) cr = true;
15108
+ if (c.lf) lf = true;
15109
+ }
15110
+ return { cr, lf };
15111
+ }
15112
+
14814
15113
  // src/analysis/arg-type-resolver.ts
14815
15114
  function findEnclosingType(callInMethod, types) {
14816
15115
  if (!callInMethod) return null;
@@ -14911,7 +15210,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
14911
15210
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14912
15211
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
14913
15212
  const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14914
- const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
15213
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines, language);
14915
15214
  return { sources, sinks: gatedSinks, sanitizers };
14916
15215
  }
14917
15216
  function sinkPatternAppliesTo(pattern, language) {
@@ -15930,6 +16229,25 @@ function isGoFprintToNonResponseWriter(call, pattern, language, sourceLines) {
15930
16229
  }
15931
16230
  return true;
15932
16231
  }
16232
+ var JAVA_OUTBOUND_STREAM_OWNERS = "HttpURLConnection|HttpsURLConnection|URLConnection|JarURLConnection|Socket|SSLSocket|Process";
16233
+ function isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines) {
16234
+ if (language !== "java" || pattern.type !== "xss") return false;
16235
+ const receiver = (call.receiver ?? "").replace(/\s+/g, "");
16236
+ if (!receiver.endsWith(".getOutputStream()")) return false;
16237
+ if (/\.openConnection\(\)\)?\.getOutputStream\(\)$/.test(receiver)) return true;
16238
+ const m = receiver.match(/^\(?([A-Za-z_]\w*)\)?\.getOutputStream\(\)$/);
16239
+ if (!m || !sourceLines) return false;
16240
+ const declRe = new RegExp(
16241
+ `\\b(?:[\\w.]+\\.)?(?:${JAVA_OUTBOUND_STREAM_OWNERS})\\s+${m[1]}\\s*[=;,)]`
16242
+ );
16243
+ const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]]*|var)\\s+${m[1]}\\s*[=;,)]`);
16244
+ for (let i2 = call.location.line - 1; i2 >= 0; i2--) {
16245
+ const line = (sourceLines[i2] ?? "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/, "");
16246
+ if (declRe.test(line)) return true;
16247
+ if (anyDeclRe.test(line)) return false;
16248
+ }
16249
+ return false;
16250
+ }
15933
16251
  function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
15934
16252
  if (language !== "go") return false;
15935
16253
  if (pattern.type !== "deserialization") return false;
@@ -16079,6 +16397,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16079
16397
  if (isGoFprintToNonResponseWriter(call, pattern, language, sourceLines)) {
16080
16398
  continue;
16081
16399
  }
16400
+ if (isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines)) {
16401
+ continue;
16402
+ }
16082
16403
  if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
16083
16404
  continue;
16084
16405
  }
@@ -16486,6 +16807,13 @@ function receiverMightBeClassImpl(receiver, className) {
16486
16807
  if (receiver === className) {
16487
16808
  return true;
16488
16809
  }
16810
+ const inlineRequire = receiver.match(/^require\(\s*['"`](?:node:)?([\w./-]+)['"`]\s*\)$/);
16811
+ if (inlineRequire && inlineRequire[1] === className) {
16812
+ return true;
16813
+ }
16814
+ if (className === "ClassLoader" && /(?:^|\.)(?:getContextClassLoader|getClassLoader|getSystemClassLoader)\(\s*\)$/.test(receiver.replace(/\s+/g, ""))) {
16815
+ return true;
16816
+ }
16489
16817
  if (receiver.endsWith(")")) {
16490
16818
  const ctorMatch = receiver.match(/^(\w+)\(/);
16491
16819
  if (ctorMatch) {
@@ -16774,8 +17102,9 @@ function calculateSinkConfidence(call, pattern) {
16774
17102
  }
16775
17103
  return Math.min(confidence, 1);
16776
17104
  }
16777
- function findSanitizers(calls, types, patterns, sourceLines) {
17105
+ function findSanitizers(calls, types, patterns, sourceLines, language) {
16778
17106
  const sanitizers = [];
17107
+ const javaReplace = (call) => language === "java" && sourceLines && (call.method_name === "replace" || call.method_name === "replaceAll") ? javaReplaceChainCredit(javaChainTextAt(sourceLines, call.location.line, call.location.column)) : null;
16779
17108
  const sanitizerMethods = /* @__PURE__ */ new Set();
16780
17109
  for (const type of types) {
16781
17110
  for (const method of type.methods) {
@@ -16800,10 +17129,15 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16800
17129
  if (inside.length !== 1) continue;
16801
17130
  const innerCall = inside[0];
16802
17131
  let matched;
16803
- for (const pattern of patterns) {
16804
- if (matchesSanitizerPattern(innerCall, pattern)) {
16805
- matched = pattern;
16806
- break;
17132
+ const replaceCredit = javaReplace(innerCall);
17133
+ if (replaceCredit) {
17134
+ matched = replaceCredit.length > 0 ? { method: innerCall.method_name, removes: replaceCredit } : void 0;
17135
+ } else {
17136
+ for (const pattern of patterns) {
17137
+ if (matchesSanitizerPattern(innerCall, pattern)) {
17138
+ matched = pattern;
17139
+ break;
17140
+ }
16807
17141
  }
16808
17142
  }
16809
17143
  if (!matched || !matched.removes || matched.removes.length === 0) continue;
@@ -16855,6 +17189,18 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16855
17189
  });
16856
17190
  continue;
16857
17191
  }
17192
+ const replaceCredit = javaReplace(call);
17193
+ if (replaceCredit) {
17194
+ if (replaceCredit.length > 0) {
17195
+ sanitizers.push({
17196
+ type: "method_call",
17197
+ method: formatSanitizerMethod(call),
17198
+ line: call.location.line,
17199
+ sanitizes: replaceCredit
17200
+ });
17201
+ }
17202
+ continue;
17203
+ }
16858
17204
  for (const pattern of patterns) {
16859
17205
  if (matchesSanitizerPattern(call, pattern)) {
16860
17206
  sanitizers.push({
@@ -16884,6 +17230,52 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16884
17230
  }
16885
17231
  return sanitizers;
16886
17232
  }
17233
+ var JAVA_LIT = String.raw`(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')`;
17234
+ var JAVA_REPLACE_ARGS_RE = new RegExp(String.raw`\.(replace|replaceAll)\(\s*${JAVA_LIT}\s*,\s*${JAVA_LIT}\s*\)`, "g");
17235
+ function javaChainTextAt(sourceLines, line, column) {
17236
+ const text = [(sourceLines[line - 1] ?? "").slice(column), ...sourceLines.slice(line, line + 7)].join("\n");
17237
+ let depth = 0;
17238
+ let i2 = 0;
17239
+ for (; i2 < text.length; i2++) {
17240
+ const c = text[i2];
17241
+ if (c === '"' || c === "'") {
17242
+ for (i2++; i2 < text.length && text[i2] !== c; i2++) if (text[i2] === "\\") i2++;
17243
+ continue;
17244
+ }
17245
+ if (c === "(" || c === "[") {
17246
+ depth++;
17247
+ continue;
17248
+ }
17249
+ if (c === ")" || c === "]") {
17250
+ if (depth === 0) break;
17251
+ depth--;
17252
+ continue;
17253
+ }
17254
+ if (depth === 0 && !/[\w$.\s]/.test(c)) break;
17255
+ }
17256
+ return text.slice(0, i2);
17257
+ }
17258
+ function javaReplaceChainCredit(chainText) {
17259
+ let cr = false;
17260
+ let lf = false;
17261
+ let lt = false;
17262
+ let gt = false;
17263
+ for (const m of chainText.matchAll(JAVA_REPLACE_ARGS_RE)) {
17264
+ const kind = m[1];
17265
+ const rawFrom = m[2] ?? m[3] ?? "";
17266
+ const from = rawFrom.replace(/\\\\/g, "\\");
17267
+ const to = m[4] ?? m[5] ?? "";
17268
+ const cov = javaReplaceCrLfCoverage(kind, rawFrom);
17269
+ if (cov.cr) cr = true;
17270
+ if (cov.lf) lf = true;
17271
+ if (from === "<" && to.startsWith("&")) lt = true;
17272
+ if (from === ">" && to.startsWith("&")) gt = true;
17273
+ }
17274
+ const out2 = [];
17275
+ if (cr && lf) out2.push("log_injection");
17276
+ if (lt && gt) out2.push("xss");
17277
+ return out2;
17278
+ }
16887
17279
  function matchesSanitizerPattern(call, pattern) {
16888
17280
  if (pattern.method) {
16889
17281
  if (call.method_name !== pattern.method) {
@@ -16924,6 +17316,15 @@ function formatSanitizerMethod(call) {
16924
17316
  // src/graph/code-graph.ts
16925
17317
  var CodeGraph = class {
16926
17318
  ir;
17319
+ /**
17320
+ * Defs whose value cannot carry the content of the values they read, so
17321
+ * taint does not flow into them. Set by `analyze()`; empty for a bare graph.
17322
+ * - an int or bool measure or test (`len(x)`, `x.length`,
17323
+ * `re.MatchString(x)`) (#568)
17324
+ * - a lookup in a constant table the file never writes (`T[req.query.k]`)
17325
+ * (#667)
17326
+ */
17327
+ taintBarrierDefIds = /* @__PURE__ */ new Set();
16927
17328
  constructor(ir) {
16928
17329
  this.ir = ir;
16929
17330
  }
@@ -17178,6 +17579,7 @@ var CodeGraph = class {
17178
17579
  for (const [fromDef, chains] of this.chainsByFromDef) {
17179
17580
  if (!result.has(fromDef)) continue;
17180
17581
  for (const chain of chains) {
17582
+ if (this.taintBarrierDefIds.has(chain.to_def)) continue;
17181
17583
  if (!result.has(chain.to_def)) {
17182
17584
  result.add(chain.to_def);
17183
17585
  changed = true;
@@ -17263,6 +17665,10 @@ function buildSanitizersByLine(sanitizers) {
17263
17665
  }
17264
17666
  return out2;
17265
17667
  }
17668
+ function templateInterpolationCount(expression) {
17669
+ if (!expression.trimStart().startsWith("`")) return 0;
17670
+ return (expression.match(/\$\{/g) ?? []).length;
17671
+ }
17266
17672
  function concatOutsideStrings(expression) {
17267
17673
  return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
17268
17674
  }
@@ -17298,31 +17704,38 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17298
17704
  const taintedVars = [];
17299
17705
  const flows = [];
17300
17706
  const javaConcat = graph.ir.meta.language === "java";
17707
+ const jsLike = graph.ir.meta.language === "javascript" || graph.ir.meta.language === "typescript";
17301
17708
  const reachableSinks = /* @__PURE__ */ new Map();
17302
17709
  const defsByLine = graph.defsByLine;
17303
17710
  const usesByLine = graph.usesByLine;
17304
17711
  const callsByLine = graph.callsByLine;
17305
17712
  const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
17306
17713
  const defById = graph.defById;
17307
- const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
17308
- const initialTaint = rawInitialTaint.filter((tv) => {
17714
+ const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine, graph.chainsByFromDef, jsLike);
17715
+ const filteredInitialTaint = rawInitialTaint.filter((tv) => {
17309
17716
  if (tv.line === tv.sourceLine) return true;
17717
+ if (graph.taintBarrierDefIds.has(tv.defId)) return false;
17310
17718
  const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
17311
17719
  return !sanCheck.sanitized;
17312
17720
  });
17721
+ const initialTaint = [
17722
+ ...filteredInitialTaint.filter((tv) => tv.sourceType !== "interprocedural_param"),
17723
+ ...filteredInitialTaint.filter((tv) => tv.sourceType === "interprocedural_param")
17724
+ ];
17313
17725
  taintedVars.push(...initialTaint);
17314
17726
  const propagatedTaint = propagateThroughChains(
17315
17727
  initialTaint,
17316
17728
  graph.chainsByFromDef,
17317
17729
  defById,
17318
- sanitizersByLine
17730
+ sanitizersByLine,
17731
+ graph.taintBarrierDefIds
17319
17732
  );
17320
17733
  taintedVars.push(...propagatedTaint);
17321
17734
  const allTaintedDefIds = /* @__PURE__ */ new Set();
17322
17735
  const taintByDefId = /* @__PURE__ */ new Map();
17323
17736
  for (const tv of taintedVars) {
17324
17737
  allTaintedDefIds.add(tv.defId);
17325
- taintByDefId.set(tv.defId, tv);
17738
+ if (!yieldsToRealSource(taintByDefId.get(tv.defId), tv)) taintByDefId.set(tv.defId, tv);
17326
17739
  }
17327
17740
  for (const sink of sinks) {
17328
17741
  const usesAtSink = usesByLine.get(sink.line) ?? [];
@@ -17335,7 +17748,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17335
17748
  }
17336
17749
  }
17337
17750
  const expr = typeof arg.expression === "string" ? arg.expression : "";
17338
- const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
17751
+ const compoundConcat = !!arg.variable && expr !== arg.variable && (javaConcat && concatOutsideStrings(expr) || jsLike && templateInterpolationCount(expr) > 1);
17339
17752
  const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17340
17753
  {
17341
17754
  for (const use of candidateUses) {
@@ -17408,12 +17821,46 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17408
17821
  }
17409
17822
  return { taintedVars, flows: deduped, reachableSinks };
17410
17823
  }
17411
- function findInitialTaint(sources, callsByLine, defsByLine) {
17824
+ var PARAM_SCOPED_SOURCE_RE = /^(?:GraphQL \w+\.\w+ resolver args|Express handler destructured request) /;
17825
+ function isCompleteConditionHeader(code) {
17826
+ if (!code) return false;
17827
+ const m = /^(?:\}\s*)?(?:else\s+)?(?:if|while)\s*\(/.exec(code);
17828
+ if (!m) return false;
17829
+ const text = code.slice(m[0].length - 1).replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17830
+ let depth = 0;
17831
+ for (const ch of text) {
17832
+ if (ch === "(") depth++;
17833
+ else if (ch === ")" && --depth === 0) return true;
17834
+ }
17835
+ return false;
17836
+ }
17837
+ function hasSecondStatement(code) {
17838
+ if (!code || /^for\b/.test(code)) return false;
17839
+ const text = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17840
+ return /;\s*(?!\/\/|\/\*|#|\}|\)|$)\S/.test(text);
17841
+ }
17842
+ function findInitialTaint(sources, callsByLine, defsByLine, chainsByFromDef, jsLike = false) {
17412
17843
  const tainted = [];
17413
17844
  for (const source of sources) {
17414
17845
  const defsOnLine = defsByLine.get(source.line) ?? [];
17846
+ let seedable = null;
17847
+ if (source.variable && hasSecondStatement(source.code)) {
17848
+ const own = defsOnLine.filter((d) => d.variable === source.variable);
17849
+ if (own.length > 0) {
17850
+ seedable = new Set(own.map((d) => d.id));
17851
+ for (const d of own) {
17852
+ for (const c of chainsByFromDef.get(d.id) ?? []) {
17853
+ if (defsOnLine.some((o) => o.id === c.to_def)) seedable.add(c.to_def);
17854
+ }
17855
+ }
17856
+ }
17857
+ }
17415
17858
  for (const def of defsOnLine) {
17416
- if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
17859
+ if (seedable && !seedable.has(def.id)) continue;
17860
+ if ((source.type === "interprocedural_param" || PARAM_SCOPED_SOURCE_RE.test(source.location ?? "")) && source.variable && def.kind === "param" && def.variable !== source.variable) {
17861
+ continue;
17862
+ }
17863
+ if (source.type === "interprocedural_param" && def.kind !== "param") {
17417
17864
  continue;
17418
17865
  }
17419
17866
  tainted.push({
@@ -17425,7 +17872,8 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17425
17872
  confidence: source.confidence
17426
17873
  });
17427
17874
  }
17428
- const defsNextLine = defsByLine.get(source.line + 1) ?? [];
17875
+ const closesOwnStatement = jsLike && defsOnLine.length > 0 && /;\s*(?:\/\/.*)?$/.test(source.code ?? "");
17876
+ const defsNextLine = isCompleteConditionHeader(source.code) ? [] : (defsByLine.get(source.line + 1) ?? []).filter((d) => !(closesOwnStatement && d.kind === "local"));
17429
17877
  for (const def of defsNextLine) {
17430
17878
  const callsOnSourceLine = callsByLine.get(source.line) ?? [];
17431
17879
  if (callsOnSourceLine.length > 0) {
@@ -17459,12 +17907,15 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17459
17907
  }
17460
17908
  return tainted;
17461
17909
  }
17462
- function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
17910
+ function yieldsToRealSource(existing, next) {
17911
+ return existing !== void 0 && next.sourceType === "interprocedural_param" && existing.sourceType !== "interprocedural_param";
17912
+ }
17913
+ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine, taintBarrierDefIds) {
17463
17914
  const propagated = [];
17464
17915
  const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
17465
17916
  const taintInfoByDefId = /* @__PURE__ */ new Map();
17466
17917
  for (const t of initialTaint) {
17467
- taintInfoByDefId.set(t.defId, t);
17918
+ if (!yieldsToRealSource(taintInfoByDefId.get(t.defId), t)) taintInfoByDefId.set(t.defId, t);
17468
17919
  }
17469
17920
  const queue = [...initialTaint.map((t) => t.defId)];
17470
17921
  const visited = new Set(queue);
@@ -17475,6 +17926,7 @@ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitize
17475
17926
  const outgoingChains = chainsByFromDef.get(currentDefId) ?? [];
17476
17927
  for (const chain of outgoingChains) {
17477
17928
  if (visited.has(chain.to_def)) continue;
17929
+ if (taintBarrierDefIds.has(chain.to_def)) continue;
17478
17930
  const targetDef = defById.get(chain.to_def);
17479
17931
  if (!targetDef) continue;
17480
17932
  const sanitizeCheck = checkSanitized(
@@ -17579,6 +18031,9 @@ function maskSanitizerCalls(expr, refs) {
17579
18031
  }
17580
18032
  return out2;
17581
18033
  }
18034
+ function maskLengthOf(expr) {
18035
+ 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 ");
18036
+ }
17582
18037
  function blankStringLiterals(expr) {
17583
18038
  return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
17584
18039
  }
@@ -17591,12 +18046,15 @@ function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType)
17591
18046
  );
17592
18047
  for (const call of calls) {
17593
18048
  if (refs.some((r) => r.methodName === call.method_name)) continue;
17594
- for (const arg of call.arguments) {
18049
+ const masked = call.arguments.map((arg) => {
17595
18050
  const expr = arg.expression ?? "";
17596
- if (!expr) continue;
17597
- const masked = maskSanitizerCalls(expr, refs);
17598
- if (masked === expr) continue;
17599
- if (re.test(blankStringLiterals(masked))) return true;
18051
+ return { expr, masked: expr ? maskSanitizerCalls(expr, refs) : expr };
18052
+ });
18053
+ if (!masked.some((m) => m.masked !== m.expr)) continue;
18054
+ for (const m of masked) {
18055
+ if (!m.masked) continue;
18056
+ const text = m.masked === m.expr ? maskLengthOf(m.masked) : m.masked;
18057
+ if (re.test(blankStringLiterals(text))) return true;
17600
18058
  }
17601
18059
  }
17602
18060
  return false;