circle-ir 4.9.26 → 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 +23 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +154 -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 +156 -4
- 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/analyzer.d.ts +36 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +30 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +360 -13
- package/dist/core/circle-ir-core.cjs +94 -5
- package/dist/core/circle-ir-core.js +94 -5
- package/dist/core/extractors/calls.js +127 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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] },
|
|
@@ -14291,10 +14364,19 @@ var DEFAULT_SINKS = [
|
|
|
14291
14364
|
// C# XSS — raw HTML output (CWE-79).
|
|
14292
14365
|
{ method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14293
14366
|
{ method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14367
|
+
// ASP.NET Core writes the response body through the `HttpResponseWritingExtensions`
|
|
14368
|
+
// extension methods rather than `Response.Write`, so the System.Web row above does
|
|
14369
|
+
// not cover it. Receiver-scoped to `Response` so the many unrelated `WriteAsync`
|
|
14370
|
+
// overloads (Stream, StreamWriter, TextWriter, PipeWriter) are untouched. (#503)
|
|
14371
|
+
{ method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14294
14372
|
{ method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14295
14373
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
14296
14374
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
14297
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"] },
|
|
14298
14380
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
14299
14381
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
14300
14382
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -14309,6 +14391,12 @@ var DEFAULT_SINKS = [
|
|
|
14309
14391
|
// filter is the constructor argument.
|
|
14310
14392
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14311
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"] },
|
|
14312
14400
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
14313
14401
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
14314
14402
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16772,8 +16860,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16772
16860
|
return false;
|
|
16773
16861
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16774
16862
|
const target = call.resolution?.target;
|
|
16863
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16775
16864
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16776
|
-
if (
|
|
16865
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16777
16866
|
} else {
|
|
16778
16867
|
return false;
|
|
16779
16868
|
}
|
|
@@ -19340,6 +19429,122 @@ var GO_PURE_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
19340
19429
|
"hex",
|
|
19341
19430
|
"base64"
|
|
19342
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
|
+
}
|
|
19343
19548
|
function isGoNonEscapeCall(call) {
|
|
19344
19549
|
const name2 = call.method_name;
|
|
19345
19550
|
const recv = (call.receiver ?? "").split(".").pop() ?? "";
|
|
@@ -19606,6 +19811,9 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
19606
19811
|
if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
|
|
19607
19812
|
continue;
|
|
19608
19813
|
}
|
|
19814
|
+
if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
|
|
19815
|
+
continue;
|
|
19816
|
+
}
|
|
19609
19817
|
const bashSafeBuiltins = /* @__PURE__ */ new Set([
|
|
19610
19818
|
"echo",
|
|
19611
19819
|
"printf",
|
|
@@ -27781,7 +27989,8 @@ var LanguageSourcesPass = class {
|
|
|
27781
27989
|
}
|
|
27782
27990
|
if (language === "csharp") {
|
|
27783
27991
|
additionalSanitizers.push(
|
|
27784
|
-
...findCSharpFullPathStartsWithGuardSanitizers(code)
|
|
27992
|
+
...findCSharpFullPathStartsWithGuardSanitizers(code),
|
|
27993
|
+
...findCSharpEarlyReturnGuardSanitizers(code, language)
|
|
27785
27994
|
);
|
|
27786
27995
|
}
|
|
27787
27996
|
if (language === "java") {
|
|
@@ -28432,17 +28641,23 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28432
28641
|
const sources = [];
|
|
28433
28642
|
const lines = sourceCode.split("\n");
|
|
28434
28643
|
const assignRe = /^\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
28435
|
-
const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
|
|
28436
28644
|
const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
|
|
28437
28645
|
const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
|
|
28438
28646
|
const formFileParams = /* @__PURE__ */ new Set();
|
|
28439
28647
|
const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
|
|
28648
|
+
const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
|
|
28649
|
+
const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
|
|
28440
28650
|
for (const line of lines) {
|
|
28441
|
-
const
|
|
28651
|
+
const formRe = new RegExp(formFileDeclRe.source, "g");
|
|
28442
28652
|
let d;
|
|
28443
|
-
while ((d =
|
|
28653
|
+
while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
|
|
28654
|
+
const reqRe = new RegExp(httpRequestDeclRe.source, "g");
|
|
28655
|
+
while ((d = reqRe.exec(line)) !== null) httpRequestReceivers.add(d[1]);
|
|
28444
28656
|
}
|
|
28445
28657
|
const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
|
|
28658
|
+
const requestReadRe = new RegExp(
|
|
28659
|
+
`\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
|
|
28660
|
+
);
|
|
28446
28661
|
const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : null;
|
|
28447
28662
|
const inlineReads = [
|
|
28448
28663
|
[requestReadRe, "http_param"],
|
|
@@ -28466,10 +28681,34 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28466
28681
|
};
|
|
28467
28682
|
const exprStmtCallRe = /^\s*(?:await\s+)?[A-Za-z_][\w.]*(?:<[^>]*>)?\s*\(.*\)\s*;\s*$/;
|
|
28468
28683
|
const nonCallKeywordRe = /^\s*(?:await\s+)?(?:if|while|for|foreach|switch|using|lock|return|throw|catch|when|nameof|typeof|sizeof|default)\b/;
|
|
28684
|
+
const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
|
|
28469
28685
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28470
28686
|
const m = assignRe.exec(lines[i2]);
|
|
28471
28687
|
if (!m) {
|
|
28472
28688
|
const stmt = lines[i2];
|
|
28689
|
+
if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
28690
|
+
const emb = new RegExp(embeddedAssignRe.source, "g");
|
|
28691
|
+
let em;
|
|
28692
|
+
let seeded = false;
|
|
28693
|
+
while ((em = emb.exec(stmt)) !== null) {
|
|
28694
|
+
const varName2 = em[1];
|
|
28695
|
+
const rhs2 = em[2];
|
|
28696
|
+
const type3 = classify(rhs2);
|
|
28697
|
+
if (!type3) continue;
|
|
28698
|
+
const lineNumber3 = i2 + 1;
|
|
28699
|
+
if (sources.some((s) => s.line === lineNumber3 && s.variable === varName2)) continue;
|
|
28700
|
+
sources.push({
|
|
28701
|
+
type: type3,
|
|
28702
|
+
location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
|
|
28703
|
+
severity: "high",
|
|
28704
|
+
line: lineNumber3,
|
|
28705
|
+
confidence: 1,
|
|
28706
|
+
variable: varName2
|
|
28707
|
+
});
|
|
28708
|
+
seeded = true;
|
|
28709
|
+
}
|
|
28710
|
+
if (seeded) continue;
|
|
28711
|
+
}
|
|
28473
28712
|
if (!exprStmtCallRe.test(stmt) || nonCallKeywordRe.test(stmt)) continue;
|
|
28474
28713
|
const hit = inlineRead(stmt);
|
|
28475
28714
|
if (!hit) continue;
|
|
@@ -30867,6 +31106,66 @@ function guardRejects(lines, i2, terminatorRe, window2) {
|
|
|
30867
31106
|
}
|
|
30868
31107
|
return false;
|
|
30869
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
|
+
}
|
|
30870
31169
|
function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
30871
31170
|
const sanitizers = [];
|
|
30872
31171
|
const lines = code.split("\n");
|
|
@@ -37264,6 +37563,53 @@ var LibraryProfileXssGatePass = class {
|
|
|
37264
37563
|
};
|
|
37265
37564
|
|
|
37266
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
|
+
}
|
|
37267
37613
|
var TaintPropagationPass = class {
|
|
37268
37614
|
name = "taint-propagation";
|
|
37269
37615
|
category = "security";
|
|
@@ -37281,11 +37627,11 @@ var TaintPropagationPass = class {
|
|
|
37281
37627
|
return { flows: [] };
|
|
37282
37628
|
}
|
|
37283
37629
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37630
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
37284
37631
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37285
37632
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37286
37633
|
for (const step of flow.path) {
|
|
37287
|
-
|
|
37288
|
-
if (fpCheck.isFalsePositive) return false;
|
|
37634
|
+
if (isVetoed(step, flow.source.line, flow.sink.line)) return false;
|
|
37289
37635
|
}
|
|
37290
37636
|
if (isCorrelatedPredicateFP(constProp, flow)) return false;
|
|
37291
37637
|
return true;
|
|
@@ -37378,7 +37724,7 @@ var TaintPropagationPass = class {
|
|
|
37378
37724
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37379
37725
|
let isFP = false;
|
|
37380
37726
|
for (const step of f.path) {
|
|
37381
|
-
if (
|
|
37727
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37382
37728
|
isFP = true;
|
|
37383
37729
|
break;
|
|
37384
37730
|
}
|
|
@@ -37401,7 +37747,7 @@ var TaintPropagationPass = class {
|
|
|
37401
37747
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37402
37748
|
let isFP = false;
|
|
37403
37749
|
for (const step of f.path) {
|
|
37404
|
-
if (
|
|
37750
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37405
37751
|
isFP = true;
|
|
37406
37752
|
break;
|
|
37407
37753
|
}
|
|
@@ -39974,6 +40320,7 @@ var VariableShadowingPass = class {
|
|
|
39974
40320
|
const outerEntry = declLocals[0];
|
|
39975
40321
|
for (let i2 = 1; i2 < numDeclLocals; i2++) {
|
|
39976
40322
|
const inner = declLocals[i2];
|
|
40323
|
+
if (inner.def.line <= outerEntry.def.line) continue;
|
|
39977
40324
|
if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line)) continue;
|
|
39978
40325
|
const key = `${variable}-${inner.def.line}`;
|
|
39979
40326
|
if (reported.has(key)) continue;
|