circle-ir 4.9.12 → 4.9.14

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 (28) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +25 -5
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/blocking-main-thread-pass.d.ts +19 -0
  5. package/dist/analysis/passes/blocking-main-thread-pass.d.ts.map +1 -1
  6. package/dist/analysis/passes/blocking-main-thread-pass.js +75 -2
  7. package/dist/analysis/passes/blocking-main-thread-pass.js.map +1 -1
  8. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/language-sources-pass.js +219 -18
  10. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  11. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/sink-filter-pass.js +225 -4
  13. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  14. package/dist/analysis/passes/taint-propagation-pass.d.ts +48 -0
  15. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  16. package/dist/analysis/passes/taint-propagation-pass.js +166 -1
  17. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  18. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  19. package/dist/analysis/taint-propagation.js +17 -2
  20. package/dist/analysis/taint-propagation.js.map +1 -1
  21. package/dist/browser/circle-ir.js +405 -34
  22. package/dist/core/circle-ir-core.cjs +60 -6
  23. package/dist/core/circle-ir-core.js +60 -6
  24. package/dist/core/extractors/calls.js +19 -0
  25. package/dist/core/extractors/calls.js.map +1 -1
  26. package/dist/core/extractors/dfg.js +78 -1
  27. package/dist/core/extractors/dfg.js.map +1 -1
  28. package/package.json +1 -1
@@ -6700,6 +6700,14 @@ function buildResolutionContext(tree, cache) {
6700
6700
  }
6701
6701
  }
6702
6702
  }
6703
+ const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
6704
+ for (const res of resourceDecls) {
6705
+ const typeNode = res.childForFieldName("type");
6706
+ const nameNode = res.childForFieldName("name");
6707
+ if (typeNode && nameNode) {
6708
+ context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
6709
+ }
6710
+ }
6703
6711
  const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
6704
6712
  for (const imp of imports) {
6705
6713
  const text = getNodeText(imp);
@@ -9363,6 +9371,7 @@ function buildCSharpDFG(tree, cache) {
9363
9371
  ];
9364
9372
  for (const method of methods) {
9365
9373
  scopeStack.push(/* @__PURE__ */ new Map());
9374
+ const methodDefStart = defs.length;
9366
9375
  const params = method.childForFieldName("parameters");
9367
9376
  if (params) {
9368
9377
  for (const def of extractParameterDefs(params, defIdCounter)) {
@@ -9392,6 +9401,7 @@ function buildCSharpDFG(tree, cache) {
9392
9401
  }
9393
9402
  }
9394
9403
  const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
9404
+ resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
9395
9405
  uses.push(...bodyUses.uses);
9396
9406
  useIdCounter = bodyUses.nextId;
9397
9407
  }
@@ -10101,6 +10111,30 @@ function findReachingDef(name2, scopeStack) {
10101
10111
  }
10102
10112
  return null;
10103
10113
  }
10114
+ function resolveReassignedUses(methodDefs, methodUses) {
10115
+ const defsByVar = /* @__PURE__ */ new Map();
10116
+ for (const def of methodDefs) {
10117
+ if (def.kind !== "local" && def.kind !== "param") continue;
10118
+ const existing = defsByVar.get(def.variable);
10119
+ if (existing) existing.push(def);
10120
+ else defsByVar.set(def.variable, [def]);
10121
+ }
10122
+ for (const list of defsByVar.values()) {
10123
+ if (list.length > 1) list.sort((a, b) => a.line - b.line || a.id - b.id);
10124
+ }
10125
+ for (const use of methodUses) {
10126
+ const list = defsByVar.get(use.variable);
10127
+ if (!list || list.length < 2) continue;
10128
+ let strictlyBefore;
10129
+ let sameLine;
10130
+ for (const def of list) {
10131
+ if (def.line < use.line) strictlyBefore = def;
10132
+ else if (def.line === use.line) sameLine = def;
10133
+ }
10134
+ const reaching = strictlyBefore ?? sameLine;
10135
+ if (reaching) use.def_id = reaching.id;
10136
+ }
10137
+ }
10104
10138
  function computeChains(defs, uses) {
10105
10139
  const chains = [];
10106
10140
  const defById = /* @__PURE__ */ new Map();
@@ -12346,9 +12380,27 @@ var DEFAULT_SINKS = [
12346
12380
  // Command Injection (CWE-78)
12347
12381
  { method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
12348
12382
  { method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
12349
- // ProcessBuilder constructor
12350
- { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12351
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12383
+ // ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
12384
+ // `String...` varargs (or a single List<String>), and EVERY element is a
12385
+ // command token, so no position is safe. Pinning these to `[0]` missed the
12386
+ // canonical shell-wrapper shape, where the payload is the last argument:
12387
+ //
12388
+ // new ProcessBuilder("bash", "-c", tainted) // payload at position 2
12389
+ // pb.command("bash", "-c", tainted)
12390
+ //
12391
+ // `Runtime.exec(String[])` with the identical array already fired, so the
12392
+ // taint was available and only the sink model was wrong.
12393
+ //
12394
+ // The positions are enumerated rather than left `[]` because the two layers
12395
+ // that read this field disagree about what an empty array means:
12396
+ // `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
12397
+ // so `[]` matches NOTHING, while the sink-reachability loop in
12398
+ // taint-propagation treats `length === 0` as "any argument".
12399
+ // `ProcessBuilder.start` gets away with `[]` only because it takes no
12400
+ // arguments at all. Enumerating is unambiguous in both, and matches how the
12401
+ // logging sinks in this file already spell it.
12402
+ { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
12403
+ { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
12352
12404
  // Commons Exec
12353
12405
  // Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
12354
12406
  // (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
@@ -13626,8 +13678,10 @@ var DEFAULT_SINKS = [
13626
13678
  // =========================================================================
13627
13679
  // Java CWE-Bench Enhancement Patterns (Collection/Builder)
13628
13680
  // =========================================================================
13629
- // Collection-based command injection (ProcessBuilder with List)
13630
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
13681
+ // Collection-based command injection (ProcessBuilder with List) — the
13682
+ // `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
13683
+ // above; a second copy here only risked the two drifting apart, which is
13684
+ // what happened (cognium-dev#351).
13631
13685
  // ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
13632
13686
  // string flows into it. See note above next to the Process-related cluster.
13633
13687
  // Jenkins DSL patterns
@@ -15557,7 +15611,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
15557
15611
  const stripped = expr.slice(1, -1);
15558
15612
  program = stripped.split("/").pop() ?? stripped;
15559
15613
  }
15560
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15614
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15561
15615
  "sh",
15562
15616
  "bash",
15563
15617
  "zsh",
@@ -15571,7 +15625,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
15571
15625
  "powershell.exe",
15572
15626
  "pwsh.exe"
15573
15627
  ]);
15574
- if (SHELL_PROGRAMS.has(program)) return false;
15628
+ if (SHELL_PROGRAMS2.has(program)) return false;
15575
15629
  const PACKAGE_MANAGER_EXEC_SUBCOMMANDS = {
15576
15630
  go: /* @__PURE__ */ new Set(["install", "run", "get"]),
15577
15631
  npm: /* @__PURE__ */ new Set(["install", "i", "exec"]),
@@ -15622,7 +15676,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
15622
15676
  const stripped = expr.slice(1, -1);
15623
15677
  program = stripped.split("/").pop() ?? stripped;
15624
15678
  }
15625
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15679
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15626
15680
  "sh",
15627
15681
  "bash",
15628
15682
  "zsh",
@@ -15636,7 +15690,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
15636
15690
  "powershell.exe",
15637
15691
  "pwsh.exe"
15638
15692
  ]);
15639
- if (SHELL_PROGRAMS.has(program)) return false;
15693
+ if (SHELL_PROGRAMS2.has(program)) return false;
15640
15694
  return true;
15641
15695
  }
15642
15696
  var CSHARP_SHELL_PROGRAMS = /* @__PURE__ */ new Set([
@@ -15869,7 +15923,7 @@ function isSafeRustCommandCall(call, pattern, language) {
15869
15923
  if (language !== "rust") return false;
15870
15924
  if (pattern.type !== "command_injection") return false;
15871
15925
  if (pattern.class !== void 0 && pattern.class !== "Command") return false;
15872
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15926
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15873
15927
  "sh",
15874
15928
  "bash",
15875
15929
  "zsh",
@@ -15904,13 +15958,13 @@ function isSafeRustCommandCall(call, pattern, language) {
15904
15958
  const stripped = expr.slice(1, -1);
15905
15959
  program = stripped.split("/").pop() ?? stripped;
15906
15960
  }
15907
- return !SHELL_PROGRAMS.has(program);
15961
+ return !SHELL_PROGRAMS2.has(program);
15908
15962
  }
15909
15963
  if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
15910
15964
  const receiverText = call.receiver ?? "";
15911
15965
  const program = extractProgram(receiverText);
15912
15966
  if (program === null) return false;
15913
- return !SHELL_PROGRAMS.has(program);
15967
+ return !SHELL_PROGRAMS2.has(program);
15914
15968
  }
15915
15969
  return false;
15916
15970
  }
@@ -18965,7 +19019,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
18965
19019
  function buildTaintFlow(source, sink, taintInfo) {
18966
19020
  const path = [];
18967
19021
  path.push({
18968
- variable: taintInfo.variable,
19022
+ variable: source.variable ?? taintInfo.variable,
18969
19023
  line: source.line,
18970
19024
  type: "source",
18971
19025
  description: `Tainted data enters via ${source.type}`
@@ -27163,6 +27217,7 @@ var LanguageSourcesPass = class {
27163
27217
  additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
27164
27218
  additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
27165
27219
  additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
27220
+ additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, "python"));
27166
27221
  const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
27167
27222
  for (const finding of pyMisconfigFindings) {
27168
27223
  ctx.addFinding(finding);
@@ -27321,6 +27376,11 @@ var LanguageSourcesPass = class {
27321
27376
  ctx.addFinding(finding);
27322
27377
  }
27323
27378
  }
27379
+ if (language === "csharp") {
27380
+ additionalSanitizers.push(
27381
+ ...findCSharpFullPathStartsWithGuardSanitizers(code)
27382
+ );
27383
+ }
27324
27384
  if (language === "java") {
27325
27385
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
27326
27386
  additionalSanitizers.push(
@@ -27329,6 +27389,7 @@ var LanguageSourcesPass = class {
27329
27389
  additionalSanitizers.push(...findJavaPathGetFileNameSanitizers(code));
27330
27390
  additionalSanitizers.push(...findJavaCanonicalPathStartsWithGuardSanitizers(code));
27331
27391
  additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
27392
+ additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, "java"));
27332
27393
  additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
27333
27394
  for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
27334
27395
  ctx.addFinding(finding);
@@ -29896,7 +29957,7 @@ function findJsPathResolveStartsWithGuardSanitizers(code) {
29896
29957
  let guardLine = -1;
29897
29958
  for (let l = c.line; l < Math.min(lines.length, c.line + 6); l++) {
29898
29959
  if (!guardRe.test(lines[l])) continue;
29899
- if (terminatorRe.test(lines[l]) || l + 1 < lines.length && terminatorRe.test(lines[l + 1])) {
29960
+ if (guardRejects(lines, l, terminatorRe, 2)) {
29900
29961
  guardLine = l + 1;
29901
29962
  break;
29902
29963
  }
@@ -30005,7 +30066,7 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
30005
30066
  let guardLine = -1;
30006
30067
  for (let l = c.line; l < Math.min(lines.length, c.line + 6); l++) {
30007
30068
  if (!guardRe.test(lines[l])) continue;
30008
- if (terminatorRe.test(lines[l]) || l + 1 < lines.length && terminatorRe.test(lines[l + 1])) {
30069
+ if (guardRejects(lines, l, terminatorRe, 2)) {
30009
30070
  guardLine = l + 1;
30010
30071
  break;
30011
30072
  }
@@ -30039,17 +30100,7 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
30039
30100
  for (let i2 = 0; i2 < lines.length; i2++) {
30040
30101
  const m = guardRe.exec(lines[i2]);
30041
30102
  if (!m) continue;
30042
- let hasTerminator = terminatorRe.test(lines[i2]);
30043
- if (!hasTerminator) {
30044
- for (let j = i2 + 1; j < Math.min(lines.length, i2 + 6); j++) {
30045
- if (terminatorRe.test(lines[j])) {
30046
- hasTerminator = true;
30047
- break;
30048
- }
30049
- if (lines[j].includes("}")) break;
30050
- }
30051
- }
30052
- if (!hasTerminator) continue;
30103
+ if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
30053
30104
  const guarded = /* @__PURE__ */ new Set([m[1]]);
30054
30105
  const mentions = (t) => {
30055
30106
  for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
@@ -30074,6 +30125,58 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
30074
30125
  }
30075
30126
  return sanitizers;
30076
30127
  }
30128
+ function guardRejects(lines, i2, terminatorRe, window2) {
30129
+ if (terminatorRe.test(lines[i2])) return true;
30130
+ const opens = (lines[i2].match(/\{/g) ?? []).length;
30131
+ const closes = (lines[i2].match(/\}/g) ?? []).length;
30132
+ if (opens > 0 && closes >= opens) return false;
30133
+ for (let j = i2 + 1; j < Math.min(lines.length, i2 + window2); j++) {
30134
+ if (terminatorRe.test(lines[j])) return true;
30135
+ if (lines[j].includes("}")) return false;
30136
+ }
30137
+ return false;
30138
+ }
30139
+ function findCSharpFullPathStartsWithGuardSanitizers(code) {
30140
+ const sanitizers = [];
30141
+ const lines = code.split("\n");
30142
+ const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
30143
+ const fullPathRe = /\bPath\s*\.\s*GetFullPath\s*\(/;
30144
+ const canonicalVars = /* @__PURE__ */ new Set();
30145
+ for (const line of lines) {
30146
+ const a = assignRe.exec(line);
30147
+ if (a && fullPathRe.test(a[2])) canonicalVars.add(a[1]);
30148
+ }
30149
+ if (canonicalVars.size === 0) return sanitizers;
30150
+ const guardRe = /\bif\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\(/;
30151
+ const terminatorRe = /\b(throw|return)\b/;
30152
+ for (let i2 = 0; i2 < lines.length; i2++) {
30153
+ const m = guardRe.exec(lines[i2]);
30154
+ if (!m || !canonicalVars.has(m[1])) continue;
30155
+ if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
30156
+ const guarded = /* @__PURE__ */ new Set([m[1]]);
30157
+ const mentions = (t) => {
30158
+ for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
30159
+ return false;
30160
+ };
30161
+ for (let l = 0; l < lines.length; l++) {
30162
+ const lt = lines[l];
30163
+ const a = assignRe.exec(lt);
30164
+ if (a) {
30165
+ if (mentions(a[2])) guarded.add(a[1]);
30166
+ else if (a[1] !== m[1]) guarded.delete(a[1]);
30167
+ }
30168
+ if (mentions(lt)) {
30169
+ sanitizers.push({
30170
+ type: "csharp_fullpath_startswith_guard",
30171
+ method: "GetFullPath",
30172
+ line: l + 1,
30173
+ sanitizes: ["path_traversal", "external_taint_escape"]
30174
+ });
30175
+ }
30176
+ }
30177
+ }
30178
+ return sanitizers;
30179
+ }
30077
30180
  function findJavaPathGetFileNameSanitizers(code) {
30078
30181
  const sanitizers = [];
30079
30182
  const lines = code.split("\n");
@@ -30112,6 +30215,34 @@ function findJavaPathGetFileNameSanitizers(code) {
30112
30215
  }
30113
30216
  return sanitizers;
30114
30217
  }
30218
+ function findCrlfWrapperFunctionSanitizers(code, language) {
30219
+ const sanitizers = [];
30220
+ const lines = code.split("\n");
30221
+ const declRe = language === "java" ? /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:String|CharSequence)\s+([A-Za-z_]\w*)\s*\(/ : /^\s*def\s+([A-Za-z_]\w*)\s*\(/;
30222
+ const stripRe = language === "java" ? /\.\s*replace(?:All)?\s*\(\s*(?:"[^"]*\\[rnt]|'\\[rnt]')/ : /(?:re\s*\.\s*sub\s*\(\s*r?["'][^"']*\\[rnt]|\.\s*replace\s*\(\s*["']\\[rnt])/;
30223
+ const bodyWindow = 10;
30224
+ const wrappers = /* @__PURE__ */ new Set();
30225
+ for (let i2 = 0; i2 < lines.length; i2++) {
30226
+ const m = declRe.exec(lines[i2]);
30227
+ if (!m) continue;
30228
+ const body2 = lines.slice(i2, Math.min(lines.length, i2 + bodyWindow)).join("\n");
30229
+ if (stripRe.test(body2)) wrappers.add(m[1]);
30230
+ }
30231
+ if (wrappers.size === 0) return sanitizers;
30232
+ for (let i2 = 0; i2 < lines.length; i2++) {
30233
+ for (const name2 of wrappers) {
30234
+ if (declRe.test(lines[i2]) && new RegExp(`\\b${name2}\\s*\\(`).test(lines[i2])) continue;
30235
+ if (!new RegExp(`\\b${name2}\\s*\\(`).test(lines[i2])) continue;
30236
+ sanitizers.push({
30237
+ type: language === "java" ? "java_wrapper_crlf_strip" : "python_wrapper_crlf_strip",
30238
+ method: name2,
30239
+ line: i2 + 1,
30240
+ sanitizes: ["log_injection", "external_taint_escape"]
30241
+ });
30242
+ }
30243
+ }
30244
+ return sanitizers;
30245
+ }
30115
30246
  function findJavaInlineCrlfStripLogSanitizers(code) {
30116
30247
  const sanitizers = [];
30117
30248
  const lines = code.split("\n");
@@ -33778,6 +33909,29 @@ var PROTOCOL_CLIENT_PACKAGES = [
33778
33909
  ];
33779
33910
  var OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executor|Exec|Launcher|ProcStarter|ProcessExecutor|RuntimeUtil)\s*[.(]/;
33780
33911
  var PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
33912
+ var PROCESS_BUILDER_COMMAND_ARGV_FORM_RE = /\.\s*command\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
33913
+ var SHELL_PROGRAMS = /* @__PURE__ */ new Set([
33914
+ "sh",
33915
+ "bash",
33916
+ "zsh",
33917
+ "ksh",
33918
+ "dash",
33919
+ "ash",
33920
+ "csh",
33921
+ "tcsh",
33922
+ "fish",
33923
+ "cmd",
33924
+ "command",
33925
+ "powershell",
33926
+ "pwsh"
33927
+ ]);
33928
+ var ARGV_FIRST_LITERAL_RE = /(?:\bnew\s+ProcessBuilder\s*\(|\.\s*command\s*\()\s*(?:Arrays\.asList\s*\(|List\.of\s*\(|Collections\.singletonList\s*\(|new\s+ArrayList\s*(?:<[^>]*>)?\s*\(\s*(?:Arrays\.asList\s*\()?|new\s+String\s*\[\s*\]\s*\{)?\s*"([^"]*)"/;
33929
+ function argvFirstLiteralIsShell(lineText) {
33930
+ const m = ARGV_FIRST_LITERAL_RE.exec(lineText);
33931
+ if (!m) return false;
33932
+ const base = m[1].split(/[\\/]/).pop().replace(/\.exe$/i, "").toLowerCase();
33933
+ return SHELL_PROGRAMS.has(base);
33934
+ }
33781
33935
  var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b[^;]*;\s*(?:\/\/.*|\/\*.*)?$/;
33782
33936
  var JEXL_ENGINE_TYPES = /* @__PURE__ */ new Set(["JexlEngine", "Jexl", "JxltEngine"]);
33783
33937
  var JEXL_EXPRESSION_TYPE_RE = /(?:Jexl)?(?:Expression|Script|Template)(?:Script)?$/;
@@ -33924,10 +34078,39 @@ function jsSsrfHostGuardedLines(code) {
33924
34078
  }
33925
34079
  return covered;
33926
34080
  }
34081
+ var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
34082
+ function isUnwrittenLocalMapKeyRead(code, source) {
34083
+ if (source.type !== "plugin_param") return false;
34084
+ const lines = code.split("\n");
34085
+ const lineText = lines[source.line - 1] ?? "";
34086
+ const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(\s*"([^"]*)"\s*\)/.exec(lineText);
34087
+ if (!read) return false;
34088
+ const [, recv, key] = read;
34089
+ const ctorRe = new RegExp(
34090
+ `\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`
34091
+ );
34092
+ if (!lines.some((l) => ctorRe.test(l))) return false;
34093
+ const escapeRe2 = new RegExp(`\\w\\s*\\([^)]*(?<![\\w.])${recv}(?![\\w.])[^)]*\\)`);
34094
+ for (const l of lines) {
34095
+ if (!escapeRe2.test(l)) continue;
34096
+ const stripped = l.replace(new RegExp(`\\b${recv}\\s*\\.\\s*\\w+\\s*\\(`, "g"), "X(");
34097
+ if (escapeRe2.test(stripped)) return false;
34098
+ }
34099
+ const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`);
34100
+ if (lines.some((l) => putRe.test(l))) return false;
34101
+ return true;
34102
+ }
33927
34103
  var CSHARP_SANITIZER_RES = [
33928
34104
  { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
33929
34105
  { re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
33930
- { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" }
34106
+ { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
34107
+ // An allowlist strip leaves only inert characters, so it covers every family
34108
+ // whose exploitation needs a metacharacter. Each has a fixture; see
34109
+ // `issue-272-csharp-allowlist-strip.test.ts`.
34110
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
34111
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
34112
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
34113
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
33931
34114
  ];
33932
34115
  function csharpSanitizedVarsByType(code) {
33933
34116
  const lines = code.split("\n");
@@ -34169,9 +34352,10 @@ var SinkFilterPass = class {
34169
34352
  const constProp = ctx.getResult("constant-propagation");
34170
34353
  const langSources = ctx.getResult("language-sources");
34171
34354
  const mergedSources = [...taintMatcher.sources, ...langSources.additionalSources];
34172
- const sources = ctx.code && ctx.language ? mergedSources.filter(
34355
+ const executableSources = ctx.code && ctx.language ? mergedSources.filter(
34173
34356
  (s) => !isNonExecutableSourceLine(ctx.code, s.line, ctx.language)
34174
34357
  ) : mergedSources;
34358
+ const sources = ctx.code ? executableSources.filter((s) => !isUnwrittenLocalMapKeyRead(ctx.code, s)) : executableSources;
34175
34359
  const sinks = [...taintMatcher.sinks];
34176
34360
  for (const s of langSources.additionalSinks) {
34177
34361
  if (!sinks.some((x) => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
@@ -34439,10 +34623,19 @@ var SinkFilterPass = class {
34439
34623
  const irTypes = ctx.graph.ir.types;
34440
34624
  filtered = filtered.filter((sink) => {
34441
34625
  if (sink.type !== "command_injection") return true;
34442
- if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
34626
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start" && sink.method !== "command") {
34627
+ return true;
34628
+ }
34443
34629
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
34444
- if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
34445
- if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
34630
+ const isCtorLine = /\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText);
34631
+ const isCommandLine = /\.\s*command\s*\(/.test(sinkLineText);
34632
+ if (!isCtorLine && !isCommandLine) return true;
34633
+ if (argvFirstLiteralIsShell(sinkLineText)) return true;
34634
+ if (isCtorLine && PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
34635
+ if (isCommandLine && PROCESS_BUILDER_COMMAND_ARGV_FORM_RE.test(sinkLineText)) {
34636
+ return false;
34637
+ }
34638
+ if (!isCtorLine) return true;
34446
34639
  const ctorCall = irCalls.find(
34447
34640
  (c) => c.method_name === "ProcessBuilder" && c.location.line === sink.line && (c.receiver === null || c.receiver === void 0)
34448
34641
  );
@@ -34746,6 +34939,36 @@ var SinkFilterPass = class {
34746
34939
  return true;
34747
34940
  });
34748
34941
  }
34942
+ if (language === "csharp") {
34943
+ const sourceLines = ctx.code.split("\n");
34944
+ const constBaseClients = /* @__PURE__ */ new Set();
34945
+ const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*\{[^}]*\bBaseAddress\s*=\s*new\s+Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
34946
+ for (const line of sourceLines) {
34947
+ const m = declRe.exec(line);
34948
+ if (m) constBaseClients.add(m[1]);
34949
+ }
34950
+ if (constBaseClients.size > 0) {
34951
+ const relativeLiteralArgRe = /\.\s*(?:Get|Post|Put|Patch|Delete|Send|GetString|GetByteArray|GetStream)\w*Async\s*\(\s*(?:@?\$?)"([^"]*)"/;
34952
+ filtered = filtered.filter((sink) => {
34953
+ if (sink.type !== "ssrf") return true;
34954
+ const text = sourceLines[sink.line - 1] ?? "";
34955
+ let onConstClient = false;
34956
+ for (const v of constBaseClients) {
34957
+ if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
34958
+ onConstClient = true;
34959
+ break;
34960
+ }
34961
+ }
34962
+ if (!onConstClient) return true;
34963
+ const m = relativeLiteralArgRe.exec(text);
34964
+ if (!m) return true;
34965
+ const lit = m[1];
34966
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit)) return true;
34967
+ if (lit.startsWith("//")) return true;
34968
+ return false;
34969
+ });
34970
+ }
34971
+ }
34749
34972
  if (["javascript", "typescript"].includes(language)) {
34750
34973
  const sourceLines = ctx.code.split("\n");
34751
34974
  filtered = filtered.filter((sink) => {
@@ -36983,6 +37206,103 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
36983
37206
  }
36984
37207
  return false;
36985
37208
  }
37209
+ var MASKABLE_STRING_LANGUAGES = /* @__PURE__ */ new Set(["python", "javascript", "typescript"]);
37210
+ function maskStringLiteralsForTokenScan(expr) {
37211
+ let out2 = "";
37212
+ let i2 = 0;
37213
+ const braceExprs = (body2) => {
37214
+ let acc = "";
37215
+ for (let j = 0; j < body2.length; ) {
37216
+ if (body2[j] === "{" && body2[j + 1] === "{") {
37217
+ j += 2;
37218
+ continue;
37219
+ }
37220
+ if (body2[j] === "}" && body2[j + 1] === "}") {
37221
+ j += 2;
37222
+ continue;
37223
+ }
37224
+ if (body2[j] === "{") {
37225
+ j++;
37226
+ let depth = 1;
37227
+ let inner = "";
37228
+ while (j < body2.length && depth > 0) {
37229
+ if (body2[j] === "{") depth++;
37230
+ else if (body2[j] === "}") {
37231
+ depth--;
37232
+ if (depth === 0) break;
37233
+ }
37234
+ inner += body2[j];
37235
+ j++;
37236
+ }
37237
+ j++;
37238
+ acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
37239
+ continue;
37240
+ }
37241
+ j++;
37242
+ }
37243
+ return acc;
37244
+ };
37245
+ while (i2 < expr.length) {
37246
+ const ch = expr[i2];
37247
+ if (ch === "`") {
37248
+ i2++;
37249
+ let acc = "";
37250
+ while (i2 < expr.length && expr[i2] !== "`") {
37251
+ if (expr[i2] === "\\") {
37252
+ i2 += 2;
37253
+ continue;
37254
+ }
37255
+ if (expr[i2] === "$" && expr[i2 + 1] === "{") {
37256
+ i2 += 2;
37257
+ let depth = 1;
37258
+ let inner = "";
37259
+ while (i2 < expr.length && depth > 0) {
37260
+ if (expr[i2] === "{") depth++;
37261
+ else if (expr[i2] === "}") {
37262
+ depth--;
37263
+ if (depth === 0) break;
37264
+ }
37265
+ inner += expr[i2];
37266
+ i2++;
37267
+ }
37268
+ i2++;
37269
+ acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
37270
+ continue;
37271
+ }
37272
+ i2++;
37273
+ }
37274
+ i2++;
37275
+ out2 += " " + acc + " ";
37276
+ continue;
37277
+ }
37278
+ if (ch === '"' || ch === "'") {
37279
+ const pm = /[A-Za-z_][A-Za-z0-9_]*$/.exec(out2);
37280
+ const prefix = pm ? pm[0] : "";
37281
+ const isPrefix = prefix.length > 0 && /^[rRbBuUfF]+$/.test(prefix);
37282
+ const isF = isPrefix && /[fF]/.test(prefix);
37283
+ if (isPrefix) out2 = out2.slice(0, out2.length - prefix.length) + " ";
37284
+ const triple = expr.startsWith(ch.repeat(3), i2);
37285
+ const delim = triple ? ch.repeat(3) : ch;
37286
+ i2 += delim.length;
37287
+ let body2 = "";
37288
+ while (i2 < expr.length && !expr.startsWith(delim, i2)) {
37289
+ if (expr[i2] === "\\") {
37290
+ body2 += " ";
37291
+ i2 += 2;
37292
+ continue;
37293
+ }
37294
+ body2 += expr[i2];
37295
+ i2++;
37296
+ }
37297
+ i2 += delim.length;
37298
+ out2 += isF ? " " + braceExprs(body2) + " " : " ";
37299
+ continue;
37300
+ }
37301
+ out2 += ch;
37302
+ i2++;
37303
+ }
37304
+ return out2;
37305
+ }
36986
37306
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
36987
37307
  const flows = [];
36988
37308
  const sourcesWithVar = sources.filter(
@@ -37336,7 +37656,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37336
37656
  }
37337
37657
  const expr = arg.expression;
37338
37658
  if (!expr) continue;
37339
- const exprIdentMatches = expr.match(/[\p{L}\p{N}_]+/gu);
37659
+ const scannable = language !== void 0 && MASKABLE_STRING_LANGUAGES.has(language) ? maskStringLiteralsForTokenScan(expr) : expr;
37660
+ const exprIdentMatches = scannable.match(/[\p{L}\p{N}_]+/gu);
37340
37661
  let candidates = complexSources.length > 0 ? complexSources.slice() : null;
37341
37662
  if (exprIdentMatches) {
37342
37663
  for (const id of new Set(exprIdentMatches)) {
@@ -40575,6 +40896,18 @@ var CRYPTO_BLOCKING_METHODS = /* @__PURE__ */ new Set([
40575
40896
  "deriveKeySync"
40576
40897
  ]);
40577
40898
  var SYNC_SUFFIX_RE2 = /Sync$/;
40899
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
40900
+ "get",
40901
+ "post",
40902
+ "put",
40903
+ "patch",
40904
+ "delete",
40905
+ "all",
40906
+ "options",
40907
+ "head",
40908
+ "use"
40909
+ ]);
40910
+ var FN_ARG_PARAMS_RE = /^(?:async\s+)?(?:function\s*[\w$]*\s*)?\(([^)]*)\)\s*(?:=>|\{)|^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/;
40578
40911
  var BlockingMainThreadPass = class {
40579
40912
  name = "blocking-main-thread";
40580
40913
  category = "performance";
@@ -40596,7 +40929,10 @@ var BlockingMainThreadPass = class {
40596
40929
  }
40597
40930
  }
40598
40931
  }
40599
- if (handlerRanges.length === 0) return { blockingInHandlers: [] };
40932
+ const inlineHandlers = this.collectInlineHandlerNames(graph.ir.calls);
40933
+ if (handlerRanges.length === 0 && inlineHandlers.size === 0) {
40934
+ return { blockingInHandlers: [] };
40935
+ }
40600
40936
  const blockingInHandlers = [];
40601
40937
  for (const call of graph.ir.calls) {
40602
40938
  const name2 = call.method_name;
@@ -40604,7 +40940,8 @@ var BlockingMainThreadPass = class {
40604
40940
  const isSyncSuffix = SYNC_SUFFIX_RE2.test(name2);
40605
40941
  if (!isCrypto && !isSyncSuffix) continue;
40606
40942
  const line = call.location.line;
40607
- const range = handlerRanges.find((r) => line >= r.start && line <= r.end);
40943
+ const enclosing = call.in_method ?? null;
40944
+ const range = handlerRanges.find((r) => line >= r.start && line <= r.end) ?? (enclosing && inlineHandlers.has(enclosing) ? { start: line, end: line, name: enclosing } : void 0);
40608
40945
  if (!range) continue;
40609
40946
  const reason = isCrypto ? "crypto" : "sync-suffix";
40610
40947
  blockingInHandlers.push({ line, method: name2, handler: range.name, reason });
@@ -40625,6 +40962,40 @@ var BlockingMainThreadPass = class {
40625
40962
  }
40626
40963
  return { blockingInHandlers };
40627
40964
  }
40965
+ /**
40966
+ * Synthetic `<verb>_handler` names that really do belong to an HTTP route.
40967
+ *
40968
+ * The extractor tags *any* function argument of *any* member-expression call
40969
+ * this way, so `items.map(x => …)` becomes `map_handler` and
40970
+ * `cache.get(k, () => …)` becomes `get_handler`. Accepting every
40971
+ * `*_handler` would pull ordinary callbacks into a pass that is explicitly
40972
+ * about the HTTP request path, so two conditions are required: the method is
40973
+ * a router verb, and the callback's own parameters look like a request
40974
+ * handler's — the same `HANDLER_PARAM_NAMES` test used for methods.
40975
+ *
40976
+ * Known limitation: `in_method` carries no receiver, so two callbacks that
40977
+ * share a verb in one file are indistinguishable. A file containing both
40978
+ * `app.get('/x', (req, res) => …)` and `cache.get(k, () => …)` will treat a
40979
+ * blocking call in the second as in-handler. Both conditions still have to
40980
+ * hold for the verb to register at all, which keeps that to files already
40981
+ * mounting a real route of the same verb.
40982
+ */
40983
+ collectInlineHandlerNames(calls) {
40984
+ const names = /* @__PURE__ */ new Set();
40985
+ for (const call of calls) {
40986
+ if (!ROUTER_METHODS.has(call.method_name)) continue;
40987
+ for (const arg of call.arguments) {
40988
+ const match = FN_ARG_PARAMS_RE.exec((arg.expression ?? "").trim());
40989
+ if (!match) continue;
40990
+ const params = (match[1] ?? match[2] ?? "").split(",").map((p) => p.trim().toLowerCase()).filter((p) => p.length > 0);
40991
+ if (params.some((p) => HANDLER_PARAM_NAMES.has(p))) {
40992
+ names.add(`${call.method_name}_handler`);
40993
+ break;
40994
+ }
40995
+ }
40996
+ }
40997
+ return names;
40998
+ }
40628
40999
  isRequestHandler(method) {
40629
41000
  if (method.annotations.some((a) => HTTP_DECORATORS.has(a))) return true;
40630
41001
  if (HANDLER_METHOD_NAMES.has(method.name)) return true;