circle-ir 4.9.9 → 4.9.11

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"] },
@@ -14941,14 +15056,30 @@ var PYTHON_TAINTED_PATTERNS = [
14941
15056
  // regex registry so it's recognized in subscript/var contexts.
14942
15057
  { pattern: /\binput\s*\(/, sourceType: "io_input" }
14943
15058
  ];
15059
+ function dropCSharpObjectCarriedWaypoints(sinks, calls) {
15060
+ const zeroArgCtors = /* @__PURE__ */ new Set();
15061
+ for (const call of calls) {
15062
+ if (call.is_constructor && call.arguments.length === 0) {
15063
+ zeroArgCtors.add(`${call.location.line}:${call.method_name}`);
15064
+ }
15065
+ }
15066
+ return sinks.filter((sink) => {
15067
+ if (sink.type !== "sql_injection") return true;
15068
+ if (sink.method && zeroArgCtors.has(`${sink.line}:${sink.method}`)) {
15069
+ return false;
15070
+ }
15071
+ return true;
15072
+ });
15073
+ }
14944
15074
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
14945
15075
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
14946
15076
  const sources = findSources(calls, types, config.sources, sourceLines, language);
14947
15077
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
14948
15078
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14949
15079
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
15080
+ const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14950
15081
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
14951
- return { sources, sinks, sanitizers };
15082
+ return { sources, sinks: gatedSinks, sanitizers };
14952
15083
  }
14953
15084
  function sinkPatternAppliesTo(pattern, language) {
14954
15085
  if (language === void 0) return true;
@@ -15219,7 +15350,7 @@ function findSources(calls, types, patterns, sourceLines, language) {
15219
15350
  }
15220
15351
  const sourceMap = /* @__PURE__ */ new Map();
15221
15352
  for (const source of sources) {
15222
- const key = `${source.line}:${source.type}`;
15353
+ const key = source.type === "interprocedural_param" && source.variable ? `${source.line}:${source.type}:${source.variable}` : `${source.line}:${source.type}`;
15223
15354
  const existing = sourceMap.get(key);
15224
15355
  if (!existing || source.confidence > existing.confidence) {
15225
15356
  sourceMap.set(key, source);
@@ -15837,6 +15968,15 @@ var CWE_78_RECEIVER_ALLOWLIST = /* @__PURE__ */ new Set([
15837
15968
  // hutool
15838
15969
  "RuntimeUtil"
15839
15970
  ]);
15971
+ function isRegexReceiver(receiver, sourceLines) {
15972
+ const r = (receiver ?? "").trim();
15973
+ if (r.length === 0) return false;
15974
+ if (r.startsWith("/") && /\/[a-z]*$/.test(r)) return true;
15975
+ if (/^(?:new\s+)?RegExp\s*\(/.test(r)) return true;
15976
+ if (!/^[A-Za-z_$][\w$]*$/.test(r) || !sourceLines) return false;
15977
+ const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
15978
+ return sourceLines.some((l) => decl.test(l));
15979
+ }
15840
15980
  function isFunctionCallbackArgument(arg) {
15841
15981
  if (arg.literal !== null && arg.literal !== void 0) return false;
15842
15982
  const expr = (arg.expression ?? "").trim();
@@ -16019,6 +16159,12 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16019
16159
  continue;
16020
16160
  }
16021
16161
  }
16162
+ if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
16163
+ continue;
16164
+ }
16165
+ if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
16166
+ continue;
16167
+ }
16022
16168
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
16023
16169
  continue;
16024
16170
  }
@@ -17200,7 +17346,13 @@ function canSourceReachSink(sourceType, sinkType) {
17200
17346
  // scan path (`generateFindings`) dropped every `http_*/io/network/interproc →
17201
17347
  // prompt_injection` flow even though `taint.flows` / the trust pass reported
17202
17348
  // it — the same "two paths disagree" shape as #129. Now emitted from scan too.
17203
- 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"],
17349
+ // xxe added (cognium-dev #282): `request.getParameter("xml")` reaching an
17350
+ // XML parser (DocumentBuilder.parse etc.) is XXE, but xxe was absent from
17351
+ // http_param/http_query (present on http_body/io_input/file_input), so the
17352
+ // pairing was gated out and generateFindings dropped it — same class as the
17353
+ // #129 log_injection/format_string/nosql_injection drop. Mirrors how
17354
+ // deserialization already sits on both http_param and http_query.
17355
+ 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"],
17204
17356
  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"],
17205
17357
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
17206
17358
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary", "log_injection", "format_string", "nosql_injection", "prompt_injection"],
@@ -17211,7 +17363,7 @@ function canSourceReachSink(sourceType, sinkType) {
17211
17363
  // annotated `/* BAD */`. Prior to 3.163.0 the reach map omitted xss
17212
17364
  // so http_path → xss inline-colocation flows were silently dropped.
17213
17365
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary", "xss", "log_injection", "format_string", "prompt_injection"],
17214
- 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"],
17366
+ 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"],
17215
17367
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
17216
17368
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
17217
17369
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -18663,6 +18815,9 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
18663
18815
  for (const source of sources) {
18664
18816
  const defsOnLine = defsByLine.get(source.line) ?? [];
18665
18817
  for (const def of defsOnLine) {
18818
+ if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
18819
+ continue;
18820
+ }
18666
18821
  tainted.push({
18667
18822
  variable: def.variable,
18668
18823
  defId: def.id,
@@ -24724,14 +24879,11 @@ var RustPlugin = class extends BaseLanguagePlugin {
24724
24879
  severity: "high",
24725
24880
  argPositions: [0]
24726
24881
  },
24727
- // Format String (Rust is mostly safe, but unchecked format! can be dangerous)
24728
- {
24729
- method: "format!",
24730
- type: "format_string",
24731
- cwe: "CWE-134",
24732
- severity: "medium",
24733
- argPositions: [0]
24734
- },
24882
+ // Format String — `format!` is NOT a CWE-134 sink in Rust. Its template
24883
+ // is a compile-time string literal; a non-literal template does not
24884
+ // compile, so an attacker-controlled format string is not expressible.
24885
+ // Interpolating data into `format!` is the ordinary, safe way to build a
24886
+ // string. (cognium-dev #294 part 1 — removed as a format_string sink.)
24735
24887
  // Unsafe operations
24736
24888
  {
24737
24889
  method: "from_raw_parts",
@@ -27571,11 +27723,19 @@ function findCSharpRequestSources(sourceCode, language) {
27571
27723
  const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
27572
27724
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
27573
27725
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
27726
+ const formFileParams = /* @__PURE__ */ new Set();
27727
+ const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
27728
+ for (const line of lines) {
27729
+ const re = new RegExp(formFileDeclRe.source, "g");
27730
+ let d;
27731
+ while ((d = re.exec(line)) !== null) formFileParams.add(d[1]);
27732
+ }
27733
+ const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
27574
27734
  for (let i2 = 0; i2 < lines.length; i2++) {
27575
27735
  const m = assignRe.exec(lines[i2]);
27576
27736
  if (!m) continue;
27577
27737
  const [, varName, rhs] = m;
27578
- const type = requestReadRe.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
27738
+ const type = requestReadRe.test(rhs) ? "http_param" : formFileReadRe?.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
27579
27739
  if (!type) continue;
27580
27740
  const lineNumber = i2 + 1;
27581
27741
  if (sources.some((s) => s.line === lineNumber && s.variable === varName)) continue;
@@ -36206,7 +36366,7 @@ var TaintPropagationPass = class {
36206
36366
  for (const f of paramFlows) {
36207
36367
  pushIfNew(f);
36208
36368
  }
36209
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
36369
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language, types) ?? [];
36210
36370
  for (const f of exprScanFlows) {
36211
36371
  if (flowKeys.has(flowKey(f))) continue;
36212
36372
  const flowForCheck = {
@@ -36358,6 +36518,36 @@ var TaintPropagationPass = class {
36358
36518
  return bestByKey.get(key) === f;
36359
36519
  });
36360
36520
  }
36521
+ if (ctx.language === "csharp" && finalFlows.length > 1) {
36522
+ const CSHARP_ADO_EXECUTE = /^Execute(?:Reader|NonQuery|Scalar)(?:Async)?$/;
36523
+ const commandTextReceiverByLine = /* @__PURE__ */ new Map();
36524
+ const execLinesByReceiver = /* @__PURE__ */ new Map();
36525
+ for (const call of calls) {
36526
+ const receiver = (call.receiver ?? "").trim();
36527
+ if (!receiver) continue;
36528
+ if (call.method_name === "CommandText") {
36529
+ commandTextReceiverByLine.set(call.location.line, receiver);
36530
+ } else if (CSHARP_ADO_EXECUTE.test(call.method_name)) {
36531
+ const lines = execLinesByReceiver.get(receiver) ?? [];
36532
+ lines.push(call.location.line);
36533
+ execLinesByReceiver.set(receiver, lines);
36534
+ }
36535
+ }
36536
+ if (commandTextReceiverByLine.size > 0) {
36537
+ const reportedSqlSinkLines = new Set(
36538
+ finalFlows.filter((f) => f.sink_type === "sql_injection").map((f) => f.sink_line)
36539
+ );
36540
+ finalFlows = finalFlows.filter((f) => {
36541
+ if (f.sink_type !== "sql_injection") return true;
36542
+ const receiver = commandTextReceiverByLine.get(f.sink_line);
36543
+ if (receiver === void 0) return true;
36544
+ const execLines = execLinesByReceiver.get(receiver) ?? [];
36545
+ return !execLines.some(
36546
+ (line) => line > f.sink_line && reportedSqlSinkLines.has(line)
36547
+ );
36548
+ });
36549
+ }
36550
+ }
36361
36551
  return { flows: finalFlows };
36362
36552
  }
36363
36553
  };
@@ -36775,7 +36965,7 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
36775
36965
  }
36776
36966
  return false;
36777
36967
  }
36778
- function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
36968
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
36779
36969
  const flows = [];
36780
36970
  const sourcesWithVar = sources.filter(
36781
36971
  (s) => typeof s.variable === "string" && s.variable.length > 0
@@ -37032,12 +37222,32 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37032
37222
  for (const s of sourcesWithVar) {
37033
37223
  if (s.line < anchor.line) anchor = s;
37034
37224
  }
37225
+ const methodAt = (line) => {
37226
+ for (const t of types ?? []) {
37227
+ for (const m of t.methods) {
37228
+ if (line >= m.start_line && line <= m.end_line) return m.name;
37229
+ }
37230
+ }
37231
+ return null;
37232
+ };
37035
37233
  const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
37036
- for (const [varName] of derived) {
37234
+ for (const [varName, derivedLine] of derived) {
37037
37235
  if (!varName || existingVars.has(varName)) continue;
37236
+ const owner = methodAt(derivedLine);
37237
+ let scopedAnchor = anchor;
37238
+ if (owner) {
37239
+ let best;
37240
+ for (const s of sourcesWithVar) {
37241
+ const sOwner = s.in_method ?? methodAt(s.line);
37242
+ if (sOwner !== owner) continue;
37243
+ if (!best || s.line < best.line) best = s;
37244
+ }
37245
+ if (best) scopedAnchor = best;
37246
+ }
37038
37247
  sourcesWithVar.push({
37039
- ...anchor,
37040
- variable: varName
37248
+ ...scopedAnchor,
37249
+ variable: varName,
37250
+ ...owner ? { in_method: owner } : {}
37041
37251
  });
37042
37252
  existingVars.add(varName);
37043
37253
  }