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.
@@ -6049,6 +6049,26 @@ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6049
6049
  "ExecuteReaderAsync",
6050
6050
  "ExecuteNonQueryAsync"
6051
6051
  ]);
6052
+ var CSHARP_RECEIVER_URL_METHODS = /* @__PURE__ */ new Set([
6053
+ "GetAsync",
6054
+ "PostAsync",
6055
+ "PutAsync",
6056
+ "PatchAsync",
6057
+ "DeleteAsync",
6058
+ "HeadAsync",
6059
+ "GetStringAsync",
6060
+ "GetByteArrayAsync",
6061
+ "GetStreamAsync",
6062
+ "GetJsonAsync"
6063
+ ]);
6064
+ var CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
6065
+ function csharpBareName(node) {
6066
+ if (node.type === "generic_name") {
6067
+ const id = node.childForFieldName("name") ?? node.namedChild(0);
6068
+ if (id) return getNodeText(id);
6069
+ }
6070
+ return getNodeText(node);
6071
+ }
6052
6072
  function extractCSharpCalls(tree, cache) {
6053
6073
  const calls = [];
6054
6074
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -6060,16 +6080,22 @@ function extractCSharpCalls(tree, cache) {
6060
6080
  if (fn?.type === "member_access_expression") {
6061
6081
  const nameNode = fn.childForFieldName("name");
6062
6082
  const exprNode = fn.childForFieldName("expression");
6063
- methodName = nameNode ? getNodeText(nameNode) : "unknown";
6083
+ methodName = nameNode ? csharpBareName(nameNode) : "unknown";
6064
6084
  receiver = exprNode ? getNodeText(exprNode) : null;
6065
6085
  } else if (fn) {
6066
- methodName = getNodeText(fn);
6086
+ methodName = csharpBareName(fn);
6067
6087
  }
6068
6088
  const argsNode = inv.childForFieldName("arguments");
6069
6089
  let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
6070
6090
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
6071
6091
  args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
6072
6092
  }
6093
+ if (methodName === "Add" && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
6094
+ methodName = "AddHeader";
6095
+ }
6096
+ if (receiver && args2.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
6097
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
6098
+ }
6073
6099
  calls.push({
6074
6100
  method_name: methodName,
6075
6101
  receiver,
@@ -6085,7 +6111,7 @@ function extractCSharpCalls(tree, cache) {
6085
6111
  const typeNode = creation.childForFieldName("type");
6086
6112
  const argsNode = creation.childForFieldName("arguments");
6087
6113
  calls.push({
6088
- method_name: typeNode ? getNodeText(typeNode) : "unknown",
6114
+ method_name: typeNode ? csharpBareName(typeNode) : "unknown",
6089
6115
  receiver: null,
6090
6116
  receiver_type: null,
6091
6117
  receiver_type_fqn: null,
@@ -6098,6 +6124,39 @@ function extractCSharpCalls(tree, cache) {
6098
6124
  const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
6099
6125
  for (const asn of assignments) {
6100
6126
  const left = asn.childForFieldName("left");
6127
+ if (left?.type === "element_access_expression") {
6128
+ const target = left.childForFieldName("expression");
6129
+ const right2 = asn.childForFieldName("right");
6130
+ if (!target || !right2) continue;
6131
+ if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target))) continue;
6132
+ const subscript = left.childForFieldName("subscript") ?? left.namedChild(1);
6133
+ const rhsText2 = getNodeText(right2);
6134
+ calls.push({
6135
+ method_name: "AddHeader",
6136
+ receiver: getNodeText(target),
6137
+ receiver_type: null,
6138
+ receiver_type_fqn: null,
6139
+ arguments: [
6140
+ {
6141
+ position: 0,
6142
+ expression: subscript ? getNodeText(subscript) : "",
6143
+ variable: null,
6144
+ literal: subscript ? getNodeText(subscript) : null,
6145
+ value: null
6146
+ },
6147
+ {
6148
+ position: 1,
6149
+ expression: rhsText2,
6150
+ variable: right2.type === "identifier" ? rhsText2 : null,
6151
+ literal: right2.type === "string_literal" ? rhsText2 : null,
6152
+ value: null
6153
+ }
6154
+ ],
6155
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6156
+ in_method: findEnclosingMethod(asn)
6157
+ });
6158
+ continue;
6159
+ }
6101
6160
  if (left?.type !== "member_access_expression") continue;
6102
6161
  const nameNode = left.childForFieldName("name");
6103
6162
  if (!nameNode) continue;
@@ -13317,6 +13376,23 @@ var DEFAULT_SINKS = [
13317
13376
  { method: "ExecuteSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13318
13377
  { method: "FromSqlRaw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13319
13378
  { method: "FromSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13379
+ // Dapper micro-ORM (cognium-dev#275). Every Dapper entry point is an
13380
+ // `IDbConnection` extension whose arg 0 is raw SQL text — `conn.Query<T>(sql)`
13381
+ // is exactly as injectable as `new SqlCommand(sql)`. Taint-gated on arg 0, so
13382
+ // a constant/interpolation-free query never fires, and a same-named method on
13383
+ // an unrelated receiver (e.g. RestSharp's `client.Execute(request)`, whose
13384
+ // arg 0 is a request object rather than a tainted string) stays clean.
13385
+ { method: "Query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13386
+ { method: "QueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13387
+ { method: "QueryFirst", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13388
+ { method: "QueryFirstAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13389
+ { method: "QueryFirstOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13390
+ { method: "QueryFirstOrDefaultAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13391
+ { method: "QuerySingle", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13392
+ { method: "QuerySingleOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13393
+ { method: "QueryMultiple", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13394
+ { method: "Execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13395
+ { method: "ExecuteAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13320
13396
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13321
13397
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13322
13398
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13382,12 +13458,23 @@ var DEFAULT_SINKS = [
13382
13458
  { method: "DeleteAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13383
13459
  { method: "GetStringAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13384
13460
  { method: "GetByteArrayAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13461
+ // Remaining HttpClient/Flurl verbs (cognium-dev#275). For HttpClient these
13462
+ // take the URL at arg 0; for Flurl's fluent form the receiver is surfaced as
13463
+ // arg 0 by CSHARP_RECEIVER_URL_METHODS. Taint-gated either way.
13464
+ // (GetStreamAsync / PutAsync / DeleteAsync are already registered below.)
13465
+ { method: "GetJsonAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13466
+ { method: "PatchAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13467
+ { method: "HeadAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13385
13468
  { method: "GetStreamAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13386
13469
  { method: "SendAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13387
13470
  { method: "DownloadString", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13388
13471
  { method: "DownloadData", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13389
13472
  { method: "DownloadFile", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [1], languages: ["csharp"] },
13390
13473
  { method: "Create", class: "WebRequest", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13474
+ // RestSharp (cognium-dev#275) — the base URL is fixed at construction, so
13475
+ // `new RestClient(userUrl)` points every later request at attacker-controlled
13476
+ // infrastructure. Ctor arg 0, taint-gated.
13477
+ { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13391
13478
  // C# code injection — dynamic script/assembly loading (CWE-94).
13392
13479
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13393
13480
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13427,6 +13514,11 @@ var DEFAULT_SINKS = [
13427
13514
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13428
13515
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13429
13516
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13517
+ // `return Content(html, "text/html")` on a controller writes arg 0 to the
13518
+ // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13519
+ // `Content(constantString)` and the JSON/plain-text content types are
13520
+ // unaffected because a constant argument carries no taint.
13521
+ { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13430
13522
  // `String.Format(fmt, …)` with an attacker-controlled FORMAT string — CWE-134.
13431
13523
  // Taint-gated on arg 0 (the format), so ordinary `String.Format("...", user)` where
13432
13524
  // only an argument is tainted never fires. .NET composite formatting can't corrupt
@@ -13452,6 +13544,10 @@ var DEFAULT_SINKS = [
13452
13544
  // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13453
13545
  // attacker-controlled query document. Class-scoped (static receiver resolves).
13454
13546
  { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13547
+ // `new BsonJavaScript(userCode)` wraps a server-side JS string — the payload
13548
+ // MongoDB's `$where` / `$function` operators execute. Unconditionally
13549
+ // dangerous when tainted, so no literal gating is needed (cognium-dev#275).
13550
+ { method: "BsonJavaScript", class: "constructor", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13455
13551
  // C# log injection — Microsoft.Extensions.Logging `ILogger` (CWE-117,
13456
13552
  // cognium-ai#318). Only the message TEMPLATE (arg[0]) is the injectable
13457
13553
  // position; the structured form `LogInformation("… {Id}", id)` keeps taint in
@@ -13463,6 +13559,16 @@ var DEFAULT_SINKS = [
13463
13559
  { method: "LogDebug", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13464
13560
  { method: "LogCritical", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13465
13561
  { method: "LogTrace", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13562
+ // C# ReDoS (CWE-1333, cognium-dev#275). The injectable position is the
13563
+ // PATTERN, not the input: a user-supplied regex can be crafted with
13564
+ // catastrophic backtracking. `Regex.IsMatch(input, pattern)` and friends take
13565
+ // the pattern at arg 1; the `new Regex(pattern)` ctor takes it at arg 0. A
13566
+ // tainted *input* with a constant pattern is not ReDoS and does not fire.
13567
+ { method: "IsMatch", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13568
+ { method: "Match", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13569
+ { method: "Matches", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13570
+ { method: "Replace", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13571
+ { method: "Regex", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13466
13572
  // Generic `ILogger.Log(logLevel, message, …)` — class-scoped (`Log` alone is
13467
13573
  // too generic); the message is arg[1].
13468
13574
  { method: "Log", class: "ILogger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1], languages: ["csharp"] },
@@ -13721,6 +13827,15 @@ var DEFAULT_SANITIZERS = [
13721
13827
  // this table; the drift surfaced as a SecuriBench Micro FP on
13722
13828
  // `sanitizers/Sanitizers3.java` (`URLEncoder.encode` + `sendRedirect`).
13723
13829
  { method: "encodeForURL", removes: ["xss", "ssrf", "open_redirect"] },
13830
+ // ASP.NET Core local-redirect guards (cognium-dev#275). `Url.IsLocalUrl(x)`
13831
+ // returns false for any absolute/scheme-relative URL, so a `Redirect(x)` gated
13832
+ // on it cannot leave the site — without this credit the guarded form is a
13833
+ // false positive. `LocalRedirect(x)` is the same check applied by the
13834
+ // framework itself: it THROWS on a non-local URL, which is why it is
13835
+ // deliberately not registered as an open_redirect sink. Recording it here
13836
+ // states that intent in the model rather than leaving it as a silent omission.
13837
+ { method: "IsLocalUrl", removes: ["open_redirect"] },
13838
+ { method: "LocalRedirect", removes: ["open_redirect"] },
13724
13839
  // URL encoding wrapper aliases (common patterns in benchmarks and real-world code)
13725
13840
  { method: "encodeURL", removes: ["xss", "ssrf", "open_redirect"] },
13726
13841
  { method: "urlEncode", removes: ["xss", "ssrf", "open_redirect"] },
@@ -14862,15 +14977,25 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
14862
14977
  function stripCsLiterals(line) {
14863
14978
  return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
14864
14979
  }
14980
+ var _csDepthLines;
14981
+ var _csDepthPrefix;
14865
14982
  function csBraceDepthBefore(lines, idx) {
14866
- let depth = 0;
14867
- for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
14868
- for (const ch of stripCsLiterals(lines[i2])) {
14869
- if (ch === "{") depth++;
14870
- else if (ch === "}") depth--;
14983
+ if (lines !== _csDepthLines) {
14984
+ const pref = new Int32Array(lines.length + 1);
14985
+ let depth = 0;
14986
+ for (let i2 = 0; i2 < lines.length; i2++) {
14987
+ pref[i2] = depth;
14988
+ for (const ch of stripCsLiterals(lines[i2])) {
14989
+ if (ch === "{") depth++;
14990
+ else if (ch === "}") depth--;
14991
+ }
14871
14992
  }
14993
+ pref[lines.length] = depth;
14994
+ _csDepthLines = lines;
14995
+ _csDepthPrefix = pref;
14872
14996
  }
14873
- return depth;
14997
+ const clamped = idx < 0 ? 0 : idx > lines.length ? lines.length : idx;
14998
+ return _csDepthPrefix[clamped];
14874
14999
  }
14875
15000
  function netBraces(fragment) {
14876
15001
  let n = 0;
@@ -14967,6 +15092,30 @@ function csThenBlock(lines, ifIdx, rest) {
14967
15092
  }
14968
15093
  return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
14969
15094
  }
15095
+ var _csGuardLines;
15096
+ var _csGuardMap;
15097
+ function csEqualityGuardIndex(lines) {
15098
+ if (lines === _csGuardLines && _csGuardMap) return _csGuardMap;
15099
+ const map = /* @__PURE__ */ new Map();
15100
+ for (let i2 = 0; i2 < lines.length; i2++) {
15101
+ const parts2 = splitCsIf(lines[i2]);
15102
+ if (!parts2) continue;
15103
+ const guard = csEqualityGuard(parts2.cond);
15104
+ if (!guard) continue;
15105
+ const entry = { i: i2, rest: parts2.rest, ifCol: parts2.ifCol, op: guard.op };
15106
+ for (const id of csIdentifiers(guard.exprSide)) {
15107
+ let arr = map.get(id);
15108
+ if (!arr) {
15109
+ arr = [];
15110
+ map.set(id, arr);
15111
+ }
15112
+ arr.push(entry);
15113
+ }
15114
+ }
15115
+ _csGuardLines = lines;
15116
+ _csGuardMap = map;
15117
+ return map;
15118
+ }
14970
15119
  function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
14971
15120
  if (language !== "csharp") return false;
14972
15121
  if (pattern.type !== "ssrf") return false;
@@ -14979,20 +15128,22 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
14979
15128
  if (candidates.size === 0) return false;
14980
15129
  const sinkIdx = call.location.line - 1;
14981
15130
  const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
14982
- for (let i2 = 0; i2 < sinkIdx; i2++) {
14983
- const parts2 = splitCsIf(sourceLines[i2]);
14984
- if (!parts2) continue;
14985
- const guard = csEqualityGuard(parts2.cond);
14986
- if (!guard) continue;
14987
- const guardIds = csIdentifiers(guard.exprSide);
14988
- if (![...guardIds].some((id) => candidates.has(id))) continue;
14989
- const block = csThenBlock(sourceLines, i2, parts2.rest);
14990
- if (guard.op === "==") {
14991
- if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
14992
- } else {
14993
- const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
14994
- if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
14995
- return true;
15131
+ const guardMap = csEqualityGuardIndex(sourceLines);
15132
+ const checked = /* @__PURE__ */ new Set();
15133
+ for (const cand of candidates) {
15134
+ const guards = guardMap.get(cand);
15135
+ if (!guards) continue;
15136
+ for (const g of guards) {
15137
+ if (g.i >= sinkIdx || checked.has(g)) continue;
15138
+ checked.add(g);
15139
+ const block = csThenBlock(sourceLines, g.i, g.rest);
15140
+ if (g.op === "==") {
15141
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
15142
+ } else {
15143
+ const guardDepth = csBraceDepthBefore(sourceLines, g.i) + netBraces(stripCsLiterals(sourceLines[g.i]).slice(0, g.ifCol));
15144
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
15145
+ return true;
15146
+ }
14996
15147
  }
14997
15148
  }
14998
15149
  }
@@ -5983,6 +5983,26 @@ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
5983
5983
  "ExecuteReaderAsync",
5984
5984
  "ExecuteNonQueryAsync"
5985
5985
  ]);
5986
+ var CSHARP_RECEIVER_URL_METHODS = /* @__PURE__ */ new Set([
5987
+ "GetAsync",
5988
+ "PostAsync",
5989
+ "PutAsync",
5990
+ "PatchAsync",
5991
+ "DeleteAsync",
5992
+ "HeadAsync",
5993
+ "GetStringAsync",
5994
+ "GetByteArrayAsync",
5995
+ "GetStreamAsync",
5996
+ "GetJsonAsync"
5997
+ ]);
5998
+ var CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
5999
+ function csharpBareName(node) {
6000
+ if (node.type === "generic_name") {
6001
+ const id = node.childForFieldName("name") ?? node.namedChild(0);
6002
+ if (id) return getNodeText(id);
6003
+ }
6004
+ return getNodeText(node);
6005
+ }
5986
6006
  function extractCSharpCalls(tree, cache) {
5987
6007
  const calls = [];
5988
6008
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -5994,16 +6014,22 @@ function extractCSharpCalls(tree, cache) {
5994
6014
  if (fn?.type === "member_access_expression") {
5995
6015
  const nameNode = fn.childForFieldName("name");
5996
6016
  const exprNode = fn.childForFieldName("expression");
5997
- methodName = nameNode ? getNodeText(nameNode) : "unknown";
6017
+ methodName = nameNode ? csharpBareName(nameNode) : "unknown";
5998
6018
  receiver = exprNode ? getNodeText(exprNode) : null;
5999
6019
  } else if (fn) {
6000
- methodName = getNodeText(fn);
6020
+ methodName = csharpBareName(fn);
6001
6021
  }
6002
6022
  const argsNode = inv.childForFieldName("arguments");
6003
6023
  let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
6004
6024
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
6005
6025
  args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
6006
6026
  }
6027
+ if (methodName === "Add" && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
6028
+ methodName = "AddHeader";
6029
+ }
6030
+ if (receiver && args2.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
6031
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
6032
+ }
6007
6033
  calls.push({
6008
6034
  method_name: methodName,
6009
6035
  receiver,
@@ -6019,7 +6045,7 @@ function extractCSharpCalls(tree, cache) {
6019
6045
  const typeNode = creation.childForFieldName("type");
6020
6046
  const argsNode = creation.childForFieldName("arguments");
6021
6047
  calls.push({
6022
- method_name: typeNode ? getNodeText(typeNode) : "unknown",
6048
+ method_name: typeNode ? csharpBareName(typeNode) : "unknown",
6023
6049
  receiver: null,
6024
6050
  receiver_type: null,
6025
6051
  receiver_type_fqn: null,
@@ -6032,6 +6058,39 @@ function extractCSharpCalls(tree, cache) {
6032
6058
  const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
6033
6059
  for (const asn of assignments) {
6034
6060
  const left = asn.childForFieldName("left");
6061
+ if (left?.type === "element_access_expression") {
6062
+ const target = left.childForFieldName("expression");
6063
+ const right2 = asn.childForFieldName("right");
6064
+ if (!target || !right2) continue;
6065
+ if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target))) continue;
6066
+ const subscript = left.childForFieldName("subscript") ?? left.namedChild(1);
6067
+ const rhsText2 = getNodeText(right2);
6068
+ calls.push({
6069
+ method_name: "AddHeader",
6070
+ receiver: getNodeText(target),
6071
+ receiver_type: null,
6072
+ receiver_type_fqn: null,
6073
+ arguments: [
6074
+ {
6075
+ position: 0,
6076
+ expression: subscript ? getNodeText(subscript) : "",
6077
+ variable: null,
6078
+ literal: subscript ? getNodeText(subscript) : null,
6079
+ value: null
6080
+ },
6081
+ {
6082
+ position: 1,
6083
+ expression: rhsText2,
6084
+ variable: right2.type === "identifier" ? rhsText2 : null,
6085
+ literal: right2.type === "string_literal" ? rhsText2 : null,
6086
+ value: null
6087
+ }
6088
+ ],
6089
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6090
+ in_method: findEnclosingMethod(asn)
6091
+ });
6092
+ continue;
6093
+ }
6035
6094
  if (left?.type !== "member_access_expression") continue;
6036
6095
  const nameNode = left.childForFieldName("name");
6037
6096
  if (!nameNode) continue;
@@ -13251,6 +13310,23 @@ var DEFAULT_SINKS = [
13251
13310
  { method: "ExecuteSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13252
13311
  { method: "FromSqlRaw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13253
13312
  { method: "FromSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13313
+ // Dapper micro-ORM (cognium-dev#275). Every Dapper entry point is an
13314
+ // `IDbConnection` extension whose arg 0 is raw SQL text — `conn.Query<T>(sql)`
13315
+ // is exactly as injectable as `new SqlCommand(sql)`. Taint-gated on arg 0, so
13316
+ // a constant/interpolation-free query never fires, and a same-named method on
13317
+ // an unrelated receiver (e.g. RestSharp's `client.Execute(request)`, whose
13318
+ // arg 0 is a request object rather than a tainted string) stays clean.
13319
+ { method: "Query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13320
+ { method: "QueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13321
+ { method: "QueryFirst", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13322
+ { method: "QueryFirstAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13323
+ { method: "QueryFirstOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13324
+ { method: "QueryFirstOrDefaultAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13325
+ { method: "QuerySingle", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13326
+ { method: "QuerySingleOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13327
+ { method: "QueryMultiple", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13328
+ { method: "Execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13329
+ { method: "ExecuteAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13254
13330
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13255
13331
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13256
13332
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13316,12 +13392,23 @@ var DEFAULT_SINKS = [
13316
13392
  { method: "DeleteAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13317
13393
  { method: "GetStringAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13318
13394
  { method: "GetByteArrayAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13395
+ // Remaining HttpClient/Flurl verbs (cognium-dev#275). For HttpClient these
13396
+ // take the URL at arg 0; for Flurl's fluent form the receiver is surfaced as
13397
+ // arg 0 by CSHARP_RECEIVER_URL_METHODS. Taint-gated either way.
13398
+ // (GetStreamAsync / PutAsync / DeleteAsync are already registered below.)
13399
+ { method: "GetJsonAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13400
+ { method: "PatchAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13401
+ { method: "HeadAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13319
13402
  { method: "GetStreamAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13320
13403
  { method: "SendAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13321
13404
  { method: "DownloadString", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13322
13405
  { method: "DownloadData", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13323
13406
  { method: "DownloadFile", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [1], languages: ["csharp"] },
13324
13407
  { method: "Create", class: "WebRequest", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13408
+ // RestSharp (cognium-dev#275) — the base URL is fixed at construction, so
13409
+ // `new RestClient(userUrl)` points every later request at attacker-controlled
13410
+ // infrastructure. Ctor arg 0, taint-gated.
13411
+ { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13325
13412
  // C# code injection — dynamic script/assembly loading (CWE-94).
13326
13413
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13327
13414
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13361,6 +13448,11 @@ var DEFAULT_SINKS = [
13361
13448
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13362
13449
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13363
13450
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13451
+ // `return Content(html, "text/html")` on a controller writes arg 0 to the
13452
+ // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13453
+ // `Content(constantString)` and the JSON/plain-text content types are
13454
+ // unaffected because a constant argument carries no taint.
13455
+ { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13364
13456
  // `String.Format(fmt, …)` with an attacker-controlled FORMAT string — CWE-134.
13365
13457
  // Taint-gated on arg 0 (the format), so ordinary `String.Format("...", user)` where
13366
13458
  // only an argument is tainted never fires. .NET composite formatting can't corrupt
@@ -13386,6 +13478,10 @@ var DEFAULT_SINKS = [
13386
13478
  // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13387
13479
  // attacker-controlled query document. Class-scoped (static receiver resolves).
13388
13480
  { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13481
+ // `new BsonJavaScript(userCode)` wraps a server-side JS string — the payload
13482
+ // MongoDB's `$where` / `$function` operators execute. Unconditionally
13483
+ // dangerous when tainted, so no literal gating is needed (cognium-dev#275).
13484
+ { method: "BsonJavaScript", class: "constructor", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13389
13485
  // C# log injection — Microsoft.Extensions.Logging `ILogger` (CWE-117,
13390
13486
  // cognium-ai#318). Only the message TEMPLATE (arg[0]) is the injectable
13391
13487
  // position; the structured form `LogInformation("… {Id}", id)` keeps taint in
@@ -13397,6 +13493,16 @@ var DEFAULT_SINKS = [
13397
13493
  { method: "LogDebug", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13398
13494
  { method: "LogCritical", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13399
13495
  { method: "LogTrace", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13496
+ // C# ReDoS (CWE-1333, cognium-dev#275). The injectable position is the
13497
+ // PATTERN, not the input: a user-supplied regex can be crafted with
13498
+ // catastrophic backtracking. `Regex.IsMatch(input, pattern)` and friends take
13499
+ // the pattern at arg 1; the `new Regex(pattern)` ctor takes it at arg 0. A
13500
+ // tainted *input* with a constant pattern is not ReDoS and does not fire.
13501
+ { method: "IsMatch", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13502
+ { method: "Match", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13503
+ { method: "Matches", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13504
+ { method: "Replace", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13505
+ { method: "Regex", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13400
13506
  // Generic `ILogger.Log(logLevel, message, …)` — class-scoped (`Log` alone is
13401
13507
  // too generic); the message is arg[1].
13402
13508
  { method: "Log", class: "ILogger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1], languages: ["csharp"] },
@@ -13655,6 +13761,15 @@ var DEFAULT_SANITIZERS = [
13655
13761
  // this table; the drift surfaced as a SecuriBench Micro FP on
13656
13762
  // `sanitizers/Sanitizers3.java` (`URLEncoder.encode` + `sendRedirect`).
13657
13763
  { method: "encodeForURL", removes: ["xss", "ssrf", "open_redirect"] },
13764
+ // ASP.NET Core local-redirect guards (cognium-dev#275). `Url.IsLocalUrl(x)`
13765
+ // returns false for any absolute/scheme-relative URL, so a `Redirect(x)` gated
13766
+ // on it cannot leave the site — without this credit the guarded form is a
13767
+ // false positive. `LocalRedirect(x)` is the same check applied by the
13768
+ // framework itself: it THROWS on a non-local URL, which is why it is
13769
+ // deliberately not registered as an open_redirect sink. Recording it here
13770
+ // states that intent in the model rather than leaving it as a silent omission.
13771
+ { method: "IsLocalUrl", removes: ["open_redirect"] },
13772
+ { method: "LocalRedirect", removes: ["open_redirect"] },
13658
13773
  // URL encoding wrapper aliases (common patterns in benchmarks and real-world code)
13659
13774
  { method: "encodeURL", removes: ["xss", "ssrf", "open_redirect"] },
13660
13775
  { method: "urlEncode", removes: ["xss", "ssrf", "open_redirect"] },
@@ -14796,15 +14911,25 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
14796
14911
  function stripCsLiterals(line) {
14797
14912
  return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
14798
14913
  }
14914
+ var _csDepthLines;
14915
+ var _csDepthPrefix;
14799
14916
  function csBraceDepthBefore(lines, idx) {
14800
- let depth = 0;
14801
- for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
14802
- for (const ch of stripCsLiterals(lines[i2])) {
14803
- if (ch === "{") depth++;
14804
- else if (ch === "}") depth--;
14917
+ if (lines !== _csDepthLines) {
14918
+ const pref = new Int32Array(lines.length + 1);
14919
+ let depth = 0;
14920
+ for (let i2 = 0; i2 < lines.length; i2++) {
14921
+ pref[i2] = depth;
14922
+ for (const ch of stripCsLiterals(lines[i2])) {
14923
+ if (ch === "{") depth++;
14924
+ else if (ch === "}") depth--;
14925
+ }
14805
14926
  }
14927
+ pref[lines.length] = depth;
14928
+ _csDepthLines = lines;
14929
+ _csDepthPrefix = pref;
14806
14930
  }
14807
- return depth;
14931
+ const clamped = idx < 0 ? 0 : idx > lines.length ? lines.length : idx;
14932
+ return _csDepthPrefix[clamped];
14808
14933
  }
14809
14934
  function netBraces(fragment) {
14810
14935
  let n = 0;
@@ -14901,6 +15026,30 @@ function csThenBlock(lines, ifIdx, rest) {
14901
15026
  }
14902
15027
  return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
14903
15028
  }
15029
+ var _csGuardLines;
15030
+ var _csGuardMap;
15031
+ function csEqualityGuardIndex(lines) {
15032
+ if (lines === _csGuardLines && _csGuardMap) return _csGuardMap;
15033
+ const map = /* @__PURE__ */ new Map();
15034
+ for (let i2 = 0; i2 < lines.length; i2++) {
15035
+ const parts2 = splitCsIf(lines[i2]);
15036
+ if (!parts2) continue;
15037
+ const guard = csEqualityGuard(parts2.cond);
15038
+ if (!guard) continue;
15039
+ const entry = { i: i2, rest: parts2.rest, ifCol: parts2.ifCol, op: guard.op };
15040
+ for (const id of csIdentifiers(guard.exprSide)) {
15041
+ let arr = map.get(id);
15042
+ if (!arr) {
15043
+ arr = [];
15044
+ map.set(id, arr);
15045
+ }
15046
+ arr.push(entry);
15047
+ }
15048
+ }
15049
+ _csGuardLines = lines;
15050
+ _csGuardMap = map;
15051
+ return map;
15052
+ }
14904
15053
  function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
14905
15054
  if (language !== "csharp") return false;
14906
15055
  if (pattern.type !== "ssrf") return false;
@@ -14913,20 +15062,22 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
14913
15062
  if (candidates.size === 0) return false;
14914
15063
  const sinkIdx = call.location.line - 1;
14915
15064
  const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
14916
- for (let i2 = 0; i2 < sinkIdx; i2++) {
14917
- const parts2 = splitCsIf(sourceLines[i2]);
14918
- if (!parts2) continue;
14919
- const guard = csEqualityGuard(parts2.cond);
14920
- if (!guard) continue;
14921
- const guardIds = csIdentifiers(guard.exprSide);
14922
- if (![...guardIds].some((id) => candidates.has(id))) continue;
14923
- const block = csThenBlock(sourceLines, i2, parts2.rest);
14924
- if (guard.op === "==") {
14925
- if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
14926
- } else {
14927
- const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
14928
- if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
14929
- return true;
15065
+ const guardMap = csEqualityGuardIndex(sourceLines);
15066
+ const checked = /* @__PURE__ */ new Set();
15067
+ for (const cand of candidates) {
15068
+ const guards = guardMap.get(cand);
15069
+ if (!guards) continue;
15070
+ for (const g of guards) {
15071
+ if (g.i >= sinkIdx || checked.has(g)) continue;
15072
+ checked.add(g);
15073
+ const block = csThenBlock(sourceLines, g.i, g.rest);
15074
+ if (g.op === "==") {
15075
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
15076
+ } else {
15077
+ const guardDepth = csBraceDepthBefore(sourceLines, g.i) + netBraces(stripCsLiterals(sourceLines[g.i]).slice(0, g.ifCol));
15078
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
15079
+ return true;
15080
+ }
14930
15081
  }
14931
15082
  }
14932
15083
  }