circle-ir 4.9.27 → 4.9.28
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 +18 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +130 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +63 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +108 -1
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +97 -4
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.d.ts.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.js +15 -0
- package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +21 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +322 -10
- package/dist/core/circle-ir-core.cjs +89 -5
- package/dist/core/circle-ir-core.js +89 -5
- package/dist/core/extractors/calls.js +127 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/package.json +1 -1
|
@@ -6184,6 +6184,31 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6184
6184
|
});
|
|
6185
6185
|
continue;
|
|
6186
6186
|
}
|
|
6187
|
+
if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
|
|
6188
|
+
const creation = asn.parent.parent;
|
|
6189
|
+
if (creation?.type !== "object_creation_expression") continue;
|
|
6190
|
+
const ctorType = creation.childForFieldName("type");
|
|
6191
|
+
const right2 = asn.childForFieldName("right");
|
|
6192
|
+
if (!ctorType || !right2) continue;
|
|
6193
|
+
const typeName = csharpBareName(ctorType);
|
|
6194
|
+
const rhsText2 = getNodeText(right2);
|
|
6195
|
+
calls.push({
|
|
6196
|
+
method_name: getNodeText(left),
|
|
6197
|
+
receiver: typeName,
|
|
6198
|
+
receiver_type: typeName,
|
|
6199
|
+
receiver_type_fqn: null,
|
|
6200
|
+
arguments: [{
|
|
6201
|
+
position: 0,
|
|
6202
|
+
expression: rhsText2,
|
|
6203
|
+
variable: right2.type === "identifier" ? rhsText2 : null,
|
|
6204
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
|
|
6205
|
+
value: null
|
|
6206
|
+
}],
|
|
6207
|
+
location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
|
|
6208
|
+
in_method: findEnclosingMethod(asn)
|
|
6209
|
+
});
|
|
6210
|
+
continue;
|
|
6211
|
+
}
|
|
6187
6212
|
if (left?.type !== "member_access_expression") continue;
|
|
6188
6213
|
const nameNode = left.childForFieldName("name");
|
|
6189
6214
|
if (!nameNode) continue;
|
|
@@ -6193,6 +6218,13 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6193
6218
|
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6194
6219
|
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6195
6220
|
if (recvType !== "DirectorySearcher") continue;
|
|
6221
|
+
} else if (propName === "StatusDescription") {
|
|
6222
|
+
const recv = exprNode ? getNodeText(exprNode) : "";
|
|
6223
|
+
if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
|
|
6224
|
+
} else if (propName === "BaseAddress") {
|
|
6225
|
+
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6226
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6227
|
+
if (recvType !== "HttpClient") continue;
|
|
6196
6228
|
} else if (propName !== "CommandText") {
|
|
6197
6229
|
continue;
|
|
6198
6230
|
}
|
|
@@ -6215,8 +6247,34 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6215
6247
|
in_method: findEnclosingMethod(asn)
|
|
6216
6248
|
});
|
|
6217
6249
|
}
|
|
6250
|
+
const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
|
|
6251
|
+
for (const cast of casts) {
|
|
6252
|
+
const typeNode = cast.childForFieldName("type");
|
|
6253
|
+
const valueNode = cast.childForFieldName("value");
|
|
6254
|
+
if (!typeNode || !valueNode) continue;
|
|
6255
|
+
const typeName = csharpBareName(typeNode);
|
|
6256
|
+
if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
|
|
6257
|
+
const valueText = getNodeText(valueNode);
|
|
6258
|
+
calls.push({
|
|
6259
|
+
method_name: typeName,
|
|
6260
|
+
receiver: null,
|
|
6261
|
+
receiver_type: null,
|
|
6262
|
+
receiver_type_fqn: null,
|
|
6263
|
+
arguments: [{
|
|
6264
|
+
position: 0,
|
|
6265
|
+
expression: valueText,
|
|
6266
|
+
variable: valueNode.type === "identifier" ? valueText : null,
|
|
6267
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
|
|
6268
|
+
value: null
|
|
6269
|
+
}],
|
|
6270
|
+
location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
|
|
6271
|
+
in_method: findEnclosingMethod(cast),
|
|
6272
|
+
is_constructor: true
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6218
6275
|
return calls;
|
|
6219
6276
|
}
|
|
6277
|
+
var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
|
|
6220
6278
|
function extractCSharpArguments(argsNode) {
|
|
6221
6279
|
const args2 = [];
|
|
6222
6280
|
let position = 0;
|
|
@@ -6236,12 +6294,22 @@ function extractCSharpArguments(argsNode) {
|
|
|
6236
6294
|
position: position++,
|
|
6237
6295
|
expression: text,
|
|
6238
6296
|
variable: expr?.type === "identifier" ? text : null,
|
|
6239
|
-
literal: expr
|
|
6297
|
+
literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
|
|
6240
6298
|
value: null
|
|
6241
6299
|
});
|
|
6242
6300
|
}
|
|
6243
6301
|
return args2;
|
|
6244
6302
|
}
|
|
6303
|
+
var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6304
|
+
"string_literal",
|
|
6305
|
+
"verbatim_string_literal",
|
|
6306
|
+
"raw_string_literal",
|
|
6307
|
+
"character_literal",
|
|
6308
|
+
"integer_literal",
|
|
6309
|
+
"real_literal",
|
|
6310
|
+
"boolean_literal",
|
|
6311
|
+
"null_literal"
|
|
6312
|
+
]);
|
|
6245
6313
|
function extractJavaScriptCalls(tree, cache) {
|
|
6246
6314
|
const calls = [];
|
|
6247
6315
|
const context = buildJSResolutionContext(tree, cache);
|
|
@@ -13892,9 +13960,14 @@ var DEFAULT_SINKS = [
|
|
|
13892
13960
|
{ method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
|
|
13893
13961
|
{ method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13894
13962
|
{ method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13895
|
-
//
|
|
13896
|
-
|
|
13897
|
-
|
|
13963
|
+
// #294/#484: no classless `from_str` / `from_slice` deserialization sink.
|
|
13964
|
+
// Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
|
|
13965
|
+
// `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
|
|
13966
|
+
// parsing, which cannot instantiate an attacker-chosen type. Removing them
|
|
13967
|
+
// was previously blocked because the class-scoped rows below could not match
|
|
13968
|
+
// the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
|
|
13969
|
+
// `::` path-separator fix in taint-matcher.ts makes them match, so the
|
|
13970
|
+
// classless rows are no longer load-bearing.
|
|
13898
13971
|
// Rust XSS (actix-web, rocket, axum response body)
|
|
13899
13972
|
{ method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
13900
13973
|
{ method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
@@ -14300,6 +14373,10 @@ var DEFAULT_SINKS = [
|
|
|
14300
14373
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
14301
14374
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
14302
14375
|
{ method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14376
|
+
// #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
|
|
14377
|
+
// attacker text into the HTTP status line (CWE-81). Surfaced by the
|
|
14378
|
+
// property-assignment synthesiser in the C# extractor, receiver-gated there.
|
|
14379
|
+
{ method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
|
|
14303
14380
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
14304
14381
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
14305
14382
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -14314,6 +14391,12 @@ var DEFAULT_SINKS = [
|
|
|
14314
14391
|
// filter is the constructor argument.
|
|
14315
14392
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14316
14393
|
{ method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14394
|
+
// #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
|
|
14395
|
+
// host and port of every RELATIVE request made on that client, so the
|
|
14396
|
+
// request arguments can all be constant and the destination still be
|
|
14397
|
+
// attacker-chosen. Surfaced by the property-assignment synthesiser, which
|
|
14398
|
+
// class-gates it to a resolved `HttpClient`.
|
|
14399
|
+
{ method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14317
14400
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
14318
14401
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
14319
14402
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16777,8 +16860,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16777
16860
|
return false;
|
|
16778
16861
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16779
16862
|
const target = call.resolution?.target;
|
|
16863
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16780
16864
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16781
|
-
if (
|
|
16865
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16782
16866
|
} else {
|
|
16783
16867
|
return false;
|
|
16784
16868
|
}
|
|
@@ -19345,6 +19429,122 @@ var GO_PURE_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
19345
19429
|
"hex",
|
|
19346
19430
|
"base64"
|
|
19347
19431
|
]);
|
|
19432
|
+
var CSHARP_PURE_STATIC_RECEIVERS = /* @__PURE__ */ new Set([
|
|
19433
|
+
// formatting / conversion / parsing — return a derived value
|
|
19434
|
+
"string",
|
|
19435
|
+
"String",
|
|
19436
|
+
"Convert",
|
|
19437
|
+
"Encoding",
|
|
19438
|
+
"BitConverter",
|
|
19439
|
+
"Math",
|
|
19440
|
+
"Int16",
|
|
19441
|
+
"Int32",
|
|
19442
|
+
"Int64",
|
|
19443
|
+
"UInt16",
|
|
19444
|
+
"UInt32",
|
|
19445
|
+
"UInt64",
|
|
19446
|
+
"Double",
|
|
19447
|
+
"Single",
|
|
19448
|
+
"Decimal",
|
|
19449
|
+
"Boolean",
|
|
19450
|
+
"Byte",
|
|
19451
|
+
"SByte",
|
|
19452
|
+
"Char",
|
|
19453
|
+
"Guid",
|
|
19454
|
+
"DateTime",
|
|
19455
|
+
"DateTimeOffset",
|
|
19456
|
+
"TimeSpan",
|
|
19457
|
+
"Enum",
|
|
19458
|
+
"Array",
|
|
19459
|
+
"Buffer",
|
|
19460
|
+
"Nullable",
|
|
19461
|
+
"CultureInfo",
|
|
19462
|
+
"StringComparer",
|
|
19463
|
+
"Comparer",
|
|
19464
|
+
"EqualityComparer",
|
|
19465
|
+
// logging / diagnostics receivers
|
|
19466
|
+
"Console",
|
|
19467
|
+
"Debug",
|
|
19468
|
+
"Trace",
|
|
19469
|
+
"Logger",
|
|
19470
|
+
"logger",
|
|
19471
|
+
"_logger",
|
|
19472
|
+
"Log"
|
|
19473
|
+
]);
|
|
19474
|
+
var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
|
|
19475
|
+
// string / value derivation
|
|
19476
|
+
"ToString",
|
|
19477
|
+
"ToArray",
|
|
19478
|
+
"ToList",
|
|
19479
|
+
"ToCharArray",
|
|
19480
|
+
"GetHashCode",
|
|
19481
|
+
"Equals",
|
|
19482
|
+
"CompareTo",
|
|
19483
|
+
"Trim",
|
|
19484
|
+
"TrimStart",
|
|
19485
|
+
"TrimEnd",
|
|
19486
|
+
"Substring",
|
|
19487
|
+
"Split",
|
|
19488
|
+
"Join",
|
|
19489
|
+
"Concat",
|
|
19490
|
+
"PadLeft",
|
|
19491
|
+
"PadRight",
|
|
19492
|
+
"ToLower",
|
|
19493
|
+
"ToUpper",
|
|
19494
|
+
"ToLowerInvariant",
|
|
19495
|
+
"ToUpperInvariant",
|
|
19496
|
+
"StartsWith",
|
|
19497
|
+
"EndsWith",
|
|
19498
|
+
"Contains",
|
|
19499
|
+
"IndexOf",
|
|
19500
|
+
"LastIndexOf",
|
|
19501
|
+
"Insert",
|
|
19502
|
+
"Remove",
|
|
19503
|
+
"Normalize",
|
|
19504
|
+
"Append",
|
|
19505
|
+
"AppendLine",
|
|
19506
|
+
"AppendFormat",
|
|
19507
|
+
"TryParse",
|
|
19508
|
+
"Parse",
|
|
19509
|
+
// structured logging (Microsoft.Extensions.Logging, Serilog, NLog, log4net)
|
|
19510
|
+
"LogInformation",
|
|
19511
|
+
"LogWarning",
|
|
19512
|
+
"LogError",
|
|
19513
|
+
"LogDebug",
|
|
19514
|
+
"LogTrace",
|
|
19515
|
+
"LogCritical",
|
|
19516
|
+
"Information",
|
|
19517
|
+
"Warning",
|
|
19518
|
+
"Verbose",
|
|
19519
|
+
"Fatal",
|
|
19520
|
+
"BeginScope",
|
|
19521
|
+
// Collection stores. A value put into a container has not left the program —
|
|
19522
|
+
// the same reasoning that puts Go's `append` in GO_BUILTIN_CALLS. Measured
|
|
19523
|
+
// on the 10 scored Juliet C# families these were 1,296 of the 2,368 escape
|
|
19524
|
+
// rows remaining after the first pass of this gate (#474): `Add` 864,
|
|
19525
|
+
// `AddLast` 432.
|
|
19526
|
+
//
|
|
19527
|
+
// Project methods are deliberately NOT gated, matching the Go gate's rule
|
|
19528
|
+
// that an unknown call must still escape: Juliet's own `BadSink` /
|
|
19529
|
+
// `GoodB2GSink` / `Action` dispatches account for a further 840 rows and are
|
|
19530
|
+
// left alone, because "the callee is defined in this file" is a different
|
|
19531
|
+
// claim needing its own change.
|
|
19532
|
+
"Add",
|
|
19533
|
+
"AddLast",
|
|
19534
|
+
"AddFirst",
|
|
19535
|
+
"AddRange",
|
|
19536
|
+
"TryAdd",
|
|
19537
|
+
"Insert",
|
|
19538
|
+
"Push",
|
|
19539
|
+
"Enqueue"
|
|
19540
|
+
]);
|
|
19541
|
+
function isCSharpNonEscapeCall(call) {
|
|
19542
|
+
const name2 = call.method_name;
|
|
19543
|
+
const recv = (call.receiver ?? "").split(".").pop() ?? "";
|
|
19544
|
+
if (CSHARP_PURE_STATIC_RECEIVERS.has(recv)) return true;
|
|
19545
|
+
if (CSHARP_NON_ESCAPE_METHODS.has(name2)) return true;
|
|
19546
|
+
return false;
|
|
19547
|
+
}
|
|
19348
19548
|
function isGoNonEscapeCall(call) {
|
|
19349
19549
|
const name2 = call.method_name;
|
|
19350
19550
|
const recv = (call.receiver ?? "").split(".").pop() ?? "";
|
|
@@ -19611,6 +19811,9 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
19611
19811
|
if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
|
|
19612
19812
|
continue;
|
|
19613
19813
|
}
|
|
19814
|
+
if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
|
|
19815
|
+
continue;
|
|
19816
|
+
}
|
|
19614
19817
|
const bashSafeBuiltins = /* @__PURE__ */ new Set([
|
|
19615
19818
|
"echo",
|
|
19616
19819
|
"printf",
|
|
@@ -27786,7 +27989,8 @@ var LanguageSourcesPass = class {
|
|
|
27786
27989
|
}
|
|
27787
27990
|
if (language === "csharp") {
|
|
27788
27991
|
additionalSanitizers.push(
|
|
27789
|
-
...findCSharpFullPathStartsWithGuardSanitizers(code)
|
|
27992
|
+
...findCSharpFullPathStartsWithGuardSanitizers(code),
|
|
27993
|
+
...findCSharpEarlyReturnGuardSanitizers(code, language)
|
|
27790
27994
|
);
|
|
27791
27995
|
}
|
|
27792
27996
|
if (language === "java") {
|
|
@@ -30902,6 +31106,66 @@ function guardRejects(lines, i2, terminatorRe, window2) {
|
|
|
30902
31106
|
}
|
|
30903
31107
|
return false;
|
|
30904
31108
|
}
|
|
31109
|
+
function findCSharpEarlyReturnGuardSanitizers(code, language) {
|
|
31110
|
+
if (language !== "csharp") return [];
|
|
31111
|
+
const sanitizers = [];
|
|
31112
|
+
const lines = code.split("\n");
|
|
31113
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
31114
|
+
const allowlistRes = [
|
|
31115
|
+
/\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
|
|
31116
|
+
/\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
|
|
31117
|
+
];
|
|
31118
|
+
const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
|
|
31119
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
31120
|
+
let guardedExpr = null;
|
|
31121
|
+
for (const re of allowlistRes) {
|
|
31122
|
+
const m = re.exec(lines[i2]);
|
|
31123
|
+
if (m) {
|
|
31124
|
+
guardedExpr = m[1];
|
|
31125
|
+
break;
|
|
31126
|
+
}
|
|
31127
|
+
}
|
|
31128
|
+
if (!guardedExpr) {
|
|
31129
|
+
const m = charClassRe.exec(lines[i2]);
|
|
31130
|
+
if (m) guardedExpr = m[1];
|
|
31131
|
+
}
|
|
31132
|
+
if (!guardedExpr) continue;
|
|
31133
|
+
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31134
|
+
const root = guardedExpr.split(".")[0];
|
|
31135
|
+
const guarded = /* @__PURE__ */ new Set([guardedExpr, root]);
|
|
31136
|
+
const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
|
|
31137
|
+
for (let b = i2 - 1; b >= 0; b--) {
|
|
31138
|
+
const a = assignRe.exec(lines[b]);
|
|
31139
|
+
if (!a || !guarded.has(a[1])) continue;
|
|
31140
|
+
for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
|
|
31141
|
+
}
|
|
31142
|
+
const mentions = (t) => {
|
|
31143
|
+
for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
|
|
31144
|
+
return false;
|
|
31145
|
+
};
|
|
31146
|
+
for (let l = i2 + 1; l < lines.length; l++) {
|
|
31147
|
+
const a = assignRe.exec(lines[l]);
|
|
31148
|
+
if (a && mentions(a[2])) guarded.add(a[1]);
|
|
31149
|
+
if (mentions(lines[l])) {
|
|
31150
|
+
sanitizers.push({
|
|
31151
|
+
type: "csharp_early_return_guard",
|
|
31152
|
+
method: "guard",
|
|
31153
|
+
line: l + 1,
|
|
31154
|
+
sanitizes: [
|
|
31155
|
+
"ssrf",
|
|
31156
|
+
"xpath_injection",
|
|
31157
|
+
"ldap_injection",
|
|
31158
|
+
"path_traversal",
|
|
31159
|
+
"command_injection",
|
|
31160
|
+
"sql_injection",
|
|
31161
|
+
"external_taint_escape"
|
|
31162
|
+
]
|
|
31163
|
+
});
|
|
31164
|
+
}
|
|
31165
|
+
}
|
|
31166
|
+
}
|
|
31167
|
+
return sanitizers;
|
|
31168
|
+
}
|
|
30905
31169
|
function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
30906
31170
|
const sanitizers = [];
|
|
30907
31171
|
const lines = code.split("\n");
|
|
@@ -37299,6 +37563,53 @@ var LibraryProfileXssGatePass = class {
|
|
|
37299
37563
|
};
|
|
37300
37564
|
|
|
37301
37565
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
37566
|
+
var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
|
|
37567
|
+
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
37568
|
+
const lines = typeof code === "string" ? code.split("\n") : [];
|
|
37569
|
+
const selfReassign = /* @__PURE__ */ new Map();
|
|
37570
|
+
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37571
|
+
for (const [line, defs] of graph.defsByLine) {
|
|
37572
|
+
const text = lines[line - 1];
|
|
37573
|
+
if (!text) continue;
|
|
37574
|
+
for (const d of defs) {
|
|
37575
|
+
if (d.kind !== "local" && d.kind !== "param") continue;
|
|
37576
|
+
const v = escape(d.variable);
|
|
37577
|
+
const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
|
|
37578
|
+
if (!m) continue;
|
|
37579
|
+
if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
|
|
37580
|
+
const arr = selfReassign.get(d.variable) ?? [];
|
|
37581
|
+
arr.push(line);
|
|
37582
|
+
selfReassign.set(d.variable, arr);
|
|
37583
|
+
}
|
|
37584
|
+
}
|
|
37585
|
+
}
|
|
37586
|
+
const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
|
|
37587
|
+
const defAt = (v, line) => {
|
|
37588
|
+
const own = (graph.defsByLine.get(line) ?? []).find((d) => d.variable === v);
|
|
37589
|
+
if (own) return own;
|
|
37590
|
+
const use = (graph.usesByLine.get(line) ?? []).find((u) => u.variable === v && u.def_id != null);
|
|
37591
|
+
return use ? graph.defById.get(use.def_id) : void 0;
|
|
37592
|
+
};
|
|
37593
|
+
return (step, sourceLine, sinkLine) => {
|
|
37594
|
+
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
37595
|
+
if (!r.isFalsePositive) return false;
|
|
37596
|
+
if (r.reason !== "variable_not_tainted") return true;
|
|
37597
|
+
const lo = Math.min(sourceLine, sinkLine);
|
|
37598
|
+
let d = defAt(step.variable, step.line);
|
|
37599
|
+
let selfHops = 0;
|
|
37600
|
+
for (let guard = 0; d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
|
|
37601
|
+
const here = d.id;
|
|
37602
|
+
const prior = (graph.usesByLine.get(d.line) ?? []).find((u) => u.variable === d.variable && u.def_id != null && u.def_id !== here);
|
|
37603
|
+
if (!prior) break;
|
|
37604
|
+
selfHops++;
|
|
37605
|
+
d = graph.defById.get(prior.def_id);
|
|
37606
|
+
}
|
|
37607
|
+
if (!d) return true;
|
|
37608
|
+
if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) return false;
|
|
37609
|
+
if (selfHops > 0 && d.line >= lo) return false;
|
|
37610
|
+
return true;
|
|
37611
|
+
};
|
|
37612
|
+
}
|
|
37302
37613
|
var TaintPropagationPass = class {
|
|
37303
37614
|
name = "taint-propagation";
|
|
37304
37615
|
category = "security";
|
|
@@ -37316,11 +37627,11 @@ var TaintPropagationPass = class {
|
|
|
37316
37627
|
return { flows: [] };
|
|
37317
37628
|
}
|
|
37318
37629
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37630
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
37319
37631
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37320
37632
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37321
37633
|
for (const step of flow.path) {
|
|
37322
|
-
|
|
37323
|
-
if (fpCheck.isFalsePositive) return false;
|
|
37634
|
+
if (isVetoed(step, flow.source.line, flow.sink.line)) return false;
|
|
37324
37635
|
}
|
|
37325
37636
|
if (isCorrelatedPredicateFP(constProp, flow)) return false;
|
|
37326
37637
|
return true;
|
|
@@ -37413,7 +37724,7 @@ var TaintPropagationPass = class {
|
|
|
37413
37724
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37414
37725
|
let isFP = false;
|
|
37415
37726
|
for (const step of f.path) {
|
|
37416
|
-
if (
|
|
37727
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37417
37728
|
isFP = true;
|
|
37418
37729
|
break;
|
|
37419
37730
|
}
|
|
@@ -37436,7 +37747,7 @@ var TaintPropagationPass = class {
|
|
|
37436
37747
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37437
37748
|
let isFP = false;
|
|
37438
37749
|
for (const step of f.path) {
|
|
37439
|
-
if (
|
|
37750
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37440
37751
|
isFP = true;
|
|
37441
37752
|
break;
|
|
37442
37753
|
}
|
|
@@ -40009,6 +40320,7 @@ var VariableShadowingPass = class {
|
|
|
40009
40320
|
const outerEntry = declLocals[0];
|
|
40010
40321
|
for (let i2 = 1; i2 < numDeclLocals; i2++) {
|
|
40011
40322
|
const inner = declLocals[i2];
|
|
40323
|
+
if (inner.def.line <= outerEntry.def.line) continue;
|
|
40012
40324
|
if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line)) continue;
|
|
40013
40325
|
const key = `${variable}-${inner.def.line}`;
|
|
40014
40326
|
if (reported.has(key)) continue;
|
|
@@ -6230,6 +6230,31 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6230
6230
|
});
|
|
6231
6231
|
continue;
|
|
6232
6232
|
}
|
|
6233
|
+
if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
|
|
6234
|
+
const creation = asn.parent.parent;
|
|
6235
|
+
if (creation?.type !== "object_creation_expression") continue;
|
|
6236
|
+
const ctorType = creation.childForFieldName("type");
|
|
6237
|
+
const right2 = asn.childForFieldName("right");
|
|
6238
|
+
if (!ctorType || !right2) continue;
|
|
6239
|
+
const typeName = csharpBareName(ctorType);
|
|
6240
|
+
const rhsText2 = getNodeText(right2);
|
|
6241
|
+
calls.push({
|
|
6242
|
+
method_name: getNodeText(left),
|
|
6243
|
+
receiver: typeName,
|
|
6244
|
+
receiver_type: typeName,
|
|
6245
|
+
receiver_type_fqn: null,
|
|
6246
|
+
arguments: [{
|
|
6247
|
+
position: 0,
|
|
6248
|
+
expression: rhsText2,
|
|
6249
|
+
variable: right2.type === "identifier" ? rhsText2 : null,
|
|
6250
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
|
|
6251
|
+
value: null
|
|
6252
|
+
}],
|
|
6253
|
+
location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
|
|
6254
|
+
in_method: findEnclosingMethod(asn)
|
|
6255
|
+
});
|
|
6256
|
+
continue;
|
|
6257
|
+
}
|
|
6233
6258
|
if (left?.type !== "member_access_expression") continue;
|
|
6234
6259
|
const nameNode = left.childForFieldName("name");
|
|
6235
6260
|
if (!nameNode) continue;
|
|
@@ -6239,6 +6264,13 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6239
6264
|
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6240
6265
|
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6241
6266
|
if (recvType !== "DirectorySearcher") continue;
|
|
6267
|
+
} else if (propName === "StatusDescription") {
|
|
6268
|
+
const recv = exprNode ? getNodeText(exprNode) : "";
|
|
6269
|
+
if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
|
|
6270
|
+
} else if (propName === "BaseAddress") {
|
|
6271
|
+
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6272
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6273
|
+
if (recvType !== "HttpClient") continue;
|
|
6242
6274
|
} else if (propName !== "CommandText") {
|
|
6243
6275
|
continue;
|
|
6244
6276
|
}
|
|
@@ -6261,8 +6293,34 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6261
6293
|
in_method: findEnclosingMethod(asn)
|
|
6262
6294
|
});
|
|
6263
6295
|
}
|
|
6296
|
+
const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
|
|
6297
|
+
for (const cast of casts) {
|
|
6298
|
+
const typeNode = cast.childForFieldName("type");
|
|
6299
|
+
const valueNode = cast.childForFieldName("value");
|
|
6300
|
+
if (!typeNode || !valueNode) continue;
|
|
6301
|
+
const typeName = csharpBareName(typeNode);
|
|
6302
|
+
if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
|
|
6303
|
+
const valueText = getNodeText(valueNode);
|
|
6304
|
+
calls.push({
|
|
6305
|
+
method_name: typeName,
|
|
6306
|
+
receiver: null,
|
|
6307
|
+
receiver_type: null,
|
|
6308
|
+
receiver_type_fqn: null,
|
|
6309
|
+
arguments: [{
|
|
6310
|
+
position: 0,
|
|
6311
|
+
expression: valueText,
|
|
6312
|
+
variable: valueNode.type === "identifier" ? valueText : null,
|
|
6313
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
|
|
6314
|
+
value: null
|
|
6315
|
+
}],
|
|
6316
|
+
location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
|
|
6317
|
+
in_method: findEnclosingMethod(cast),
|
|
6318
|
+
is_constructor: true
|
|
6319
|
+
});
|
|
6320
|
+
}
|
|
6264
6321
|
return calls;
|
|
6265
6322
|
}
|
|
6323
|
+
var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
|
|
6266
6324
|
function extractCSharpArguments(argsNode) {
|
|
6267
6325
|
const args2 = [];
|
|
6268
6326
|
let position = 0;
|
|
@@ -6282,12 +6340,22 @@ function extractCSharpArguments(argsNode) {
|
|
|
6282
6340
|
position: position++,
|
|
6283
6341
|
expression: text,
|
|
6284
6342
|
variable: expr?.type === "identifier" ? text : null,
|
|
6285
|
-
literal: expr
|
|
6343
|
+
literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
|
|
6286
6344
|
value: null
|
|
6287
6345
|
});
|
|
6288
6346
|
}
|
|
6289
6347
|
return args2;
|
|
6290
6348
|
}
|
|
6349
|
+
var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6350
|
+
"string_literal",
|
|
6351
|
+
"verbatim_string_literal",
|
|
6352
|
+
"raw_string_literal",
|
|
6353
|
+
"character_literal",
|
|
6354
|
+
"integer_literal",
|
|
6355
|
+
"real_literal",
|
|
6356
|
+
"boolean_literal",
|
|
6357
|
+
"null_literal"
|
|
6358
|
+
]);
|
|
6291
6359
|
function extractJavaScriptCalls(tree, cache) {
|
|
6292
6360
|
const calls = [];
|
|
6293
6361
|
const context = buildJSResolutionContext(tree, cache);
|
|
@@ -13311,9 +13379,14 @@ var DEFAULT_SINKS = [
|
|
|
13311
13379
|
{ method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
|
|
13312
13380
|
{ method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13313
13381
|
{ method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13314
|
-
//
|
|
13315
|
-
|
|
13316
|
-
|
|
13382
|
+
// #294/#484: no classless `from_str` / `from_slice` deserialization sink.
|
|
13383
|
+
// Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
|
|
13384
|
+
// `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
|
|
13385
|
+
// parsing, which cannot instantiate an attacker-chosen type. Removing them
|
|
13386
|
+
// was previously blocked because the class-scoped rows below could not match
|
|
13387
|
+
// the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
|
|
13388
|
+
// `::` path-separator fix in taint-matcher.ts makes them match, so the
|
|
13389
|
+
// classless rows are no longer load-bearing.
|
|
13317
13390
|
// Rust XSS (actix-web, rocket, axum response body)
|
|
13318
13391
|
{ method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
13319
13392
|
{ method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
@@ -13719,6 +13792,10 @@ var DEFAULT_SINKS = [
|
|
|
13719
13792
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
13720
13793
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
13721
13794
|
{ method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13795
|
+
// #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
|
|
13796
|
+
// attacker text into the HTTP status line (CWE-81). Surfaced by the
|
|
13797
|
+
// property-assignment synthesiser in the C# extractor, receiver-gated there.
|
|
13798
|
+
{ method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
|
|
13722
13799
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
13723
13800
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
13724
13801
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -13733,6 +13810,12 @@ var DEFAULT_SINKS = [
|
|
|
13733
13810
|
// filter is the constructor argument.
|
|
13734
13811
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13735
13812
|
{ method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13813
|
+
// #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
|
|
13814
|
+
// host and port of every RELATIVE request made on that client, so the
|
|
13815
|
+
// request arguments can all be constant and the destination still be
|
|
13816
|
+
// attacker-chosen. Surfaced by the property-assignment synthesiser, which
|
|
13817
|
+
// class-gates it to a resolved `HttpClient`.
|
|
13818
|
+
{ method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13736
13819
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
13737
13820
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
13738
13821
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16077,8 +16160,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16077
16160
|
return false;
|
|
16078
16161
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16079
16162
|
const target = call.resolution?.target;
|
|
16163
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16080
16164
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16081
|
-
if (
|
|
16165
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16082
16166
|
} else {
|
|
16083
16167
|
return false;
|
|
16084
16168
|
}
|