circle-ir 4.9.27 → 4.9.29
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 +58 -5
- 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 +162 -8
- 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 +214 -16
- 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 +62 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +486 -33
- package/dist/core/circle-ir-core.cjs +173 -13
- package/dist/core/circle-ir-core.js +173 -13
- package/dist/core/extractors/calls.js +155 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/types.js +50 -7
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/types/config.d.ts +14 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4434,6 +4434,21 @@ function extractTypes(tree, cache, language) {
|
|
|
4434
4434
|
}
|
|
4435
4435
|
return extractJavaTypes(tree, cache);
|
|
4436
4436
|
}
|
|
4437
|
+
function csharpMemberNodes(body2) {
|
|
4438
|
+
if (!body2) return [];
|
|
4439
|
+
const out2 = [];
|
|
4440
|
+
const visit = (parent, depth) => {
|
|
4441
|
+
if (depth > 32) return;
|
|
4442
|
+
for (let i2 = 0; i2 < parent.childCount; i2++) {
|
|
4443
|
+
const c = parent.child(i2);
|
|
4444
|
+
if (!c) continue;
|
|
4445
|
+
if (c.type.startsWith("preproc_")) visit(c, depth + 1);
|
|
4446
|
+
else out2.push(c);
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
visit(body2, 0);
|
|
4450
|
+
return out2;
|
|
4451
|
+
}
|
|
4437
4452
|
function extractCSharpTypes(tree, cache) {
|
|
4438
4453
|
const types = [];
|
|
4439
4454
|
const KINDS = {
|
|
@@ -4450,9 +4465,8 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4450
4465
|
const methods = [];
|
|
4451
4466
|
const fields = extractCSharpFields(body2);
|
|
4452
4467
|
if (body2) {
|
|
4453
|
-
for (
|
|
4454
|
-
|
|
4455
|
-
if (!m || m.type !== "method_declaration") continue;
|
|
4468
|
+
for (const m of csharpMemberNodes(body2)) {
|
|
4469
|
+
if (m.type !== "method_declaration") continue;
|
|
4456
4470
|
const mName = m.childForFieldName("name");
|
|
4457
4471
|
const paramList = m.childForFieldName("parameters");
|
|
4458
4472
|
const parameters = [];
|
|
@@ -4503,9 +4517,7 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4503
4517
|
function extractCSharpFields(body2) {
|
|
4504
4518
|
const fields = [];
|
|
4505
4519
|
if (!body2) return fields;
|
|
4506
|
-
for (
|
|
4507
|
-
const c = body2.child(i2);
|
|
4508
|
-
if (!c) continue;
|
|
4520
|
+
for (const c of csharpMemberNodes(body2)) {
|
|
4509
4521
|
if (c.type === "field_declaration") {
|
|
4510
4522
|
let varDecl = null;
|
|
4511
4523
|
for (let k = 0; k < c.childCount; k++) {
|
|
@@ -6039,6 +6051,13 @@ function extractCalls(tree, cache, language) {
|
|
|
6039
6051
|
function buildCSharpReceiverTypeMap(tree, cache) {
|
|
6040
6052
|
const map = /* @__PURE__ */ new Map();
|
|
6041
6053
|
const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
|
|
6054
|
+
for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
|
|
6055
|
+
const nameNode = prm.childForFieldName("name");
|
|
6056
|
+
const typeNode = prm.childForFieldName("type");
|
|
6057
|
+
if (nameNode?.type !== "identifier" || !typeNode) continue;
|
|
6058
|
+
const t = getNodeText(typeNode);
|
|
6059
|
+
if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
|
|
6060
|
+
}
|
|
6042
6061
|
for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
|
|
6043
6062
|
const typeNode = vd.childForFieldName("type");
|
|
6044
6063
|
const declaredType = typeNode ? getNodeText(typeNode) : null;
|
|
@@ -6068,6 +6087,7 @@ function findFirstDescendant(node, type) {
|
|
|
6068
6087
|
}
|
|
6069
6088
|
return null;
|
|
6070
6089
|
}
|
|
6090
|
+
var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
|
|
6071
6091
|
var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
|
|
6072
6092
|
"ExecuteScalar",
|
|
6073
6093
|
"ExecuteReader",
|
|
@@ -6184,6 +6204,31 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6184
6204
|
});
|
|
6185
6205
|
continue;
|
|
6186
6206
|
}
|
|
6207
|
+
if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
|
|
6208
|
+
const creation = asn.parent.parent;
|
|
6209
|
+
if (creation?.type !== "object_creation_expression") continue;
|
|
6210
|
+
const ctorType = creation.childForFieldName("type");
|
|
6211
|
+
const right2 = asn.childForFieldName("right");
|
|
6212
|
+
if (!ctorType || !right2) continue;
|
|
6213
|
+
const typeName = csharpBareName(ctorType);
|
|
6214
|
+
const rhsText2 = getNodeText(right2);
|
|
6215
|
+
calls.push({
|
|
6216
|
+
method_name: getNodeText(left),
|
|
6217
|
+
receiver: typeName,
|
|
6218
|
+
receiver_type: typeName,
|
|
6219
|
+
receiver_type_fqn: null,
|
|
6220
|
+
arguments: [{
|
|
6221
|
+
position: 0,
|
|
6222
|
+
expression: rhsText2,
|
|
6223
|
+
variable: right2.type === "identifier" ? rhsText2 : null,
|
|
6224
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
|
|
6225
|
+
value: null
|
|
6226
|
+
}],
|
|
6227
|
+
location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
|
|
6228
|
+
in_method: findEnclosingMethod(asn)
|
|
6229
|
+
});
|
|
6230
|
+
continue;
|
|
6231
|
+
}
|
|
6187
6232
|
if (left?.type !== "member_access_expression") continue;
|
|
6188
6233
|
const nameNode = left.childForFieldName("name");
|
|
6189
6234
|
if (!nameNode) continue;
|
|
@@ -6193,6 +6238,14 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6193
6238
|
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6194
6239
|
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6195
6240
|
if (recvType !== "DirectorySearcher") continue;
|
|
6241
|
+
} else if (propName === "StatusDescription") {
|
|
6242
|
+
const recv = exprNode ? getNodeText(exprNode) : "";
|
|
6243
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6244
|
+
if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
|
|
6245
|
+
} else if (propName === "BaseAddress") {
|
|
6246
|
+
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6247
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6248
|
+
if (recvType !== "HttpClient") continue;
|
|
6196
6249
|
} else if (propName !== "CommandText") {
|
|
6197
6250
|
continue;
|
|
6198
6251
|
}
|
|
@@ -6215,8 +6268,34 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6215
6268
|
in_method: findEnclosingMethod(asn)
|
|
6216
6269
|
});
|
|
6217
6270
|
}
|
|
6271
|
+
const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
|
|
6272
|
+
for (const cast of casts) {
|
|
6273
|
+
const typeNode = cast.childForFieldName("type");
|
|
6274
|
+
const valueNode = cast.childForFieldName("value");
|
|
6275
|
+
if (!typeNode || !valueNode) continue;
|
|
6276
|
+
const typeName = csharpBareName(typeNode);
|
|
6277
|
+
if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
|
|
6278
|
+
const valueText = getNodeText(valueNode);
|
|
6279
|
+
calls.push({
|
|
6280
|
+
method_name: typeName,
|
|
6281
|
+
receiver: null,
|
|
6282
|
+
receiver_type: null,
|
|
6283
|
+
receiver_type_fqn: null,
|
|
6284
|
+
arguments: [{
|
|
6285
|
+
position: 0,
|
|
6286
|
+
expression: valueText,
|
|
6287
|
+
variable: valueNode.type === "identifier" ? valueText : null,
|
|
6288
|
+
literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
|
|
6289
|
+
value: null
|
|
6290
|
+
}],
|
|
6291
|
+
location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
|
|
6292
|
+
in_method: findEnclosingMethod(cast),
|
|
6293
|
+
is_constructor: true
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6218
6296
|
return calls;
|
|
6219
6297
|
}
|
|
6298
|
+
var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
|
|
6220
6299
|
function extractCSharpArguments(argsNode) {
|
|
6221
6300
|
const args2 = [];
|
|
6222
6301
|
let position = 0;
|
|
@@ -6236,12 +6315,22 @@ function extractCSharpArguments(argsNode) {
|
|
|
6236
6315
|
position: position++,
|
|
6237
6316
|
expression: text,
|
|
6238
6317
|
variable: expr?.type === "identifier" ? text : null,
|
|
6239
|
-
literal: expr
|
|
6318
|
+
literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
|
|
6240
6319
|
value: null
|
|
6241
6320
|
});
|
|
6242
6321
|
}
|
|
6243
6322
|
return args2;
|
|
6244
6323
|
}
|
|
6324
|
+
var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6325
|
+
"string_literal",
|
|
6326
|
+
"verbatim_string_literal",
|
|
6327
|
+
"raw_string_literal",
|
|
6328
|
+
"character_literal",
|
|
6329
|
+
"integer_literal",
|
|
6330
|
+
"real_literal",
|
|
6331
|
+
"boolean_literal",
|
|
6332
|
+
"null_literal"
|
|
6333
|
+
]);
|
|
6245
6334
|
function extractJavaScriptCalls(tree, cache) {
|
|
6246
6335
|
const calls = [];
|
|
6247
6336
|
const context = buildJSResolutionContext(tree, cache);
|
|
@@ -11652,6 +11741,23 @@ var DEFAULT_SOURCES = [
|
|
|
11652
11741
|
{ method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11653
11742
|
{ method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11654
11743
|
// Database result sources
|
|
11744
|
+
// C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
|
|
11745
|
+
// rows above one-for-one, including the severity split: console/file I/O is
|
|
11746
|
+
// `io_input` at high, a database row read is `db_input` at medium. There is
|
|
11747
|
+
// no separate confidence tier — `type` + `severity` IS the tier, exactly as
|
|
11748
|
+
// for `BufferedReader.readLine` vs `ResultSet.getString`.
|
|
11749
|
+
{ method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11750
|
+
{ method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11751
|
+
{ method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11752
|
+
{ method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11753
|
+
// Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
|
|
11754
|
+
// `OpenRead` return the channel; the read off it is covered by the
|
|
11755
|
+
// StreamReader rows above, and seeding the channel too lets a flow form when
|
|
11756
|
+
// the read is inlined.
|
|
11757
|
+
{ method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11758
|
+
{ method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11759
|
+
{ method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11760
|
+
{ method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11655
11761
|
{ method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11656
11762
|
{ method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11657
11763
|
{ method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
@@ -13892,9 +13998,14 @@ var DEFAULT_SINKS = [
|
|
|
13892
13998
|
{ method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
|
|
13893
13999
|
{ method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13894
14000
|
{ method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
|
|
13895
|
-
//
|
|
13896
|
-
|
|
13897
|
-
|
|
14001
|
+
// #294/#484: no classless `from_str` / `from_slice` deserialization sink.
|
|
14002
|
+
// Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
|
|
14003
|
+
// `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
|
|
14004
|
+
// parsing, which cannot instantiate an attacker-chosen type. Removing them
|
|
14005
|
+
// was previously blocked because the class-scoped rows below could not match
|
|
14006
|
+
// the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
|
|
14007
|
+
// `::` path-separator fix in taint-matcher.ts makes them match, so the
|
|
14008
|
+
// classless rows are no longer load-bearing.
|
|
13898
14009
|
// Rust XSS (actix-web, rocket, axum response body)
|
|
13899
14010
|
{ method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
13900
14011
|
{ method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
@@ -14211,8 +14322,12 @@ var DEFAULT_SINKS = [
|
|
|
14211
14322
|
{ method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14212
14323
|
{ method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14213
14324
|
{ method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14214
|
-
|
|
14215
|
-
|
|
14325
|
+
// #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
|
|
14326
|
+
// is not CWE-22. The `(string path)` overload still matches.
|
|
14327
|
+
{ method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
|
|
14328
|
+
// #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
|
|
14329
|
+
// is not CWE-22. The `(string path)` overload still matches.
|
|
14330
|
+
{ method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
|
|
14216
14331
|
// Remaining System.IO.File path APIs — class-scoped so they don't collide
|
|
14217
14332
|
// with unrelated methods. Copy/Move take a source AND destination path (both
|
|
14218
14333
|
// attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
|
|
@@ -14256,6 +14371,23 @@ var DEFAULT_SINKS = [
|
|
|
14256
14371
|
// `new RestClient(userUrl)` points every later request at attacker-controlled
|
|
14257
14372
|
// infrastructure. Ctor arg 0, taint-gated.
|
|
14258
14373
|
{ method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14374
|
+
// C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
|
|
14375
|
+
// part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
|
|
14376
|
+
// and can then execute attacker-supplied C#, so the source argument is the
|
|
14377
|
+
// injection point. The signature is
|
|
14378
|
+
// `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
|
|
14379
|
+
// so the sources begin at position 1 — position 0 is the options object.
|
|
14380
|
+
//
|
|
14381
|
+
// These rows are only useful together with the #530 StringBuilder
|
|
14382
|
+
// propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
|
|
14383
|
+
// through `sb.Append(...)` and reads it back with `ToString()`, so before
|
|
14384
|
+
// that fix the sink registered but no taint ever reached it.
|
|
14385
|
+
{ method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
|
|
14386
|
+
{ method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
|
|
14387
|
+
// Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
|
|
14388
|
+
// `Create` compiles it; either is the point where attacker text becomes code.
|
|
14389
|
+
{ method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14390
|
+
{ method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14259
14391
|
// C# code injection — dynamic script/assembly loading (CWE-94).
|
|
14260
14392
|
{ method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14261
14393
|
{ method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
@@ -14300,6 +14432,10 @@ var DEFAULT_SINKS = [
|
|
|
14300
14432
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
14301
14433
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
14302
14434
|
{ method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14435
|
+
// #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
|
|
14436
|
+
// attacker text into the HTTP status line (CWE-81). Surfaced by the
|
|
14437
|
+
// property-assignment synthesiser in the C# extractor, receiver-gated there.
|
|
14438
|
+
{ method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
|
|
14303
14439
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
14304
14440
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
14305
14441
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -14314,6 +14450,12 @@ var DEFAULT_SINKS = [
|
|
|
14314
14450
|
// filter is the constructor argument.
|
|
14315
14451
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14316
14452
|
{ method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14453
|
+
// #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
|
|
14454
|
+
// host and port of every RELATIVE request made on that client, so the
|
|
14455
|
+
// request arguments can all be constant and the destination still be
|
|
14456
|
+
// attacker-chosen. Surfaced by the property-assignment synthesiser, which
|
|
14457
|
+
// class-gates it to a resolved `HttpClient`.
|
|
14458
|
+
{ method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14317
14459
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
14318
14460
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
14319
14461
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16146,6 +16288,20 @@ function escapeRe(s) {
|
|
|
16146
16288
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16147
16289
|
}
|
|
16148
16290
|
var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
|
|
16291
|
+
function argIsStreamExpression(call, position) {
|
|
16292
|
+
const arg = call.arguments.find((a) => a.position === position);
|
|
16293
|
+
if (!arg) return false;
|
|
16294
|
+
const expr = (arg.expression ?? "").trim();
|
|
16295
|
+
if (!expr) return false;
|
|
16296
|
+
return (
|
|
16297
|
+
// `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
|
|
16298
|
+
/^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
|
|
16299
|
+
/\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
|
|
16300
|
+
/\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
|
|
16301
|
+
/\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
|
|
16302
|
+
/^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
|
|
16303
|
+
);
|
|
16304
|
+
}
|
|
16149
16305
|
function argIsStringLiteral(call, position) {
|
|
16150
16306
|
const arg = call.arguments.find((a) => a.position === position);
|
|
16151
16307
|
if (!arg) return false;
|
|
@@ -16383,6 +16539,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
16383
16539
|
if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
|
|
16384
16540
|
continue;
|
|
16385
16541
|
}
|
|
16542
|
+
if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
|
|
16543
|
+
continue;
|
|
16544
|
+
}
|
|
16386
16545
|
if (pattern.type === "command_injection") {
|
|
16387
16546
|
if (call.is_constructor) {
|
|
16388
16547
|
if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
|
|
@@ -16777,8 +16936,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16777
16936
|
return false;
|
|
16778
16937
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16779
16938
|
const target = call.resolution?.target;
|
|
16939
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16780
16940
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16781
|
-
if (
|
|
16941
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16782
16942
|
} else {
|
|
16783
16943
|
return false;
|
|
16784
16944
|
}
|
|
@@ -19345,6 +19505,122 @@ var GO_PURE_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
19345
19505
|
"hex",
|
|
19346
19506
|
"base64"
|
|
19347
19507
|
]);
|
|
19508
|
+
var CSHARP_PURE_STATIC_RECEIVERS = /* @__PURE__ */ new Set([
|
|
19509
|
+
// formatting / conversion / parsing — return a derived value
|
|
19510
|
+
"string",
|
|
19511
|
+
"String",
|
|
19512
|
+
"Convert",
|
|
19513
|
+
"Encoding",
|
|
19514
|
+
"BitConverter",
|
|
19515
|
+
"Math",
|
|
19516
|
+
"Int16",
|
|
19517
|
+
"Int32",
|
|
19518
|
+
"Int64",
|
|
19519
|
+
"UInt16",
|
|
19520
|
+
"UInt32",
|
|
19521
|
+
"UInt64",
|
|
19522
|
+
"Double",
|
|
19523
|
+
"Single",
|
|
19524
|
+
"Decimal",
|
|
19525
|
+
"Boolean",
|
|
19526
|
+
"Byte",
|
|
19527
|
+
"SByte",
|
|
19528
|
+
"Char",
|
|
19529
|
+
"Guid",
|
|
19530
|
+
"DateTime",
|
|
19531
|
+
"DateTimeOffset",
|
|
19532
|
+
"TimeSpan",
|
|
19533
|
+
"Enum",
|
|
19534
|
+
"Array",
|
|
19535
|
+
"Buffer",
|
|
19536
|
+
"Nullable",
|
|
19537
|
+
"CultureInfo",
|
|
19538
|
+
"StringComparer",
|
|
19539
|
+
"Comparer",
|
|
19540
|
+
"EqualityComparer",
|
|
19541
|
+
// logging / diagnostics receivers
|
|
19542
|
+
"Console",
|
|
19543
|
+
"Debug",
|
|
19544
|
+
"Trace",
|
|
19545
|
+
"Logger",
|
|
19546
|
+
"logger",
|
|
19547
|
+
"_logger",
|
|
19548
|
+
"Log"
|
|
19549
|
+
]);
|
|
19550
|
+
var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
|
|
19551
|
+
// string / value derivation
|
|
19552
|
+
"ToString",
|
|
19553
|
+
"ToArray",
|
|
19554
|
+
"ToList",
|
|
19555
|
+
"ToCharArray",
|
|
19556
|
+
"GetHashCode",
|
|
19557
|
+
"Equals",
|
|
19558
|
+
"CompareTo",
|
|
19559
|
+
"Trim",
|
|
19560
|
+
"TrimStart",
|
|
19561
|
+
"TrimEnd",
|
|
19562
|
+
"Substring",
|
|
19563
|
+
"Split",
|
|
19564
|
+
"Join",
|
|
19565
|
+
"Concat",
|
|
19566
|
+
"PadLeft",
|
|
19567
|
+
"PadRight",
|
|
19568
|
+
"ToLower",
|
|
19569
|
+
"ToUpper",
|
|
19570
|
+
"ToLowerInvariant",
|
|
19571
|
+
"ToUpperInvariant",
|
|
19572
|
+
"StartsWith",
|
|
19573
|
+
"EndsWith",
|
|
19574
|
+
"Contains",
|
|
19575
|
+
"IndexOf",
|
|
19576
|
+
"LastIndexOf",
|
|
19577
|
+
"Insert",
|
|
19578
|
+
"Remove",
|
|
19579
|
+
"Normalize",
|
|
19580
|
+
"Append",
|
|
19581
|
+
"AppendLine",
|
|
19582
|
+
"AppendFormat",
|
|
19583
|
+
"TryParse",
|
|
19584
|
+
"Parse",
|
|
19585
|
+
// structured logging (Microsoft.Extensions.Logging, Serilog, NLog, log4net)
|
|
19586
|
+
"LogInformation",
|
|
19587
|
+
"LogWarning",
|
|
19588
|
+
"LogError",
|
|
19589
|
+
"LogDebug",
|
|
19590
|
+
"LogTrace",
|
|
19591
|
+
"LogCritical",
|
|
19592
|
+
"Information",
|
|
19593
|
+
"Warning",
|
|
19594
|
+
"Verbose",
|
|
19595
|
+
"Fatal",
|
|
19596
|
+
"BeginScope",
|
|
19597
|
+
// Collection stores. A value put into a container has not left the program —
|
|
19598
|
+
// the same reasoning that puts Go's `append` in GO_BUILTIN_CALLS. Measured
|
|
19599
|
+
// on the 10 scored Juliet C# families these were 1,296 of the 2,368 escape
|
|
19600
|
+
// rows remaining after the first pass of this gate (#474): `Add` 864,
|
|
19601
|
+
// `AddLast` 432.
|
|
19602
|
+
//
|
|
19603
|
+
// Project methods are deliberately NOT gated, matching the Go gate's rule
|
|
19604
|
+
// that an unknown call must still escape: Juliet's own `BadSink` /
|
|
19605
|
+
// `GoodB2GSink` / `Action` dispatches account for a further 840 rows and are
|
|
19606
|
+
// left alone, because "the callee is defined in this file" is a different
|
|
19607
|
+
// claim needing its own change.
|
|
19608
|
+
"Add",
|
|
19609
|
+
"AddLast",
|
|
19610
|
+
"AddFirst",
|
|
19611
|
+
"AddRange",
|
|
19612
|
+
"TryAdd",
|
|
19613
|
+
"Insert",
|
|
19614
|
+
"Push",
|
|
19615
|
+
"Enqueue"
|
|
19616
|
+
]);
|
|
19617
|
+
function isCSharpNonEscapeCall(call) {
|
|
19618
|
+
const name2 = call.method_name;
|
|
19619
|
+
const recv = (call.receiver ?? "").split(".").pop() ?? "";
|
|
19620
|
+
if (CSHARP_PURE_STATIC_RECEIVERS.has(recv)) return true;
|
|
19621
|
+
if (CSHARP_NON_ESCAPE_METHODS.has(name2)) return true;
|
|
19622
|
+
return false;
|
|
19623
|
+
}
|
|
19348
19624
|
function isGoNonEscapeCall(call) {
|
|
19349
19625
|
const name2 = call.method_name;
|
|
19350
19626
|
const recv = (call.receiver ?? "").split(".").pop() ?? "";
|
|
@@ -19611,6 +19887,9 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
19611
19887
|
if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
|
|
19612
19888
|
continue;
|
|
19613
19889
|
}
|
|
19890
|
+
if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
|
|
19891
|
+
continue;
|
|
19892
|
+
}
|
|
19614
19893
|
const bashSafeBuiltins = /* @__PURE__ */ new Set([
|
|
19615
19894
|
"echo",
|
|
19616
19895
|
"printf",
|
|
@@ -27786,7 +28065,8 @@ var LanguageSourcesPass = class {
|
|
|
27786
28065
|
}
|
|
27787
28066
|
if (language === "csharp") {
|
|
27788
28067
|
additionalSanitizers.push(
|
|
27789
|
-
...findCSharpFullPathStartsWithGuardSanitizers(code)
|
|
28068
|
+
...findCSharpFullPathStartsWithGuardSanitizers(code),
|
|
28069
|
+
...findCSharpEarlyReturnGuardSanitizers(code, language)
|
|
27790
28070
|
);
|
|
27791
28071
|
}
|
|
27792
28072
|
if (language === "java") {
|
|
@@ -28443,23 +28723,35 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28443
28723
|
const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
|
|
28444
28724
|
const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
|
|
28445
28725
|
const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
|
|
28726
|
+
const streamReaderVars = /* @__PURE__ */ new Set();
|
|
28727
|
+
const streamReaderDeclRe = /\b(?:StreamReader|TextReader|BinaryReader)\??\s+([A-Za-z_]\w*)/g;
|
|
28728
|
+
const dbReaderVars = /* @__PURE__ */ new Set();
|
|
28729
|
+
const dbReaderDeclRe = /\b(?:SqlDataReader|DbDataReader|IDataReader|OdbcDataReader|OleDbDataReader)\??\s+([A-Za-z_]\w*)/g;
|
|
28446
28730
|
for (const line of lines) {
|
|
28447
28731
|
const formRe = new RegExp(formFileDeclRe.source, "g");
|
|
28448
28732
|
let d;
|
|
28449
28733
|
while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
|
|
28450
28734
|
const reqRe = new RegExp(httpRequestDeclRe.source, "g");
|
|
28451
28735
|
while ((d = reqRe.exec(line)) !== null) httpRequestReceivers.add(d[1]);
|
|
28736
|
+
const srRe = new RegExp(streamReaderDeclRe.source, "g");
|
|
28737
|
+
while ((d = srRe.exec(line)) !== null) streamReaderVars.add(d[1]);
|
|
28738
|
+
const dbRe = new RegExp(dbReaderDeclRe.source, "g");
|
|
28739
|
+
while ((d = dbRe.exec(line)) !== null) dbReaderVars.add(d[1]);
|
|
28452
28740
|
}
|
|
28741
|
+
const streamReadRe = streamReaderVars.size > 0 ? new RegExp(`\\b(?:${[...streamReaderVars].join("|")})\\s*\\.\\s*(?:ReadLine|ReadToEnd|ReadLineAsync|ReadToEndAsync|ReadBlock|Read)\\s*\\(`) : null;
|
|
28742
|
+
const dbReadRe = dbReaderVars.size > 0 ? new RegExp(`\\b(?:${[...dbReaderVars].join("|")})\\s*\\.\\s*(?:GetString|GetValue|GetInt32|GetInt64|GetDecimal|GetDateTime|GetFieldValue|\\[)`) : null;
|
|
28453
28743
|
const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
|
|
28454
28744
|
const requestReadRe = new RegExp(
|
|
28455
28745
|
`\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
|
|
28456
28746
|
);
|
|
28457
|
-
const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : null;
|
|
28747
|
+
const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : streamReadRe?.test(text) ? "io_input" : dbReadRe?.test(text) ? "db_input" : null;
|
|
28458
28748
|
const inlineReads = [
|
|
28459
28749
|
[requestReadRe, "http_param"],
|
|
28460
28750
|
[formFileReadRe, "http_param"],
|
|
28461
28751
|
[consoleReadRe, "io_input"],
|
|
28462
|
-
[envReadRe, "env_input"]
|
|
28752
|
+
[envReadRe, "env_input"],
|
|
28753
|
+
[streamReadRe, "io_input"],
|
|
28754
|
+
[dbReadRe, "db_input"]
|
|
28463
28755
|
];
|
|
28464
28756
|
const inlineRead = (text) => {
|
|
28465
28757
|
const argsStart = text.indexOf("(");
|
|
@@ -28480,7 +28772,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28480
28772
|
const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
|
|
28481
28773
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28482
28774
|
const m = assignRe.exec(lines[i2]);
|
|
28483
|
-
|
|
28775
|
+
const rhsCrossesBlock = m !== null && m[2].includes("{");
|
|
28776
|
+
if (!m || rhsCrossesBlock) {
|
|
28484
28777
|
const stmt = lines[i2];
|
|
28485
28778
|
if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
28486
28779
|
const emb = new RegExp(embeddedAssignRe.source, "g");
|
|
@@ -28496,7 +28789,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28496
28789
|
sources.push({
|
|
28497
28790
|
type: type3,
|
|
28498
28791
|
location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
|
|
28499
|
-
|
|
28792
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28793
|
+
severity: type3 === "db_input" ? "medium" : "high",
|
|
28500
28794
|
line: lineNumber3,
|
|
28501
28795
|
confidence: 1,
|
|
28502
28796
|
variable: varName2
|
|
@@ -28515,7 +28809,7 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28515
28809
|
sources.push({
|
|
28516
28810
|
type: type2,
|
|
28517
28811
|
location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
|
|
28518
|
-
severity: "high",
|
|
28812
|
+
severity: type2 === "db_input" ? "medium" : "high",
|
|
28519
28813
|
line: lineNumber2,
|
|
28520
28814
|
confidence: 1,
|
|
28521
28815
|
variable: "<inline>",
|
|
@@ -28531,7 +28825,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28531
28825
|
sources.push({
|
|
28532
28826
|
type,
|
|
28533
28827
|
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
|
|
28534
|
-
|
|
28828
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28829
|
+
severity: type === "db_input" ? "medium" : "high",
|
|
28535
28830
|
line: lineNumber,
|
|
28536
28831
|
confidence: 1,
|
|
28537
28832
|
variable: varName
|
|
@@ -28698,7 +28993,8 @@ function findPythonAssignmentSources(sourceCode, language) {
|
|
|
28698
28993
|
sources.push({
|
|
28699
28994
|
type,
|
|
28700
28995
|
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
|
|
28701
|
-
|
|
28996
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28997
|
+
severity: type === "db_input" ? "medium" : "high",
|
|
28702
28998
|
line: lineNumber,
|
|
28703
28999
|
confidence: 0.95,
|
|
28704
29000
|
variable: varName
|
|
@@ -30902,6 +31198,66 @@ function guardRejects(lines, i2, terminatorRe, window2) {
|
|
|
30902
31198
|
}
|
|
30903
31199
|
return false;
|
|
30904
31200
|
}
|
|
31201
|
+
function findCSharpEarlyReturnGuardSanitizers(code, language) {
|
|
31202
|
+
if (language !== "csharp") return [];
|
|
31203
|
+
const sanitizers = [];
|
|
31204
|
+
const lines = code.split("\n");
|
|
31205
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
31206
|
+
const allowlistRes = [
|
|
31207
|
+
/\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
|
|
31208
|
+
/\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
|
|
31209
|
+
];
|
|
31210
|
+
const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
|
|
31211
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
31212
|
+
let guardedExpr = null;
|
|
31213
|
+
for (const re of allowlistRes) {
|
|
31214
|
+
const m = re.exec(lines[i2]);
|
|
31215
|
+
if (m) {
|
|
31216
|
+
guardedExpr = m[1];
|
|
31217
|
+
break;
|
|
31218
|
+
}
|
|
31219
|
+
}
|
|
31220
|
+
if (!guardedExpr) {
|
|
31221
|
+
const m = charClassRe.exec(lines[i2]);
|
|
31222
|
+
if (m) guardedExpr = m[1];
|
|
31223
|
+
}
|
|
31224
|
+
if (!guardedExpr) continue;
|
|
31225
|
+
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31226
|
+
const root = guardedExpr.split(".")[0];
|
|
31227
|
+
const guarded = /* @__PURE__ */ new Set([guardedExpr, root]);
|
|
31228
|
+
const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
|
|
31229
|
+
for (let b = i2 - 1; b >= 0; b--) {
|
|
31230
|
+
const a = assignRe.exec(lines[b]);
|
|
31231
|
+
if (!a || !guarded.has(a[1])) continue;
|
|
31232
|
+
for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
|
|
31233
|
+
}
|
|
31234
|
+
const mentions = (t) => {
|
|
31235
|
+
for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
|
|
31236
|
+
return false;
|
|
31237
|
+
};
|
|
31238
|
+
for (let l = i2 + 1; l < lines.length; l++) {
|
|
31239
|
+
const a = assignRe.exec(lines[l]);
|
|
31240
|
+
if (a && mentions(a[2])) guarded.add(a[1]);
|
|
31241
|
+
if (mentions(lines[l])) {
|
|
31242
|
+
sanitizers.push({
|
|
31243
|
+
type: "csharp_early_return_guard",
|
|
31244
|
+
method: "guard",
|
|
31245
|
+
line: l + 1,
|
|
31246
|
+
sanitizes: [
|
|
31247
|
+
"ssrf",
|
|
31248
|
+
"xpath_injection",
|
|
31249
|
+
"ldap_injection",
|
|
31250
|
+
"path_traversal",
|
|
31251
|
+
"command_injection",
|
|
31252
|
+
"sql_injection",
|
|
31253
|
+
"external_taint_escape"
|
|
31254
|
+
]
|
|
31255
|
+
});
|
|
31256
|
+
}
|
|
31257
|
+
}
|
|
31258
|
+
}
|
|
31259
|
+
return sanitizers;
|
|
31260
|
+
}
|
|
30905
31261
|
function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
30906
31262
|
const sanitizers = [];
|
|
30907
31263
|
const lines = code.split("\n");
|
|
@@ -37299,6 +37655,53 @@ var LibraryProfileXssGatePass = class {
|
|
|
37299
37655
|
};
|
|
37300
37656
|
|
|
37301
37657
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
37658
|
+
var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
|
|
37659
|
+
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
37660
|
+
const lines = typeof code === "string" ? code.split("\n") : [];
|
|
37661
|
+
const selfReassign = /* @__PURE__ */ new Map();
|
|
37662
|
+
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37663
|
+
for (const [line, defs] of graph.defsByLine) {
|
|
37664
|
+
const text = lines[line - 1];
|
|
37665
|
+
if (!text) continue;
|
|
37666
|
+
for (const d of defs) {
|
|
37667
|
+
if (d.kind !== "local" && d.kind !== "param") continue;
|
|
37668
|
+
const v = escape(d.variable);
|
|
37669
|
+
const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
|
|
37670
|
+
if (!m) continue;
|
|
37671
|
+
if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
|
|
37672
|
+
const arr = selfReassign.get(d.variable) ?? [];
|
|
37673
|
+
arr.push(line);
|
|
37674
|
+
selfReassign.set(d.variable, arr);
|
|
37675
|
+
}
|
|
37676
|
+
}
|
|
37677
|
+
}
|
|
37678
|
+
const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
|
|
37679
|
+
const defAt = (v, line) => {
|
|
37680
|
+
const own = (graph.defsByLine.get(line) ?? []).find((d) => d.variable === v);
|
|
37681
|
+
if (own) return own;
|
|
37682
|
+
const use = (graph.usesByLine.get(line) ?? []).find((u) => u.variable === v && u.def_id != null);
|
|
37683
|
+
return use ? graph.defById.get(use.def_id) : void 0;
|
|
37684
|
+
};
|
|
37685
|
+
return (step, sourceLine, sinkLine) => {
|
|
37686
|
+
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
37687
|
+
if (!r.isFalsePositive) return false;
|
|
37688
|
+
if (r.reason !== "variable_not_tainted") return true;
|
|
37689
|
+
const lo = Math.min(sourceLine, sinkLine);
|
|
37690
|
+
let d = defAt(step.variable, step.line);
|
|
37691
|
+
let selfHops = 0;
|
|
37692
|
+
for (let guard = 0; d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
|
|
37693
|
+
const here = d.id;
|
|
37694
|
+
const prior = (graph.usesByLine.get(d.line) ?? []).find((u) => u.variable === d.variable && u.def_id != null && u.def_id !== here);
|
|
37695
|
+
if (!prior) break;
|
|
37696
|
+
selfHops++;
|
|
37697
|
+
d = graph.defById.get(prior.def_id);
|
|
37698
|
+
}
|
|
37699
|
+
if (!d) return true;
|
|
37700
|
+
if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) return false;
|
|
37701
|
+
if (selfHops > 0 && d.line >= lo) return false;
|
|
37702
|
+
return true;
|
|
37703
|
+
};
|
|
37704
|
+
}
|
|
37302
37705
|
var TaintPropagationPass = class {
|
|
37303
37706
|
name = "taint-propagation";
|
|
37304
37707
|
category = "security";
|
|
@@ -37316,11 +37719,11 @@ var TaintPropagationPass = class {
|
|
|
37316
37719
|
return { flows: [] };
|
|
37317
37720
|
}
|
|
37318
37721
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37722
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
37319
37723
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37320
37724
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37321
37725
|
for (const step of flow.path) {
|
|
37322
|
-
|
|
37323
|
-
if (fpCheck.isFalsePositive) return false;
|
|
37726
|
+
if (isVetoed(step, flow.source.line, flow.sink.line)) return false;
|
|
37324
37727
|
}
|
|
37325
37728
|
if (isCorrelatedPredicateFP(constProp, flow)) return false;
|
|
37326
37729
|
return true;
|
|
@@ -37413,7 +37816,7 @@ var TaintPropagationPass = class {
|
|
|
37413
37816
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37414
37817
|
let isFP = false;
|
|
37415
37818
|
for (const step of f.path) {
|
|
37416
|
-
if (
|
|
37819
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37417
37820
|
isFP = true;
|
|
37418
37821
|
break;
|
|
37419
37822
|
}
|
|
@@ -37436,7 +37839,7 @@ var TaintPropagationPass = class {
|
|
|
37436
37839
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
37437
37840
|
let isFP = false;
|
|
37438
37841
|
for (const step of f.path) {
|
|
37439
|
-
if (
|
|
37842
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
37440
37843
|
isFP = true;
|
|
37441
37844
|
break;
|
|
37442
37845
|
}
|
|
@@ -38121,13 +38524,22 @@ function maskStringLiteralsForTokenScan(expr) {
|
|
|
38121
38524
|
}
|
|
38122
38525
|
return out2;
|
|
38123
38526
|
}
|
|
38527
|
+
function argsReferenceTaintedVar(expr, sources) {
|
|
38528
|
+
const codePart = expr.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
38529
|
+
return sources.some(
|
|
38530
|
+
(s) => new RegExp(
|
|
38531
|
+
`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`,
|
|
38532
|
+
"u"
|
|
38533
|
+
).test(codePart)
|
|
38534
|
+
);
|
|
38535
|
+
}
|
|
38124
38536
|
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
|
|
38125
38537
|
const flows = [];
|
|
38126
|
-
const
|
|
38127
|
-
const
|
|
38538
|
+
const methodRanges = (langs) => language && langs.includes(language) && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
|
|
38539
|
+
const enclosingStart = (ranges, line) => {
|
|
38128
38540
|
let best = -1;
|
|
38129
38541
|
let bestSpan = -1;
|
|
38130
|
-
for (const [start2, end] of
|
|
38542
|
+
for (const [start2, end] of ranges) {
|
|
38131
38543
|
if (line >= start2 && line <= end && end - start2 > bestSpan) {
|
|
38132
38544
|
best = start2;
|
|
38133
38545
|
bestSpan = end - start2;
|
|
@@ -38135,6 +38547,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38135
38547
|
}
|
|
38136
38548
|
return best;
|
|
38137
38549
|
};
|
|
38550
|
+
const goFnRanges = methodRanges(["go"]);
|
|
38551
|
+
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38552
|
+
const csFnRanges = methodRanges(["csharp"]);
|
|
38553
|
+
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
38138
38554
|
const sourcesWithVar = sources.filter(
|
|
38139
38555
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
38140
38556
|
);
|
|
@@ -38436,16 +38852,47 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38436
38852
|
if (!m) continue;
|
|
38437
38853
|
const [, obj, rhs] = m;
|
|
38438
38854
|
if (known.has(obj)) continue;
|
|
38439
|
-
|
|
38440
|
-
const rhsTainted = sourcesWithVar.some(
|
|
38441
|
-
(s) => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`, "u").test(codePart)
|
|
38442
|
-
);
|
|
38443
|
-
if (!rhsTainted) continue;
|
|
38855
|
+
if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
|
|
38444
38856
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
38445
38857
|
added = true;
|
|
38446
38858
|
}
|
|
38447
38859
|
}
|
|
38448
38860
|
}
|
|
38861
|
+
const builderSeeds = /* @__PURE__ */ new Set();
|
|
38862
|
+
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
38863
|
+
const lines = code.split("\n");
|
|
38864
|
+
const builders = /* @__PURE__ */ new Set();
|
|
38865
|
+
for (const line of lines) {
|
|
38866
|
+
for (const m of line.matchAll(/\bStringBuilder\s+([\p{L}_][\p{L}\p{N}_]*)/gu)) builders.add(m[1]);
|
|
38867
|
+
for (const m of line.matchAll(/\b([\p{L}_][\p{L}\p{N}_]*)\s*=\s*new\s+StringBuilder\b/gu)) builders.add(m[1]);
|
|
38868
|
+
}
|
|
38869
|
+
if (builders.size > 0) {
|
|
38870
|
+
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
38871
|
+
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
38872
|
+
let added = true;
|
|
38873
|
+
let guard = 0;
|
|
38874
|
+
while (added && guard < lines.length + 2) {
|
|
38875
|
+
added = false;
|
|
38876
|
+
guard++;
|
|
38877
|
+
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38878
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
38879
|
+
const m = appendRe.exec(lines[i2]);
|
|
38880
|
+
if (!m) continue;
|
|
38881
|
+
const [, obj, args2] = m;
|
|
38882
|
+
if (!builders.has(obj) || known.has(obj)) continue;
|
|
38883
|
+
const appendFn = csFnStart(i2 + 1);
|
|
38884
|
+
const reachable = sourcesWithVar.filter(
|
|
38885
|
+
(sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
|
|
38886
|
+
);
|
|
38887
|
+
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
38888
|
+
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
38889
|
+
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
38890
|
+
added = true;
|
|
38891
|
+
}
|
|
38892
|
+
}
|
|
38893
|
+
}
|
|
38894
|
+
}
|
|
38895
|
+
}
|
|
38449
38896
|
const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
|
|
38450
38897
|
const reCache = /* @__PURE__ */ new Map();
|
|
38451
38898
|
for (const s of sourcesWithVar) {
|
|
@@ -38508,6 +38955,11 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38508
38955
|
const sinkFn = goFnStart(sink.line);
|
|
38509
38956
|
if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn) continue;
|
|
38510
38957
|
}
|
|
38958
|
+
if (csFnRanges.length > 0 && builderSeeds.has(`${source.variable}@${source.line}`)) {
|
|
38959
|
+
const srcFn = csFnStart(source.line);
|
|
38960
|
+
const sinkFn = csFnStart(sink.line);
|
|
38961
|
+
if (srcFn !== sinkFn) continue;
|
|
38962
|
+
}
|
|
38511
38963
|
if (complex) {
|
|
38512
38964
|
const re = reCache.get(source.variable);
|
|
38513
38965
|
if (!re || !re.test(expr)) continue;
|
|
@@ -40009,6 +40461,7 @@ var VariableShadowingPass = class {
|
|
|
40009
40461
|
const outerEntry = declLocals[0];
|
|
40010
40462
|
for (let i2 = 1; i2 < numDeclLocals; i2++) {
|
|
40011
40463
|
const inner = declLocals[i2];
|
|
40464
|
+
if (inner.def.line <= outerEntry.def.line) continue;
|
|
40012
40465
|
if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line)) continue;
|
|
40013
40466
|
const key = `${variable}-${inner.def.line}`;
|
|
40014
40467
|
if (reported.has(key)) continue;
|