circle-ir 4.9.8 → 4.9.10

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.
@@ -6003,6 +6003,26 @@ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6003
6003
  "ExecuteReaderAsync",
6004
6004
  "ExecuteNonQueryAsync"
6005
6005
  ]);
6006
+ var CSHARP_RECEIVER_URL_METHODS = /* @__PURE__ */ new Set([
6007
+ "GetAsync",
6008
+ "PostAsync",
6009
+ "PutAsync",
6010
+ "PatchAsync",
6011
+ "DeleteAsync",
6012
+ "HeadAsync",
6013
+ "GetStringAsync",
6014
+ "GetByteArrayAsync",
6015
+ "GetStreamAsync",
6016
+ "GetJsonAsync"
6017
+ ]);
6018
+ var CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
6019
+ function csharpBareName(node) {
6020
+ if (node.type === "generic_name") {
6021
+ const id = node.childForFieldName("name") ?? node.namedChild(0);
6022
+ if (id) return getNodeText(id);
6023
+ }
6024
+ return getNodeText(node);
6025
+ }
6006
6026
  function extractCSharpCalls(tree, cache) {
6007
6027
  const calls = [];
6008
6028
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -6014,16 +6034,22 @@ function extractCSharpCalls(tree, cache) {
6014
6034
  if (fn?.type === "member_access_expression") {
6015
6035
  const nameNode = fn.childForFieldName("name");
6016
6036
  const exprNode = fn.childForFieldName("expression");
6017
- methodName = nameNode ? getNodeText(nameNode) : "unknown";
6037
+ methodName = nameNode ? csharpBareName(nameNode) : "unknown";
6018
6038
  receiver = exprNode ? getNodeText(exprNode) : null;
6019
6039
  } else if (fn) {
6020
- methodName = getNodeText(fn);
6040
+ methodName = csharpBareName(fn);
6021
6041
  }
6022
6042
  const argsNode = inv.childForFieldName("arguments");
6023
6043
  let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
6024
6044
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
6025
6045
  args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
6026
6046
  }
6047
+ if (methodName === "Add" && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
6048
+ methodName = "AddHeader";
6049
+ }
6050
+ if (receiver && args2.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
6051
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
6052
+ }
6027
6053
  calls.push({
6028
6054
  method_name: methodName,
6029
6055
  receiver,
@@ -6039,7 +6065,7 @@ function extractCSharpCalls(tree, cache) {
6039
6065
  const typeNode = creation.childForFieldName("type");
6040
6066
  const argsNode = creation.childForFieldName("arguments");
6041
6067
  calls.push({
6042
- method_name: typeNode ? getNodeText(typeNode) : "unknown",
6068
+ method_name: typeNode ? csharpBareName(typeNode) : "unknown",
6043
6069
  receiver: null,
6044
6070
  receiver_type: null,
6045
6071
  receiver_type_fqn: null,
@@ -6052,6 +6078,39 @@ function extractCSharpCalls(tree, cache) {
6052
6078
  const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
6053
6079
  for (const asn of assignments) {
6054
6080
  const left = asn.childForFieldName("left");
6081
+ if (left?.type === "element_access_expression") {
6082
+ const target = left.childForFieldName("expression");
6083
+ const right2 = asn.childForFieldName("right");
6084
+ if (!target || !right2) continue;
6085
+ if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target))) continue;
6086
+ const subscript = left.childForFieldName("subscript") ?? left.namedChild(1);
6087
+ const rhsText2 = getNodeText(right2);
6088
+ calls.push({
6089
+ method_name: "AddHeader",
6090
+ receiver: getNodeText(target),
6091
+ receiver_type: null,
6092
+ receiver_type_fqn: null,
6093
+ arguments: [
6094
+ {
6095
+ position: 0,
6096
+ expression: subscript ? getNodeText(subscript) : "",
6097
+ variable: null,
6098
+ literal: subscript ? getNodeText(subscript) : null,
6099
+ value: null
6100
+ },
6101
+ {
6102
+ position: 1,
6103
+ expression: rhsText2,
6104
+ variable: right2.type === "identifier" ? rhsText2 : null,
6105
+ literal: right2.type === "string_literal" ? rhsText2 : null,
6106
+ value: null
6107
+ }
6108
+ ],
6109
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6110
+ in_method: findEnclosingMethod(asn)
6111
+ });
6112
+ continue;
6113
+ }
6055
6114
  if (left?.type !== "member_access_expression") continue;
6056
6115
  const nameNode = left.childForFieldName("name");
6057
6116
  if (!nameNode) continue;
@@ -13922,6 +13981,23 @@ var DEFAULT_SINKS = [
13922
13981
  { method: "ExecuteSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13923
13982
  { method: "FromSqlRaw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13924
13983
  { method: "FromSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13984
+ // Dapper micro-ORM (cognium-dev#275). Every Dapper entry point is an
13985
+ // `IDbConnection` extension whose arg 0 is raw SQL text — `conn.Query<T>(sql)`
13986
+ // is exactly as injectable as `new SqlCommand(sql)`. Taint-gated on arg 0, so
13987
+ // a constant/interpolation-free query never fires, and a same-named method on
13988
+ // an unrelated receiver (e.g. RestSharp's `client.Execute(request)`, whose
13989
+ // arg 0 is a request object rather than a tainted string) stays clean.
13990
+ { method: "Query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13991
+ { method: "QueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13992
+ { method: "QueryFirst", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13993
+ { method: "QueryFirstAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13994
+ { method: "QueryFirstOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13995
+ { method: "QueryFirstOrDefaultAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13996
+ { method: "QuerySingle", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13997
+ { method: "QuerySingleOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13998
+ { method: "QueryMultiple", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13999
+ { method: "Execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14000
+ { method: "ExecuteAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13925
14001
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13926
14002
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13927
14003
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13987,12 +14063,23 @@ var DEFAULT_SINKS = [
13987
14063
  { method: "DeleteAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13988
14064
  { method: "GetStringAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13989
14065
  { method: "GetByteArrayAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14066
+ // Remaining HttpClient/Flurl verbs (cognium-dev#275). For HttpClient these
14067
+ // take the URL at arg 0; for Flurl's fluent form the receiver is surfaced as
14068
+ // arg 0 by CSHARP_RECEIVER_URL_METHODS. Taint-gated either way.
14069
+ // (GetStreamAsync / PutAsync / DeleteAsync are already registered below.)
14070
+ { method: "GetJsonAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14071
+ { method: "PatchAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14072
+ { method: "HeadAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13990
14073
  { method: "GetStreamAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13991
14074
  { method: "SendAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13992
14075
  { method: "DownloadString", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13993
14076
  { method: "DownloadData", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13994
14077
  { method: "DownloadFile", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [1], languages: ["csharp"] },
13995
14078
  { method: "Create", class: "WebRequest", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14079
+ // RestSharp (cognium-dev#275) — the base URL is fixed at construction, so
14080
+ // `new RestClient(userUrl)` points every later request at attacker-controlled
14081
+ // infrastructure. Ctor arg 0, taint-gated.
14082
+ { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13996
14083
  // C# code injection — dynamic script/assembly loading (CWE-94).
13997
14084
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13998
14085
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -14032,6 +14119,11 @@ var DEFAULT_SINKS = [
14032
14119
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
14033
14120
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
14034
14121
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
14122
+ // `return Content(html, "text/html")` on a controller writes arg 0 to the
14123
+ // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
14124
+ // `Content(constantString)` and the JSON/plain-text content types are
14125
+ // unaffected because a constant argument carries no taint.
14126
+ { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
14035
14127
  // `String.Format(fmt, …)` with an attacker-controlled FORMAT string — CWE-134.
14036
14128
  // Taint-gated on arg 0 (the format), so ordinary `String.Format("...", user)` where
14037
14129
  // only an argument is tainted never fires. .NET composite formatting can't corrupt
@@ -14057,6 +14149,10 @@ var DEFAULT_SINKS = [
14057
14149
  // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
14058
14150
  // attacker-controlled query document. Class-scoped (static receiver resolves).
14059
14151
  { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
14152
+ // `new BsonJavaScript(userCode)` wraps a server-side JS string — the payload
14153
+ // MongoDB's `$where` / `$function` operators execute. Unconditionally
14154
+ // dangerous when tainted, so no literal gating is needed (cognium-dev#275).
14155
+ { method: "BsonJavaScript", class: "constructor", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
14060
14156
  // C# log injection — Microsoft.Extensions.Logging `ILogger` (CWE-117,
14061
14157
  // cognium-ai#318). Only the message TEMPLATE (arg[0]) is the injectable
14062
14158
  // position; the structured form `LogInformation("… {Id}", id)` keeps taint in
@@ -14068,6 +14164,16 @@ var DEFAULT_SINKS = [
14068
14164
  { method: "LogDebug", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
14069
14165
  { method: "LogCritical", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
14070
14166
  { method: "LogTrace", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
14167
+ // C# ReDoS (CWE-1333, cognium-dev#275). The injectable position is the
14168
+ // PATTERN, not the input: a user-supplied regex can be crafted with
14169
+ // catastrophic backtracking. `Regex.IsMatch(input, pattern)` and friends take
14170
+ // the pattern at arg 1; the `new Regex(pattern)` ctor takes it at arg 0. A
14171
+ // tainted *input* with a constant pattern is not ReDoS and does not fire.
14172
+ { method: "IsMatch", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
14173
+ { method: "Match", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
14174
+ { method: "Matches", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
14175
+ { method: "Replace", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
14176
+ { method: "Regex", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["csharp"] },
14071
14177
  // Generic `ILogger.Log(logLevel, message, …)` — class-scoped (`Log` alone is
14072
14178
  // too generic); the message is arg[1].
14073
14179
  { method: "Log", class: "ILogger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1], languages: ["csharp"] },
@@ -14326,6 +14432,15 @@ var DEFAULT_SANITIZERS = [
14326
14432
  // this table; the drift surfaced as a SecuriBench Micro FP on
14327
14433
  // `sanitizers/Sanitizers3.java` (`URLEncoder.encode` + `sendRedirect`).
14328
14434
  { method: "encodeForURL", removes: ["xss", "ssrf", "open_redirect"] },
14435
+ // ASP.NET Core local-redirect guards (cognium-dev#275). `Url.IsLocalUrl(x)`
14436
+ // returns false for any absolute/scheme-relative URL, so a `Redirect(x)` gated
14437
+ // on it cannot leave the site — without this credit the guarded form is a
14438
+ // false positive. `LocalRedirect(x)` is the same check applied by the
14439
+ // framework itself: it THROWS on a non-local URL, which is why it is
14440
+ // deliberately not registered as an open_redirect sink. Recording it here
14441
+ // states that intent in the model rather than leaving it as a silent omission.
14442
+ { method: "IsLocalUrl", removes: ["open_redirect"] },
14443
+ { method: "LocalRedirect", removes: ["open_redirect"] },
14329
14444
  // URL encoding wrapper aliases (common patterns in benchmarks and real-world code)
14330
14445
  { method: "encodeURL", removes: ["xss", "ssrf", "open_redirect"] },
14331
14446
  { method: "urlEncode", removes: ["xss", "ssrf", "open_redirect"] },
@@ -15562,15 +15677,25 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
15562
15677
  function stripCsLiterals(line) {
15563
15678
  return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
15564
15679
  }
15680
+ var _csDepthLines;
15681
+ var _csDepthPrefix;
15565
15682
  function csBraceDepthBefore(lines, idx) {
15566
- let depth = 0;
15567
- for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
15568
- for (const ch of stripCsLiterals(lines[i2])) {
15569
- if (ch === "{") depth++;
15570
- else if (ch === "}") depth--;
15683
+ if (lines !== _csDepthLines) {
15684
+ const pref = new Int32Array(lines.length + 1);
15685
+ let depth = 0;
15686
+ for (let i2 = 0; i2 < lines.length; i2++) {
15687
+ pref[i2] = depth;
15688
+ for (const ch of stripCsLiterals(lines[i2])) {
15689
+ if (ch === "{") depth++;
15690
+ else if (ch === "}") depth--;
15691
+ }
15571
15692
  }
15693
+ pref[lines.length] = depth;
15694
+ _csDepthLines = lines;
15695
+ _csDepthPrefix = pref;
15572
15696
  }
15573
- return depth;
15697
+ const clamped = idx < 0 ? 0 : idx > lines.length ? lines.length : idx;
15698
+ return _csDepthPrefix[clamped];
15574
15699
  }
15575
15700
  function netBraces(fragment) {
15576
15701
  let n = 0;
@@ -15667,6 +15792,30 @@ function csThenBlock(lines, ifIdx, rest) {
15667
15792
  }
15668
15793
  return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
15669
15794
  }
15795
+ var _csGuardLines;
15796
+ var _csGuardMap;
15797
+ function csEqualityGuardIndex(lines) {
15798
+ if (lines === _csGuardLines && _csGuardMap) return _csGuardMap;
15799
+ const map = /* @__PURE__ */ new Map();
15800
+ for (let i2 = 0; i2 < lines.length; i2++) {
15801
+ const parts2 = splitCsIf(lines[i2]);
15802
+ if (!parts2) continue;
15803
+ const guard = csEqualityGuard(parts2.cond);
15804
+ if (!guard) continue;
15805
+ const entry = { i: i2, rest: parts2.rest, ifCol: parts2.ifCol, op: guard.op };
15806
+ for (const id of csIdentifiers(guard.exprSide)) {
15807
+ let arr = map.get(id);
15808
+ if (!arr) {
15809
+ arr = [];
15810
+ map.set(id, arr);
15811
+ }
15812
+ arr.push(entry);
15813
+ }
15814
+ }
15815
+ _csGuardLines = lines;
15816
+ _csGuardMap = map;
15817
+ return map;
15818
+ }
15670
15819
  function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
15671
15820
  if (language !== "csharp") return false;
15672
15821
  if (pattern.type !== "ssrf") return false;
@@ -15679,20 +15828,22 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
15679
15828
  if (candidates.size === 0) return false;
15680
15829
  const sinkIdx = call.location.line - 1;
15681
15830
  const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
15682
- for (let i2 = 0; i2 < sinkIdx; i2++) {
15683
- const parts2 = splitCsIf(sourceLines[i2]);
15684
- if (!parts2) continue;
15685
- const guard = csEqualityGuard(parts2.cond);
15686
- if (!guard) continue;
15687
- const guardIds = csIdentifiers(guard.exprSide);
15688
- if (![...guardIds].some((id) => candidates.has(id))) continue;
15689
- const block = csThenBlock(sourceLines, i2, parts2.rest);
15690
- if (guard.op === "==") {
15691
- if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
15692
- } else {
15693
- const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
15694
- if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
15695
- return true;
15831
+ const guardMap = csEqualityGuardIndex(sourceLines);
15832
+ const checked = /* @__PURE__ */ new Set();
15833
+ for (const cand of candidates) {
15834
+ const guards = guardMap.get(cand);
15835
+ if (!guards) continue;
15836
+ for (const g of guards) {
15837
+ if (g.i >= sinkIdx || checked.has(g)) continue;
15838
+ checked.add(g);
15839
+ const block = csThenBlock(sourceLines, g.i, g.rest);
15840
+ if (g.op === "==") {
15841
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
15842
+ } else {
15843
+ const guardDepth = csBraceDepthBefore(sourceLines, g.i) + netBraces(stripCsLiterals(sourceLines[g.i]).slice(0, g.ifCol));
15844
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
15845
+ return true;
15846
+ }
15696
15847
  }
15697
15848
  }
15698
15849
  }
@@ -17164,7 +17315,13 @@ function canSourceReachSink(sourceType, sinkType) {
17164
17315
  // scan path (`generateFindings`) dropped every `http_*/io/network/interproc →
17165
17316
  // prompt_injection` flow even though `taint.flows` / the trust pass reported
17166
17317
  // it — the same "two paths disagree" shape as #129. Now emitted from scan too.
17167
- http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17318
+ // xxe added (cognium-dev #282): `request.getParameter("xml")` reaching an
17319
+ // XML parser (DocumentBuilder.parse etc.) is XXE, but xxe was absent from
17320
+ // http_param/http_query (present on http_body/io_input/file_input), so the
17321
+ // pairing was gated out and generateFindings dropped it — same class as the
17322
+ // #129 log_injection/format_string/nosql_injection drop. Mirrors how
17323
+ // deserialization already sits on both http_param and http_query.
17324
+ http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization", "xxe", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17168
17325
  http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17169
17326
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17170
17327
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
@@ -17175,7 +17332,7 @@ function canSourceReachSink(sourceType, sinkType) {
17175
17332
  // annotated `/* BAD */`. Prior to 3.163.0 the reach map omitted xss
17176
17333
  // so http_path → xss inline-colocation flows were silently dropped.
17177
17334
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary", "xss", "log_injection", "format_string", "prompt_injection"],
17178
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17335
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization", "xxe", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17179
17336
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
17180
17337
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
17181
17338
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -24688,14 +24845,11 @@ var RustPlugin = class extends BaseLanguagePlugin {
24688
24845
  severity: "high",
24689
24846
  argPositions: [0]
24690
24847
  },
24691
- // Format String (Rust is mostly safe, but unchecked format! can be dangerous)
24692
- {
24693
- method: "format!",
24694
- type: "format_string",
24695
- cwe: "CWE-134",
24696
- severity: "medium",
24697
- argPositions: [0]
24698
- },
24848
+ // Format String `format!` is NOT a CWE-134 sink in Rust. Its template
24849
+ // is a compile-time string literal; a non-literal template does not
24850
+ // compile, so an attacker-controlled format string is not expressible.
24851
+ // Interpolating data into `format!` is the ordinary, safe way to build a
24852
+ // string. (cognium-dev #294 part 1 — removed as a format_string sink.)
24699
24853
  // Unsafe operations
24700
24854
  {
24701
24855
  method: "from_raw_parts",
@@ -27535,11 +27689,19 @@ function findCSharpRequestSources(sourceCode, language) {
27535
27689
  const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
27536
27690
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
27537
27691
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
27692
+ const formFileParams = /* @__PURE__ */ new Set();
27693
+ const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
27694
+ for (const line of lines) {
27695
+ const re = new RegExp(formFileDeclRe.source, "g");
27696
+ let d;
27697
+ while ((d = re.exec(line)) !== null) formFileParams.add(d[1]);
27698
+ }
27699
+ const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
27538
27700
  for (let i2 = 0; i2 < lines.length; i2++) {
27539
27701
  const m = assignRe.exec(lines[i2]);
27540
27702
  if (!m) continue;
27541
27703
  const [, varName, rhs] = m;
27542
- const type = requestReadRe.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
27704
+ const type = requestReadRe.test(rhs) ? "http_param" : formFileReadRe?.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
27543
27705
  if (!type) continue;
27544
27706
  const lineNumber = i2 + 1;
27545
27707
  if (sources.some((s) => s.line === lineNumber && s.variable === varName)) continue;
@@ -28259,6 +28421,13 @@ function buildJavaTaintedVars(sourceCode, seedVars) {
28259
28421
  "interface",
28260
28422
  "enum"
28261
28423
  ]);
28424
+ const complexTaintedRes = [];
28425
+ for (const v of knownTainted) {
28426
+ if (!/^[\p{L}\p{N}_]+$/u.test(v)) {
28427
+ const esc = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28428
+ complexTaintedRes.push(new RegExp(`(?<![\\p{L}\\p{N}_])${esc}(?![\\p{L}\\p{N}_])`, "u"));
28429
+ }
28430
+ }
28262
28431
  let changed = true;
28263
28432
  let guard = 0;
28264
28433
  while (changed && guard < lines.length + 2) {
@@ -28276,11 +28445,9 @@ function buildJavaTaintedVars(sourceCode, seedVars) {
28276
28445
  const rhs = m[2];
28277
28446
  if (JAVA_KEYWORDS.has(lhs)) continue;
28278
28447
  if (knownTainted.has(lhs)) continue;
28279
- const escaped = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28280
28448
  const rhsCode = rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
28281
- const ref = [...knownTainted].some(
28282
- (v) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(v)}(?![\\p{L}\\p{N}_])`, "u").test(rhsCode)
28283
- );
28449
+ const rhsIdents = rhsCode.match(/[\p{L}\p{N}_]+/gu);
28450
+ const ref = rhsIdents !== null && rhsIdents.some((t) => knownTainted.has(t)) || complexTaintedRes.length > 0 && complexTaintedRes.some((re) => re.test(rhsCode));
28284
28451
  if (ref) {
28285
28452
  derived.set(lhs, i2 + 1);
28286
28453
  knownTainted.add(lhs);
@@ -36703,10 +36870,18 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
36703
36870
  void types;
36704
36871
  return flows;
36705
36872
  }
36873
+ var _reassignSplitCode;
36874
+ var _reassignSplitLines;
36875
+ function splitLinesCached(code) {
36876
+ if (code === _reassignSplitCode && _reassignSplitLines) return _reassignSplitLines;
36877
+ _reassignSplitCode = code;
36878
+ _reassignSplitLines = code.split("\n");
36879
+ return _reassignSplitLines;
36880
+ }
36706
36881
  function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
36707
36882
  if (!variable || sinkLine - srcLine < 2) return false;
36708
36883
  if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
36709
- const lines = code.split("\n");
36884
+ const lines = splitLinesCached(code);
36710
36885
  const lo = Math.max(0, srcLine);
36711
36886
  const hi = Math.min(lines.length, sinkLine - 1);
36712
36887
  const strLit = `(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|\`[^\`\\\\]*(?:\\\\.[^\`\\\\]*)*\`)`;
@@ -37019,9 +37194,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37019
37194
  }
37020
37195
  }
37021
37196
  }
37197
+ const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
37022
37198
  const reCache = /* @__PURE__ */ new Map();
37023
37199
  for (const s of sourcesWithVar) {
37024
- if (reCache.has(s.variable)) continue;
37200
+ if (SIMPLE_IDENT.test(s.variable) || reCache.has(s.variable)) continue;
37025
37201
  const escaped = s.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37026
37202
  reCache.set(s.variable, new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u"));
37027
37203
  }
@@ -37031,6 +37207,23 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37031
37207
  existing.push(call);
37032
37208
  callsByLine.set(call.location.line, existing);
37033
37209
  }
37210
+ const sourcesByVar = /* @__PURE__ */ new Map();
37211
+ const complexSources = [];
37212
+ sourcesWithVar.forEach((source, ord) => {
37213
+ const complex = !SIMPLE_IDENT.test(source.variable);
37214
+ const entry = { source, ord, complex };
37215
+ if (!complex) {
37216
+ let list = sourcesByVar.get(source.variable);
37217
+ if (!list) {
37218
+ list = [];
37219
+ sourcesByVar.set(source.variable, list);
37220
+ }
37221
+ list.push(entry);
37222
+ } else {
37223
+ complexSources.push(entry);
37224
+ }
37225
+ });
37226
+ const flowKeys = /* @__PURE__ */ new Set();
37034
37227
  for (const sink of sinks) {
37035
37228
  if (unreachableLines.has(sink.line)) continue;
37036
37229
  const callsAtSink = callsByLine.get(sink.line) ?? [];
@@ -37041,17 +37234,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37041
37234
  }
37042
37235
  const expr = arg.expression;
37043
37236
  if (!expr) continue;
37044
- for (const source of sourcesWithVar) {
37237
+ const exprIdentMatches = expr.match(/[\p{L}\p{N}_]+/gu);
37238
+ let candidates = complexSources.length > 0 ? complexSources.slice() : null;
37239
+ if (exprIdentMatches) {
37240
+ for (const id of new Set(exprIdentMatches)) {
37241
+ const list = sourcesByVar.get(id);
37242
+ if (!list) continue;
37243
+ (candidates ??= []).push(...list);
37244
+ }
37245
+ }
37246
+ if (!candidates) continue;
37247
+ if (candidates.length > 1) candidates.sort((a, b) => a.ord - b.ord);
37248
+ for (const { source, complex } of candidates) {
37045
37249
  if (source.line >= sink.line) continue;
37046
37250
  if (source.in_method && call.in_method && source.in_method !== call.in_method) {
37047
37251
  continue;
37048
37252
  }
37049
- const re = reCache.get(source.variable);
37050
- if (!re || !re.test(expr)) continue;
37253
+ if (complex) {
37254
+ const re = reCache.get(source.variable);
37255
+ if (!re || !re.test(expr)) continue;
37256
+ }
37051
37257
  if (!sourceSemanticsAllowed(source, sink.type)) continue;
37052
- if (flows.some(
37053
- (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
37054
- )) continue;
37258
+ const dedupKey = `${source.line}:${sink.line}:${sink.type}`;
37259
+ if (flowKeys.has(dedupKey)) continue;
37055
37260
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
37056
37261
  break;
37057
37262
  }
@@ -37070,6 +37275,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37070
37275
  confidence: source.confidence * sink.confidence * 0.7,
37071
37276
  sanitized: false
37072
37277
  });
37278
+ flowKeys.add(dedupKey);
37073
37279
  break;
37074
37280
  }
37075
37281
  }