circle-ir 4.6.0 → 4.7.1

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 (33) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +25 -3
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/interprocedural.d.ts.map +1 -1
  5. package/dist/analysis/interprocedural.js +16 -1
  6. package/dist/analysis/interprocedural.js.map +1 -1
  7. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  8. package/dist/analysis/passes/language-sources-pass.js +50 -3
  9. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  10. package/dist/analysis/passes/missing-public-doc-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/missing-public-doc-pass.js +6 -1
  12. package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -1
  13. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/sink-filter-pass.js +10 -0
  15. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  16. package/dist/analysis/passes/unrestricted-file-upload-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/unrestricted-file-upload-pass.js +6 -4
  18. package/dist/analysis/passes/unrestricted-file-upload-pass.js.map +1 -1
  19. package/dist/analysis/passes/unused-variable-pass.d.ts.map +1 -1
  20. package/dist/analysis/passes/unused-variable-pass.js +69 -20
  21. package/dist/analysis/passes/unused-variable-pass.js.map +1 -1
  22. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  23. package/dist/analysis/passes/weak-crypto-pass.js +66 -0
  24. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  25. package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
  26. package/dist/analysis/passes/weak-hash-pass.js +46 -0
  27. package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
  28. package/dist/browser/circle-ir.js +203 -25
  29. package/dist/core/circle-ir-core.cjs +29 -7
  30. package/dist/core/circle-ir-core.js +29 -7
  31. package/dist/core/extractors/dfg.js +6 -3
  32. package/dist/core/extractors/dfg.js.map +1 -1
  33. package/package.json +1 -1
@@ -9918,15 +9918,15 @@ function computeChains(defs, uses) {
9918
9918
  existing.push(use);
9919
9919
  usesByLine.set(use.line, existing);
9920
9920
  }
9921
+ const seenChains = /* @__PURE__ */ new Set();
9921
9922
  for (const def of defs) {
9922
9923
  if (def.kind === "local") {
9923
9924
  const usesOnLine = usesByLine.get(def.line) ?? [];
9924
9925
  for (const use of usesOnLine) {
9925
9926
  if (use.def_id !== null && use.def_id !== def.id) {
9926
- const exists = chains.some(
9927
- (c) => c.from_def === use.def_id && c.to_def === def.id && c.via === use.variable
9928
- );
9929
- if (!exists) {
9927
+ const key = `${use.def_id}\0${def.id}\0${use.variable}`;
9928
+ if (!seenChains.has(key)) {
9929
+ seenChains.add(key);
9930
9930
  chains.push({
9931
9931
  from_def: use.def_id,
9932
9932
  to_def: def.id,
@@ -12501,8 +12501,11 @@ var DEFAULT_SINKS = [
12501
12501
  // SAX handler sinks (can lead to XSS in parsed content)
12502
12502
  { method: "startElement", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2] },
12503
12503
  { method: "characters", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
12504
- // Template output sinks
12505
- { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
12504
+ // Template output sinks. `output` is classless (Java template engines /
12505
+ // Velocity), but Rust `std::process::Command::…output()` is process
12506
+ // execution, not HTML — it already fires the CWE-78 command_injection sink,
12507
+ // so exclude Rust here to drop the spurious xss (cognium-dev#146).
12508
+ { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], exclude_languages: ["rust"] },
12506
12509
  { method: "setOutput", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
12507
12510
  { method: "writeAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
12508
12511
  // AntiSamy specific (SAX filters)
@@ -13793,7 +13796,10 @@ var DEFAULT_SINKS = [
13793
13796
  { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13794
13797
  { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13795
13798
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13796
- { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13799
+ // Both the single-string overload `Process.Start("sh -c " + x)` and the
13800
+ // `(fileName, arguments)` overload `Process.Start("/bin/sh", "-c " + x)` are
13801
+ // injectable — the second is the argv path where taint rides arg[1] (#276).
13802
+ { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13797
13803
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13798
13804
  // C# path traversal — System.IO file APIs (CWE-22). Distinctive method names.
13799
13805
  { method: "ReadAllText", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13834,6 +13840,22 @@ var DEFAULT_SINKS = [
13834
13840
  // filter is the constructor argument.
13835
13841
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13836
13842
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13843
+ // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13844
+ // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13845
+ // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
13846
+ // `TypedResults.Redirect(url)` also route through the same method name. Taint-
13847
+ // gated, so a constant URL never fires. `LocalRedirect` is deliberately NOT
13848
+ // registered — it rejects non-local URLs and is the documented mitigation.
13849
+ { method: "Redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13850
+ { method: "RedirectPermanent", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13851
+ // C# CRLF / HTTP response-header injection — classic ASP.NET header writers
13852
+ // (CWE-113, cognium-dev#273/#275). The injectable value is the second arg.
13853
+ { method: "AddHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13854
+ { method: "AppendHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13855
+ // C# NoSQL injection — MongoDB filter built from raw JSON (CWE-943,
13856
+ // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13857
+ // attacker-controlled query document. Class-scoped (static receiver resolves).
13858
+ { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13837
13859
  // C# XPath injection — System.Xml.XPath (CWE-643). Distinctive selector methods.
13838
13860
  { method: "SelectSingleNode", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13839
13861
  { method: "SelectNodes", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -18562,6 +18584,26 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
18562
18584
  "Command",
18563
18585
  "CommandContext"
18564
18586
  ]);
18587
+ const ormQueryBuilderMethods = /* @__PURE__ */ new Set([
18588
+ "findMany",
18589
+ "findFirst",
18590
+ "findUnique",
18591
+ "findUniqueOrThrow",
18592
+ "findFirstOrThrow",
18593
+ "findOne",
18594
+ "findAll",
18595
+ "findByPk",
18596
+ "findAndCountAll",
18597
+ "findBy",
18598
+ "findOneBy",
18599
+ "where",
18600
+ "andWhere",
18601
+ "orWhere",
18602
+ "whereIn",
18603
+ "whereNot",
18604
+ "first",
18605
+ "bind"
18606
+ ]);
18565
18607
  const sanitizerMethods = /* @__PURE__ */ new Set();
18566
18608
  for (const san of sanitizers) {
18567
18609
  sanitizerMethods.add(san.method);
@@ -18586,7 +18628,7 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
18586
18628
  }
18587
18629
  const targetMethod = getMethodNode(methodNodes, call.method_name);
18588
18630
  if (!targetMethod) {
18589
- if (taintedArgPositions.length > 0 && !collectionMethods.has(call.method_name) && !sanitizerMethods.has(call.method_name) && !safeUtilityMethods.has(call.method_name)) {
18631
+ if (taintedArgPositions.length > 0 && !collectionMethods.has(call.method_name) && !sanitizerMethods.has(call.method_name) && !safeUtilityMethods.has(call.method_name) && !ormQueryBuilderMethods.has(call.method_name)) {
18590
18632
  const isBash = graph.ir.meta.language === "bash";
18591
18633
  const bashSafeBuiltins = /* @__PURE__ */ new Set([
18592
18634
  "echo",
@@ -26375,6 +26417,7 @@ var LanguageSourcesPass = class {
26375
26417
  additionalSources.push(...findSetterChainSources(types, code, language));
26376
26418
  additionalSources.push(...findJavaScriptAssignmentSources(code, language));
26377
26419
  additionalSources.push(...findCSharpRequestSources(code, language));
26420
+ additionalSources.push(...findGoArgvSources(code, language));
26378
26421
  const jsDOMSinks = findJavaScriptDOMSinks(code, language);
26379
26422
  for (const s of jsDOMSinks) {
26380
26423
  const alreadyExists = additionalSinks.some((x) => x.line === s.line && x.cwe === s.cwe);
@@ -27029,6 +27072,31 @@ function findSetterChainSources(types, sourceCode, language) {
27029
27072
  }
27030
27073
  return sources;
27031
27074
  }
27075
+ function findGoArgvSources(sourceCode, language) {
27076
+ if (language !== "go") return [];
27077
+ const sources = [];
27078
+ const lines = sourceCode.split("\n");
27079
+ const rangeRe = /\bfor\b[^;{]*?,\s*([A-Za-z_]\w*)\s*:?=\s*range\s+os\.Args\b/;
27080
+ const assignRe = /\b([A-Za-z_]\w*)\s*:?=\s*os\.Args\b/;
27081
+ for (let i2 = 0; i2 < lines.length; i2++) {
27082
+ const line = lines[i2];
27083
+ const rm = rangeRe.exec(line);
27084
+ const m = rm ?? assignRe.exec(line);
27085
+ if (!m) continue;
27086
+ const varName = m[1];
27087
+ const lineNumber = i2 + 1;
27088
+ if (sources.some((s) => s.line === lineNumber && s.variable === varName)) continue;
27089
+ sources.push({
27090
+ type: "io_input",
27091
+ location: `${varName} = os.Args`,
27092
+ severity: "high",
27093
+ line: lineNumber,
27094
+ confidence: 1,
27095
+ variable: varName
27096
+ });
27097
+ }
27098
+ return sources;
27099
+ }
27032
27100
  function findCSharpRequestSources(sourceCode, language) {
27033
27101
  if (language !== "csharp") return [];
27034
27102
  const sources = [];
@@ -27036,11 +27104,12 @@ function findCSharpRequestSources(sourceCode, language) {
27036
27104
  const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
27037
27105
  const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
27038
27106
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
27107
+ const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
27039
27108
  for (let i2 = 0; i2 < lines.length; i2++) {
27040
27109
  const m = assignRe.exec(lines[i2]);
27041
27110
  if (!m) continue;
27042
27111
  const [, varName, rhs] = m;
27043
- const type = requestReadRe.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : null;
27112
+ const type = requestReadRe.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
27044
27113
  if (!type) continue;
27045
27114
  const lineNumber = i2 + 1;
27046
27115
  if (sources.some((s) => s.line === lineNumber && s.variable === varName)) continue;
@@ -27872,7 +27941,7 @@ function findBashTaintSources(sourceCode, dfg) {
27872
27941
  });
27873
27942
  }
27874
27943
  }
27875
- const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
27944
+ const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})(?:[:#%/^,@!*+?=-][^}]*)?\}/g;
27876
27945
  let em;
27877
27946
  while ((em = envRe.exec(line)) !== null) {
27878
27947
  const envVar = em[1] ?? em[2];
@@ -33928,7 +33997,9 @@ var SinkFilterPass = class {
33928
33997
  if (language === "csharp") {
33929
33998
  const sourceLines = ctx.code.split("\n");
33930
33999
  const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
34000
+ const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
33931
34001
  filtered = filtered.filter((sink) => {
34002
+ if (sink.type === "xxe" && xxeHardened) return false;
33932
34003
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
33933
34004
  for (const { re, type } of CSHARP_SANITIZER_RES) {
33934
34005
  if (type === sink.type && re.test(sinkLineText)) return false;
@@ -37084,6 +37155,8 @@ function isPublicMethod(method, language) {
37084
37155
  switch (language) {
37085
37156
  case "java":
37086
37157
  return method.modifiers.includes("public");
37158
+ case "csharp":
37159
+ return method.modifiers.includes("public");
37087
37160
  case "javascript":
37088
37161
  case "typescript":
37089
37162
  case "tsx":
@@ -37105,7 +37178,7 @@ var MissingPublicDocPass = class {
37105
37178
  if (UTIL_DIR_RE.test(graph.ir.meta.file)) {
37106
37179
  return { missingDocMethods: [], missingDocTypes: [] };
37107
37180
  }
37108
- if (!["java", "javascript", "typescript", "tsx", "python"].includes(language)) {
37181
+ if (!["java", "javascript", "typescript", "tsx", "python", "csharp"].includes(language)) {
37109
37182
  return { missingDocMethods: [], missingDocTypes: [] };
37110
37183
  }
37111
37184
  const lines = code.split("\n");
@@ -38101,6 +38174,7 @@ var SKIP_NAMES3 = /* @__PURE__ */ new Set([
38101
38174
  "unknown",
38102
38175
  "bigint"
38103
38176
  ]);
38177
+ var EMPTY_LINES = [];
38104
38178
  var UnusedVariablePass = class {
38105
38179
  name = "unused-variable";
38106
38180
  category = "reliability";
@@ -38113,6 +38187,29 @@ var UnusedVariablePass = class {
38113
38187
  const codeLines = code.split("\n");
38114
38188
  const unusedVars = [];
38115
38189
  const reported = /* @__PURE__ */ new Set();
38190
+ const defsByVar = /* @__PURE__ */ new Map();
38191
+ for (const d of graph.ir.dfg.defs) {
38192
+ const arr = defsByVar.get(d.variable);
38193
+ if (arr) arr.push(d);
38194
+ else defsByVar.set(d.variable, [d]);
38195
+ }
38196
+ const nameLines = /* @__PURE__ */ new Map();
38197
+ const idRe = /[A-Za-z_][A-Za-z0-9_]*/g;
38198
+ for (let i2 = 0; i2 < codeLines.length; i2++) {
38199
+ const line = codeLines[i2];
38200
+ idRe.lastIndex = 0;
38201
+ let seen = null;
38202
+ let m;
38203
+ while ((m = idRe.exec(line)) !== null) {
38204
+ const w = m[0];
38205
+ if (seen === null) seen = /* @__PURE__ */ new Set();
38206
+ if (seen.has(w)) continue;
38207
+ seen.add(w);
38208
+ const arr = nameLines.get(w);
38209
+ if (arr) arr.push(i2 + 1);
38210
+ else nameLines.set(w, [i2 + 1]);
38211
+ }
38212
+ }
38116
38213
  for (const def of graph.ir.dfg.defs) {
38117
38214
  if (def.kind !== "local") continue;
38118
38215
  const variable = def.variable;
@@ -38123,22 +38220,19 @@ var UnusedVariablePass = class {
38123
38220
  if (/\bexport\b/.test(lineText)) continue;
38124
38221
  const uses = graph.usesOfDef(def.id);
38125
38222
  if (uses.length > 0) continue;
38126
- const otherDefs = graph.ir.dfg.defs.filter(
38127
- (d) => d.variable === variable && d.id !== def.id
38128
- );
38223
+ const otherDefs = (defsByVar.get(variable) ?? []).filter((d) => d.id !== def.id);
38129
38224
  const otherDefLines = new Set(otherDefs.map((d) => d.line));
38130
- const escapedName = variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38131
- const namePattern = new RegExp(`\\b${escapedName}\\b`);
38225
+ const plainName = /^[A-Za-z_][A-Za-z0-9_]*$/.test(variable);
38226
+ const idxLines = plainName ? nameLines.get(variable) ?? EMPTY_LINES : null;
38227
+ const namePattern = plainName ? null : new RegExp(`\\b${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
38132
38228
  if (otherDefLines.size === 0) {
38133
- const usedElsewhere = codeLines.some(
38134
- (line, idx) => idx !== def.line - 1 && namePattern.test(line)
38135
- );
38229
+ const usedElsewhere = idxLines !== null ? idxLines.some((ln) => ln !== def.line) : codeLines.some((line, idx) => idx !== def.line - 1 && namePattern.test(line));
38136
38230
  if (usedElsewhere) continue;
38137
38231
  } else {
38138
38232
  const lineText2 = codeLines[def.line - 1] ?? "";
38139
38233
  const isTrueDeclaration = /\b(?:let|const|var)\s+[\w{[]/.test(lineText2) || /\b(?:int|long|float|double|boolean|byte|char|short|var|final)\b/.test(lineText2) || /\b[A-Z]\w*(?:<[^>]*>)?\s+\w/.test(lineText2) || /\blet\s+(?:mut\s+)?\w/.test(lineText2);
38140
38234
  if (isTrueDeclaration) {
38141
- const usedBeforeNextDef = codeLines.some((line, idx) => {
38235
+ const usedBeforeNextDef = idxLines !== null ? idxLines.some((ln) => ln !== def.line && !otherDefLines.has(ln) && !otherDefs.some((d) => d.line > def.line && d.line < ln)) : codeLines.some((line, idx) => {
38142
38236
  const lineNum = idx + 1;
38143
38237
  if (lineNum === def.line || otherDefLines.has(lineNum)) return false;
38144
38238
  if (!namePattern.test(line)) return false;
@@ -38146,7 +38240,7 @@ var UnusedVariablePass = class {
38146
38240
  });
38147
38241
  if (usedBeforeNextDef) continue;
38148
38242
  } else {
38149
- const usedOnNonDefLine = codeLines.some((line, idx) => {
38243
+ const usedOnNonDefLine = idxLines !== null ? idxLines.some((ln) => ln !== def.line && !otherDefLines.has(ln)) : codeLines.some((line, idx) => {
38150
38244
  const lineNum = idx + 1;
38151
38245
  return lineNum !== def.line && !otherDefLines.has(lineNum) && namePattern.test(line);
38152
38246
  });
@@ -41538,6 +41632,28 @@ var WEAK_HASH_NAMES = /* @__PURE__ */ new Set([
41538
41632
  "sha-1",
41539
41633
  "sha1"
41540
41634
  ]);
41635
+ var CSHARP_WEAK_HASH_CLASSES = /* @__PURE__ */ new Set(["MD5", "SHA1"]);
41636
+ var CSHARP_HASH_FACTORY_RECEIVERS = /* @__PURE__ */ new Set([
41637
+ "HashAlgorithm",
41638
+ "KeyedHashAlgorithm",
41639
+ "HMAC",
41640
+ "CryptoConfig"
41641
+ ]);
41642
+ var CSHARP_WEAK_HASH_CTORS = {
41643
+ MD5CryptoServiceProvider: "md5",
41644
+ MD5Cng: "md5",
41645
+ SHA1CryptoServiceProvider: "sha1",
41646
+ SHA1Managed: "sha1",
41647
+ SHA1Cng: "sha1",
41648
+ HMACMD5: "md5",
41649
+ HMACSHA1: "sha1"
41650
+ };
41651
+ function csharpWeakHashAlgo(cleaned) {
41652
+ if (!cleaned) return null;
41653
+ const base = (cleaned.split(".").pop() ?? cleaned).replace(/^hmac/, "");
41654
+ if (base === "sha") return "sha1";
41655
+ return WEAK_HASH_NAMES.has(base) ? base : null;
41656
+ }
41541
41657
  var COMMONS_DIGEST_METHODS = /* @__PURE__ */ new Set([
41542
41658
  "md2",
41543
41659
  "md2Hex",
@@ -41697,6 +41813,19 @@ var WeakHashPass = class {
41697
41813
  }
41698
41814
  return null;
41699
41815
  }
41816
+ if (language === "csharp") {
41817
+ if ((method === "Create" || method === "HashData") && CSHARP_WEAK_HASH_CLASSES.has(receiver)) {
41818
+ return { algorithm: receiver.toLowerCase(), api: `${receiver}.${method}` };
41819
+ }
41820
+ if ((method === "Create" || method === "CreateFromName") && CSHARP_HASH_FACTORY_RECEIVERS.has(receiver)) {
41821
+ const algo = csharpWeakHashAlgo(literalAlgo(call, 0));
41822
+ if (algo) return { algorithm: algo, api: `${receiver}.${method}` };
41823
+ }
41824
+ if (call.is_constructor && CSHARP_WEAK_HASH_CTORS[method]) {
41825
+ return { algorithm: CSHARP_WEAK_HASH_CTORS[method], api: `new ${method}()` };
41826
+ }
41827
+ return null;
41828
+ }
41700
41829
  return null;
41701
41830
  }
41702
41831
  };
@@ -41758,6 +41887,27 @@ var WEAK_CIPHER_BASES = /* @__PURE__ */ new Set([
41758
41887
  "seed",
41759
41888
  "cast5"
41760
41889
  ]);
41890
+ var CSHARP_WEAK_CIPHER_CLASSES = /* @__PURE__ */ new Set(["DES", "TripleDES", "RC2", "RC4"]);
41891
+ var CSHARP_CIPHER_FACTORY_RECEIVERS = /* @__PURE__ */ new Set(["SymmetricAlgorithm", "CryptoConfig"]);
41892
+ var CSHARP_WEAK_CIPHER_CTORS = {
41893
+ DESCryptoServiceProvider: "des",
41894
+ TripleDESCryptoServiceProvider: "3des",
41895
+ RC2CryptoServiceProvider: "rc2"
41896
+ };
41897
+ var CSHARP_RSA_FACTORY_CLASSES = /* @__PURE__ */ new Set(["RSA", "DSA"]);
41898
+ var CSHARP_RSA_CTORS = /* @__PURE__ */ new Set([
41899
+ "RSACryptoServiceProvider",
41900
+ "DSACryptoServiceProvider",
41901
+ "RSACng",
41902
+ "DSACng"
41903
+ ]);
41904
+ function csharpWeakCipher(cleaned) {
41905
+ if (!cleaned) return null;
41906
+ const base = (cleaned.split(".").pop() ?? cleaned).toLowerCase();
41907
+ if (base === "tripledes" || base === "desede") return "3des";
41908
+ if (base === "arc4") return "rc4";
41909
+ return WEAK_CIPHER_BASES.has(base) ? base : null;
41910
+ }
41761
41911
  function classifyJavaCipherSpec(spec) {
41762
41912
  const parts2 = spec.split("/").map((p) => p.trim().toLowerCase());
41763
41913
  const base = parts2[0] ?? "";
@@ -42149,6 +42299,34 @@ var WeakCryptoPass = class {
42149
42299
  }
42150
42300
  return out2;
42151
42301
  }
42302
+ if (language === "csharp") {
42303
+ if (method === "Create" && CSHARP_WEAK_CIPHER_CLASSES.has(receiver)) {
42304
+ const base = receiver === "TripleDES" ? "3des" : receiver.toLowerCase();
42305
+ out2.push({ issue: "weak-cipher", detail: base, api: `${receiver}.Create` });
42306
+ }
42307
+ if ((method === "Create" || method === "CreateFromName") && CSHARP_CIPHER_FACTORY_RECEIVERS.has(receiver)) {
42308
+ const base = csharpWeakCipher(literalAlgo2(call, 0));
42309
+ if (base) out2.push({ issue: "weak-cipher", detail: base, api: `${receiver}.${method}` });
42310
+ }
42311
+ if (call.is_constructor && CSHARP_WEAK_CIPHER_CTORS[method]) {
42312
+ out2.push({ issue: "weak-cipher", detail: CSHARP_WEAK_CIPHER_CTORS[method], api: `new ${method}()` });
42313
+ }
42314
+ const rsaFactory = method === "Create" && CSHARP_RSA_FACTORY_CLASSES.has(receiver);
42315
+ const rsaCtor = call.is_constructor && CSHARP_RSA_CTORS.has(method);
42316
+ if (rsaFactory || rsaCtor) {
42317
+ const sizeArg = call.arguments.find((a) => a.position === 0);
42318
+ const expr = (sizeArg?.literal ?? sizeArg?.expression ?? "").trim();
42319
+ const n = parseInt(expr, 10);
42320
+ if (Number.isFinite(n) && n > 0 && n < 2048) {
42321
+ out2.push({
42322
+ issue: "weak-rsa-key",
42323
+ detail: String(n),
42324
+ api: rsaCtor ? `new ${method}()` : `${receiver}.Create`
42325
+ });
42326
+ }
42327
+ }
42328
+ return out2;
42329
+ }
42152
42330
  return out2;
42153
42331
  }
42154
42332
  };
@@ -42832,8 +43010,7 @@ var InfoDisclosureStacktracePass = class {
42832
43010
  // src/analysis/passes/unrestricted-file-upload-pass.ts
42833
43011
  var UPLOAD_NAME_RE = /(?:getOriginalFilename|getSubmittedFileName|originalname|originalName|\.filename|\.Filename|FileHeader\.Filename|UploadFile)/;
42834
43012
  var FILE_SAFE_CALL_RE = /(?:secure_filename|FilenameUtils\.getExtension|\.lastIndexOf\(['"]\.['"]\)|ALLOWED_EXT|ALLOWED_EXTENSIONS|allowedExtensions|\bfileFilter\b|filepath\.Ext|path\.extname)/;
42835
- function lineWindow(code, startLine, endLine) {
42836
- const lines = code.split("\n");
43013
+ function lineWindow(lines, startLine, endLine) {
42837
43014
  const s = Math.max(0, startLine - 1);
42838
43015
  const e = Math.min(lines.length, endLine);
42839
43016
  return lines.slice(s, e).join("\n");
@@ -42853,10 +43030,11 @@ var UnrestrictedFileUploadPass = class {
42853
43030
  const { graph, language, code } = ctx;
42854
43031
  const file = graph.ir.meta.file;
42855
43032
  const findings = [];
43033
+ const codeLines = code.split("\n");
42856
43034
  const safeFunctionRanges = [];
42857
43035
  for (const t of graph.ir.types) {
42858
43036
  for (const m of t.methods) {
42859
- const body2 = lineWindow(code, m.start_line, m.end_line);
43037
+ const body2 = lineWindow(codeLines, m.start_line, m.end_line);
42860
43038
  if (FILE_SAFE_CALL_RE.test(body2)) {
42861
43039
  safeFunctionRanges.push({ start: m.start_line, end: m.end_line });
42862
43040
  }
@@ -42866,7 +43044,7 @@ var UnrestrictedFileUploadPass = class {
42866
43044
  for (const r of safeFunctionRanges) {
42867
43045
  if (line >= r.start && line <= r.end) return true;
42868
43046
  }
42869
- const win = lineWindow(code, Math.max(1, line - 20), line + 5);
43047
+ const win = lineWindow(codeLines, Math.max(1, line - 20), line + 5);
42870
43048
  return FILE_SAFE_CALL_RE.test(win);
42871
43049
  };
42872
43050
  if (language === "java") {
@@ -9964,15 +9964,15 @@ function computeChains(defs, uses) {
9964
9964
  existing.push(use);
9965
9965
  usesByLine.set(use.line, existing);
9966
9966
  }
9967
+ const seenChains = /* @__PURE__ */ new Set();
9967
9968
  for (const def of defs) {
9968
9969
  if (def.kind === "local") {
9969
9970
  const usesOnLine = usesByLine.get(def.line) ?? [];
9970
9971
  for (const use of usesOnLine) {
9971
9972
  if (use.def_id !== null && use.def_id !== def.id) {
9972
- const exists = chains.some(
9973
- (c) => c.from_def === use.def_id && c.to_def === def.id && c.via === use.variable
9974
- );
9975
- if (!exists) {
9973
+ const key = `${use.def_id}\0${def.id}\0${use.variable}`;
9974
+ if (!seenChains.has(key)) {
9975
+ seenChains.add(key);
9976
9976
  chains.push({
9977
9977
  from_def: use.def_id,
9978
9978
  to_def: def.id,
@@ -11896,8 +11896,11 @@ var DEFAULT_SINKS = [
11896
11896
  // SAX handler sinks (can lead to XSS in parsed content)
11897
11897
  { method: "startElement", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2] },
11898
11898
  { method: "characters", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11899
- // Template output sinks
11900
- { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11899
+ // Template output sinks. `output` is classless (Java template engines /
11900
+ // Velocity), but Rust `std::process::Command::…output()` is process
11901
+ // execution, not HTML — it already fires the CWE-78 command_injection sink,
11902
+ // so exclude Rust here to drop the spurious xss (cognium-dev#146).
11903
+ { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], exclude_languages: ["rust"] },
11901
11904
  { method: "setOutput", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11902
11905
  { method: "writeAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
11903
11906
  // AntiSamy specific (SAX filters)
@@ -13188,7 +13191,10 @@ var DEFAULT_SINKS = [
13188
13191
  { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13189
13192
  { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13190
13193
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13191
- { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13194
+ // Both the single-string overload `Process.Start("sh -c " + x)` and the
13195
+ // `(fileName, arguments)` overload `Process.Start("/bin/sh", "-c " + x)` are
13196
+ // injectable — the second is the argv path where taint rides arg[1] (#276).
13197
+ { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13192
13198
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13193
13199
  // C# path traversal — System.IO file APIs (CWE-22). Distinctive method names.
13194
13200
  { method: "ReadAllText", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13229,6 +13235,22 @@ var DEFAULT_SINKS = [
13229
13235
  // filter is the constructor argument.
13230
13236
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13231
13237
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13238
+ // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13239
+ // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13240
+ // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
13241
+ // `TypedResults.Redirect(url)` also route through the same method name. Taint-
13242
+ // gated, so a constant URL never fires. `LocalRedirect` is deliberately NOT
13243
+ // registered — it rejects non-local URLs and is the documented mitigation.
13244
+ { method: "Redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13245
+ { method: "RedirectPermanent", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13246
+ // C# CRLF / HTTP response-header injection — classic ASP.NET header writers
13247
+ // (CWE-113, cognium-dev#273/#275). The injectable value is the second arg.
13248
+ { method: "AddHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13249
+ { method: "AppendHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13250
+ // C# NoSQL injection — MongoDB filter built from raw JSON (CWE-943,
13251
+ // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13252
+ // attacker-controlled query document. Class-scoped (static receiver resolves).
13253
+ { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13232
13254
  // C# XPath injection — System.Xml.XPath (CWE-643). Distinctive selector methods.
13233
13255
  { method: "SelectSingleNode", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13234
13256
  { method: "SelectNodes", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -9898,15 +9898,15 @@ function computeChains(defs, uses) {
9898
9898
  existing.push(use);
9899
9899
  usesByLine.set(use.line, existing);
9900
9900
  }
9901
+ const seenChains = /* @__PURE__ */ new Set();
9901
9902
  for (const def of defs) {
9902
9903
  if (def.kind === "local") {
9903
9904
  const usesOnLine = usesByLine.get(def.line) ?? [];
9904
9905
  for (const use of usesOnLine) {
9905
9906
  if (use.def_id !== null && use.def_id !== def.id) {
9906
- const exists = chains.some(
9907
- (c) => c.from_def === use.def_id && c.to_def === def.id && c.via === use.variable
9908
- );
9909
- if (!exists) {
9907
+ const key = `${use.def_id}\0${def.id}\0${use.variable}`;
9908
+ if (!seenChains.has(key)) {
9909
+ seenChains.add(key);
9910
9910
  chains.push({
9911
9911
  from_def: use.def_id,
9912
9912
  to_def: def.id,
@@ -11830,8 +11830,11 @@ var DEFAULT_SINKS = [
11830
11830
  // SAX handler sinks (can lead to XSS in parsed content)
11831
11831
  { method: "startElement", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2] },
11832
11832
  { method: "characters", class: "ContentHandler", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11833
- // Template output sinks
11834
- { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11833
+ // Template output sinks. `output` is classless (Java template engines /
11834
+ // Velocity), but Rust `std::process::Command::…output()` is process
11835
+ // execution, not HTML — it already fires the CWE-78 command_injection sink,
11836
+ // so exclude Rust here to drop the spurious xss (cognium-dev#146).
11837
+ { method: "output", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], exclude_languages: ["rust"] },
11835
11838
  { method: "setOutput", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11836
11839
  { method: "writeAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
11837
11840
  // AntiSamy specific (SAX filters)
@@ -13122,7 +13125,10 @@ var DEFAULT_SINKS = [
13122
13125
  { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13123
13126
  { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13124
13127
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13125
- { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13128
+ // Both the single-string overload `Process.Start("sh -c " + x)` and the
13129
+ // `(fileName, arguments)` overload `Process.Start("/bin/sh", "-c " + x)` are
13130
+ // injectable — the second is the argv path where taint rides arg[1] (#276).
13131
+ { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13126
13132
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13127
13133
  // C# path traversal — System.IO file APIs (CWE-22). Distinctive method names.
13128
13134
  { method: "ReadAllText", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13163,6 +13169,22 @@ var DEFAULT_SINKS = [
13163
13169
  // filter is the constructor argument.
13164
13170
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13165
13171
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13172
+ // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13173
+ // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13174
+ // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
13175
+ // `TypedResults.Redirect(url)` also route through the same method name. Taint-
13176
+ // gated, so a constant URL never fires. `LocalRedirect` is deliberately NOT
13177
+ // registered — it rejects non-local URLs and is the documented mitigation.
13178
+ { method: "Redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13179
+ { method: "RedirectPermanent", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13180
+ // C# CRLF / HTTP response-header injection — classic ASP.NET header writers
13181
+ // (CWE-113, cognium-dev#273/#275). The injectable value is the second arg.
13182
+ { method: "AddHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13183
+ { method: "AppendHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13184
+ // C# NoSQL injection — MongoDB filter built from raw JSON (CWE-943,
13185
+ // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13186
+ // attacker-controlled query document. Class-scoped (static receiver resolves).
13187
+ { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13166
13188
  // C# XPath injection — System.Xml.XPath (CWE-643). Distinctive selector methods.
13167
13189
  { method: "SelectSingleNode", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13168
13190
  { method: "SelectNodes", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -1014,15 +1014,18 @@ function computeChains(defs, uses) {
1014
1014
  }
1015
1015
  // For each definition, find uses on the same line that might be in its initializer
1016
1016
  // This is a heuristic - more precise would require tracking def-use relationships in AST
1017
+ // Dedup via a Set keyed on (from_def, to_def, via) — an `Array.some` scan per
1018
+ // insertion was O(chains²) on large files (cognium-ai#305).
1019
+ const seenChains = new Set();
1017
1020
  for (const def of defs) {
1018
1021
  if (def.kind === 'local') {
1019
1022
  const usesOnLine = usesByLine.get(def.line) ?? [];
1020
1023
  for (const use of usesOnLine) {
1021
1024
  // If this use has a reaching def, create a chain
1022
1025
  if (use.def_id !== null && use.def_id !== def.id) {
1023
- // Avoid duplicate chains
1024
- const exists = chains.some(c => c.from_def === use.def_id && c.to_def === def.id && c.via === use.variable);
1025
- if (!exists) {
1026
+ const key = `${use.def_id}${def.id}${use.variable}`;
1027
+ if (!seenChains.has(key)) {
1028
+ seenChains.add(key);
1026
1029
  chains.push({
1027
1030
  from_def: use.def_id,
1028
1031
  to_def: def.id,