circle-ir 4.9.9 → 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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +56 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +8 -2
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +26 -5
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +140 -14
- package/dist/core/circle-ir-core.cjs +118 -3
- package/dist/core/circle-ir-core.js +118 -3
- package/dist/core/extractors/calls.js +102 -3
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/languages/plugins/rust.d.ts.map +1 -1
- package/dist/languages/plugins/rust.js +5 -8
- package/dist/languages/plugins/rust.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 ?
|
|
6017
|
+
methodName = nameNode ? csharpBareName(nameNode) : "unknown";
|
|
5998
6018
|
receiver = exprNode ? getNodeText(exprNode) : null;
|
|
5999
6019
|
} else if (fn) {
|
|
6000
|
-
methodName =
|
|
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 ?
|
|
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"] },
|
|
@@ -160,6 +160,53 @@ const CSHARP_COMMAND_EXECUTE_METHODS = new Set([
|
|
|
160
160
|
'ExecuteScalar', 'ExecuteReader', 'ExecuteNonQuery',
|
|
161
161
|
'ExecuteScalarAsync', 'ExecuteReaderAsync', 'ExecuteNonQueryAsync',
|
|
162
162
|
]);
|
|
163
|
+
/**
|
|
164
|
+
* Flurl fluent HTTP methods (cognium-dev#275). Flurl extends `string`/`Url`, so
|
|
165
|
+
* the request target is the RECEIVER — `userUrl.GetStringAsync()` — which the
|
|
166
|
+
* arg-position sink model cannot address.
|
|
167
|
+
*
|
|
168
|
+
* These names collide with `HttpClient`'s (`client.GetStringAsync(url)`), where
|
|
169
|
+
* the URL is already arg 0 and the receiver is an untainted client. Prepending
|
|
170
|
+
* the receiver unconditionally would shift that URL to index 1 and silently
|
|
171
|
+
* break every existing HttpClient SSRF detection. The two are disambiguated by
|
|
172
|
+
* arity instead: the Flurl fluent form passes no URL argument, HttpClient always
|
|
173
|
+
* does — so the receiver is only surfaced as arg 0 for a ZERO-argument call.
|
|
174
|
+
*
|
|
175
|
+
* Consequence: body-carrying Flurl verbs (`url.PostJsonAsync(dto)`,
|
|
176
|
+
* `url.DownloadFileAsync(dir)`) are NOT covered — they pass an argument, so the
|
|
177
|
+
* guard skips them. Covering those needs real receiver-type resolution to tell a
|
|
178
|
+
* `Url`/`string` receiver from an `HttpClient` one; listing their names here
|
|
179
|
+
* without it would only add entries the guard can never reach.
|
|
180
|
+
*/
|
|
181
|
+
const CSHARP_RECEIVER_URL_METHODS = new Set([
|
|
182
|
+
'GetAsync', 'PostAsync', 'PutAsync', 'PatchAsync', 'DeleteAsync', 'HeadAsync',
|
|
183
|
+
'GetStringAsync', 'GetByteArrayAsync', 'GetStreamAsync', 'GetJsonAsync',
|
|
184
|
+
]);
|
|
185
|
+
/**
|
|
186
|
+
* Receiver expressions that denote an ASP.NET header dictionary —
|
|
187
|
+
* `Response.Headers`, `ctx.Response.Headers`, `req.Headers`. Used to normalize
|
|
188
|
+
* `.Headers.Add(k, v)` / `.Headers[k] = v` onto the registered `AddHeader`
|
|
189
|
+
* CRLF sink without registering the ubiquitous bare `Add`. (cognium-dev#275)
|
|
190
|
+
*/
|
|
191
|
+
const CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
|
|
192
|
+
/**
|
|
193
|
+
* Bare method/type name for a C# node, with any generic type-argument list
|
|
194
|
+
* stripped (cognium-dev#275).
|
|
195
|
+
*
|
|
196
|
+
* tree-sitter-c-sharp represents `conn.Query<User>(sql)` with a `generic_name`
|
|
197
|
+
* in the `name` field, so `getNodeText` yields `"Query<User>"` — which matches
|
|
198
|
+
* no registry entry, making EVERY generic C# call invisible to the sink matcher
|
|
199
|
+
* (Dapper's `Query<T>`/`QueryAsync<T>`, `Deserialize<T>`, `GetService<T>`, …).
|
|
200
|
+
* Taking the identifier child restores the bare name the registry keys on.
|
|
201
|
+
*/
|
|
202
|
+
function csharpBareName(node) {
|
|
203
|
+
if (node.type === 'generic_name') {
|
|
204
|
+
const id = node.childForFieldName('name') ?? node.namedChild(0);
|
|
205
|
+
if (id)
|
|
206
|
+
return getNodeText(id);
|
|
207
|
+
}
|
|
208
|
+
return getNodeText(node);
|
|
209
|
+
}
|
|
163
210
|
function extractCSharpCalls(tree, cache) {
|
|
164
211
|
const calls = [];
|
|
165
212
|
const typeMap = buildCSharpReceiverTypeMap(tree, cache);
|
|
@@ -171,11 +218,11 @@ function extractCSharpCalls(tree, cache) {
|
|
|
171
218
|
if (fn?.type === 'member_access_expression') {
|
|
172
219
|
const nameNode = fn.childForFieldName('name');
|
|
173
220
|
const exprNode = fn.childForFieldName('expression');
|
|
174
|
-
methodName = nameNode ?
|
|
221
|
+
methodName = nameNode ? csharpBareName(nameNode) : 'unknown';
|
|
175
222
|
receiver = exprNode ? getNodeText(exprNode) : null;
|
|
176
223
|
}
|
|
177
224
|
else if (fn) {
|
|
178
|
-
methodName =
|
|
225
|
+
methodName = csharpBareName(fn);
|
|
179
226
|
}
|
|
180
227
|
const argsNode = inv.childForFieldName('arguments');
|
|
181
228
|
let args = argsNode ? extractCSharpArguments(argsNode) : [];
|
|
@@ -187,6 +234,20 @@ function extractCSharpCalls(tree, cache) {
|
|
|
187
234
|
if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
|
|
188
235
|
args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args];
|
|
189
236
|
}
|
|
237
|
+
// `Response.Headers.Add(name, value)` is the ASP.NET Core response-header
|
|
238
|
+
// writer, but a bare `Add` is far too common to register as a CRLF sink
|
|
239
|
+
// (List.Add, Dictionary.Add, …). Normalize the header-dictionary form to the
|
|
240
|
+
// already-registered `AddHeader` name instead, gated on the receiver
|
|
241
|
+
// expression ending in `.Headers`, so the existing arg-1 crlf sink matches
|
|
242
|
+
// and nothing else named `Add` is touched. (cognium-dev#275)
|
|
243
|
+
if (methodName === 'Add' && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
|
|
244
|
+
methodName = 'AddHeader';
|
|
245
|
+
}
|
|
246
|
+
// Flurl fluent form only — see CSHARP_RECEIVER_URL_METHODS. The arity guard
|
|
247
|
+
// is what keeps `client.GetStringAsync(url)` (HttpClient) untouched.
|
|
248
|
+
if (receiver && args.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
|
|
249
|
+
args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
|
|
250
|
+
}
|
|
190
251
|
calls.push({
|
|
191
252
|
method_name: methodName,
|
|
192
253
|
receiver,
|
|
@@ -202,7 +263,7 @@ function extractCSharpCalls(tree, cache) {
|
|
|
202
263
|
const typeNode = creation.childForFieldName('type');
|
|
203
264
|
const argsNode = creation.childForFieldName('arguments');
|
|
204
265
|
calls.push({
|
|
205
|
-
method_name: typeNode ?
|
|
266
|
+
method_name: typeNode ? csharpBareName(typeNode) : 'unknown',
|
|
206
267
|
receiver: null,
|
|
207
268
|
receiver_type: null,
|
|
208
269
|
receiver_type_fqn: null,
|
|
@@ -221,6 +282,44 @@ function extractCSharpCalls(tree, cache) {
|
|
|
221
282
|
const assignments = getNodesFromCache(tree.rootNode, 'assignment_expression', cache);
|
|
222
283
|
for (const asn of assignments) {
|
|
223
284
|
const left = asn.childForFieldName('left');
|
|
285
|
+
// `Response.Headers["X-Foo"] = tainted` — the indexer form of the header
|
|
286
|
+
// write above. Emitted as `AddHeader(name, value)` so the same arg-1 crlf
|
|
287
|
+
// sink matches. (cognium-dev#275)
|
|
288
|
+
if (left?.type === 'element_access_expression') {
|
|
289
|
+
const target = left.childForFieldName('expression');
|
|
290
|
+
const right = asn.childForFieldName('right');
|
|
291
|
+
if (!target || !right)
|
|
292
|
+
continue;
|
|
293
|
+
if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target)))
|
|
294
|
+
continue;
|
|
295
|
+
const subscript = left.childForFieldName('subscript') ?? left.namedChild(1);
|
|
296
|
+
const rhsText = getNodeText(right);
|
|
297
|
+
calls.push({
|
|
298
|
+
method_name: 'AddHeader',
|
|
299
|
+
receiver: getNodeText(target),
|
|
300
|
+
receiver_type: null,
|
|
301
|
+
receiver_type_fqn: null,
|
|
302
|
+
arguments: [
|
|
303
|
+
{
|
|
304
|
+
position: 0,
|
|
305
|
+
expression: subscript ? getNodeText(subscript) : '',
|
|
306
|
+
variable: null,
|
|
307
|
+
literal: subscript ? getNodeText(subscript) : null,
|
|
308
|
+
value: null,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
position: 1,
|
|
312
|
+
expression: rhsText,
|
|
313
|
+
variable: right.type === 'identifier' ? rhsText : null,
|
|
314
|
+
literal: right.type === 'string_literal' ? rhsText : null,
|
|
315
|
+
value: null,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
|
|
319
|
+
in_method: findEnclosingMethod(asn),
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
224
323
|
if (left?.type !== 'member_access_expression')
|
|
225
324
|
continue;
|
|
226
325
|
const nameNode = left.childForFieldName('name');
|