circle-ir 4.9.28 → 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 +40 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +54 -7
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +117 -12
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +41 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +165 -24
- package/dist/core/circle-ir-core.cjs +85 -9
- package/dist/core/circle-ir-core.js +85 -9
- package/dist/core/extractors/calls.js +29 -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",
|
|
@@ -6220,7 +6240,8 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6220
6240
|
if (recvType !== "DirectorySearcher") continue;
|
|
6221
6241
|
} else if (propName === "StatusDescription") {
|
|
6222
6242
|
const recv = exprNode ? getNodeText(exprNode) : "";
|
|
6223
|
-
|
|
6243
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6244
|
+
if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
|
|
6224
6245
|
} else if (propName === "BaseAddress") {
|
|
6225
6246
|
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6226
6247
|
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
@@ -11720,6 +11741,23 @@ var DEFAULT_SOURCES = [
|
|
|
11720
11741
|
{ method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11721
11742
|
{ method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11722
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"] },
|
|
11723
11761
|
{ method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11724
11762
|
{ method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11725
11763
|
{ method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
@@ -14284,8 +14322,12 @@ var DEFAULT_SINKS = [
|
|
|
14284
14322
|
{ method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14285
14323
|
{ method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14286
14324
|
{ method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
14287
|
-
|
|
14288
|
-
|
|
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"] },
|
|
14289
14331
|
// Remaining System.IO.File path APIs — class-scoped so they don't collide
|
|
14290
14332
|
// with unrelated methods. Copy/Move take a source AND destination path (both
|
|
14291
14333
|
// attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
|
|
@@ -14329,6 +14371,23 @@ var DEFAULT_SINKS = [
|
|
|
14329
14371
|
// `new RestClient(userUrl)` points every later request at attacker-controlled
|
|
14330
14372
|
// infrastructure. Ctor arg 0, taint-gated.
|
|
14331
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"] },
|
|
14332
14391
|
// C# code injection — dynamic script/assembly loading (CWE-94).
|
|
14333
14392
|
{ method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14334
14393
|
{ method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
@@ -16229,6 +16288,20 @@ function escapeRe(s) {
|
|
|
16229
16288
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16230
16289
|
}
|
|
16231
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
|
+
}
|
|
16232
16305
|
function argIsStringLiteral(call, position) {
|
|
16233
16306
|
const arg = call.arguments.find((a) => a.position === position);
|
|
16234
16307
|
if (!arg) return false;
|
|
@@ -16466,6 +16539,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
16466
16539
|
if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
|
|
16467
16540
|
continue;
|
|
16468
16541
|
}
|
|
16542
|
+
if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
|
|
16543
|
+
continue;
|
|
16544
|
+
}
|
|
16469
16545
|
if (pattern.type === "command_injection") {
|
|
16470
16546
|
if (call.is_constructor) {
|
|
16471
16547
|
if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
|
|
@@ -28647,23 +28723,35 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28647
28723
|
const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
|
|
28648
28724
|
const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
|
|
28649
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;
|
|
28650
28730
|
for (const line of lines) {
|
|
28651
28731
|
const formRe = new RegExp(formFileDeclRe.source, "g");
|
|
28652
28732
|
let d;
|
|
28653
28733
|
while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
|
|
28654
28734
|
const reqRe = new RegExp(httpRequestDeclRe.source, "g");
|
|
28655
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]);
|
|
28656
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;
|
|
28657
28743
|
const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
|
|
28658
28744
|
const requestReadRe = new RegExp(
|
|
28659
28745
|
`\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
|
|
28660
28746
|
);
|
|
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;
|
|
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;
|
|
28662
28748
|
const inlineReads = [
|
|
28663
28749
|
[requestReadRe, "http_param"],
|
|
28664
28750
|
[formFileReadRe, "http_param"],
|
|
28665
28751
|
[consoleReadRe, "io_input"],
|
|
28666
|
-
[envReadRe, "env_input"]
|
|
28752
|
+
[envReadRe, "env_input"],
|
|
28753
|
+
[streamReadRe, "io_input"],
|
|
28754
|
+
[dbReadRe, "db_input"]
|
|
28667
28755
|
];
|
|
28668
28756
|
const inlineRead = (text) => {
|
|
28669
28757
|
const argsStart = text.indexOf("(");
|
|
@@ -28684,7 +28772,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28684
28772
|
const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
|
|
28685
28773
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28686
28774
|
const m = assignRe.exec(lines[i2]);
|
|
28687
|
-
|
|
28775
|
+
const rhsCrossesBlock = m !== null && m[2].includes("{");
|
|
28776
|
+
if (!m || rhsCrossesBlock) {
|
|
28688
28777
|
const stmt = lines[i2];
|
|
28689
28778
|
if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
28690
28779
|
const emb = new RegExp(embeddedAssignRe.source, "g");
|
|
@@ -28700,7 +28789,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28700
28789
|
sources.push({
|
|
28701
28790
|
type: type3,
|
|
28702
28791
|
location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
|
|
28703
|
-
|
|
28792
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28793
|
+
severity: type3 === "db_input" ? "medium" : "high",
|
|
28704
28794
|
line: lineNumber3,
|
|
28705
28795
|
confidence: 1,
|
|
28706
28796
|
variable: varName2
|
|
@@ -28719,7 +28809,7 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28719
28809
|
sources.push({
|
|
28720
28810
|
type: type2,
|
|
28721
28811
|
location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
|
|
28722
|
-
severity: "high",
|
|
28812
|
+
severity: type2 === "db_input" ? "medium" : "high",
|
|
28723
28813
|
line: lineNumber2,
|
|
28724
28814
|
confidence: 1,
|
|
28725
28815
|
variable: "<inline>",
|
|
@@ -28735,7 +28825,8 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
28735
28825
|
sources.push({
|
|
28736
28826
|
type,
|
|
28737
28827
|
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
|
|
28738
|
-
|
|
28828
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28829
|
+
severity: type === "db_input" ? "medium" : "high",
|
|
28739
28830
|
line: lineNumber,
|
|
28740
28831
|
confidence: 1,
|
|
28741
28832
|
variable: varName
|
|
@@ -28902,7 +28993,8 @@ function findPythonAssignmentSources(sourceCode, language) {
|
|
|
28902
28993
|
sources.push({
|
|
28903
28994
|
type,
|
|
28904
28995
|
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
|
|
28905
|
-
|
|
28996
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
28997
|
+
severity: type === "db_input" ? "medium" : "high",
|
|
28906
28998
|
line: lineNumber,
|
|
28907
28999
|
confidence: 0.95,
|
|
28908
29000
|
variable: varName
|
|
@@ -38432,13 +38524,22 @@ function maskStringLiteralsForTokenScan(expr) {
|
|
|
38432
38524
|
}
|
|
38433
38525
|
return out2;
|
|
38434
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
|
+
}
|
|
38435
38536
|
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
|
|
38436
38537
|
const flows = [];
|
|
38437
|
-
const
|
|
38438
|
-
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) => {
|
|
38439
38540
|
let best = -1;
|
|
38440
38541
|
let bestSpan = -1;
|
|
38441
|
-
for (const [start2, end] of
|
|
38542
|
+
for (const [start2, end] of ranges) {
|
|
38442
38543
|
if (line >= start2 && line <= end && end - start2 > bestSpan) {
|
|
38443
38544
|
best = start2;
|
|
38444
38545
|
bestSpan = end - start2;
|
|
@@ -38446,6 +38547,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38446
38547
|
}
|
|
38447
38548
|
return best;
|
|
38448
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);
|
|
38449
38554
|
const sourcesWithVar = sources.filter(
|
|
38450
38555
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
38451
38556
|
);
|
|
@@ -38747,16 +38852,47 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38747
38852
|
if (!m) continue;
|
|
38748
38853
|
const [, obj, rhs] = m;
|
|
38749
38854
|
if (known.has(obj)) continue;
|
|
38750
|
-
|
|
38751
|
-
const rhsTainted = sourcesWithVar.some(
|
|
38752
|
-
(s) => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`, "u").test(codePart)
|
|
38753
|
-
);
|
|
38754
|
-
if (!rhsTainted) continue;
|
|
38855
|
+
if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
|
|
38755
38856
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
38756
38857
|
added = true;
|
|
38757
38858
|
}
|
|
38758
38859
|
}
|
|
38759
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
|
+
}
|
|
38760
38896
|
const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
|
|
38761
38897
|
const reCache = /* @__PURE__ */ new Map();
|
|
38762
38898
|
for (const s of sourcesWithVar) {
|
|
@@ -38819,6 +38955,11 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38819
38955
|
const sinkFn = goFnStart(sink.line);
|
|
38820
38956
|
if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn) continue;
|
|
38821
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
|
+
}
|
|
38822
38963
|
if (complex) {
|
|
38823
38964
|
const re = reCache.get(source.variable);
|
|
38824
38965
|
if (!re || !re.test(expr)) continue;
|
|
@@ -4480,6 +4480,21 @@ function extractTypes(tree, cache, language) {
|
|
|
4480
4480
|
}
|
|
4481
4481
|
return extractJavaTypes(tree, cache);
|
|
4482
4482
|
}
|
|
4483
|
+
function csharpMemberNodes(body2) {
|
|
4484
|
+
if (!body2) return [];
|
|
4485
|
+
const out2 = [];
|
|
4486
|
+
const visit = (parent, depth) => {
|
|
4487
|
+
if (depth > 32) return;
|
|
4488
|
+
for (let i2 = 0; i2 < parent.childCount; i2++) {
|
|
4489
|
+
const c = parent.child(i2);
|
|
4490
|
+
if (!c) continue;
|
|
4491
|
+
if (c.type.startsWith("preproc_")) visit(c, depth + 1);
|
|
4492
|
+
else out2.push(c);
|
|
4493
|
+
}
|
|
4494
|
+
};
|
|
4495
|
+
visit(body2, 0);
|
|
4496
|
+
return out2;
|
|
4497
|
+
}
|
|
4483
4498
|
function extractCSharpTypes(tree, cache) {
|
|
4484
4499
|
const types = [];
|
|
4485
4500
|
const KINDS = {
|
|
@@ -4496,9 +4511,8 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4496
4511
|
const methods = [];
|
|
4497
4512
|
const fields = extractCSharpFields(body2);
|
|
4498
4513
|
if (body2) {
|
|
4499
|
-
for (
|
|
4500
|
-
|
|
4501
|
-
if (!m || m.type !== "method_declaration") continue;
|
|
4514
|
+
for (const m of csharpMemberNodes(body2)) {
|
|
4515
|
+
if (m.type !== "method_declaration") continue;
|
|
4502
4516
|
const mName = m.childForFieldName("name");
|
|
4503
4517
|
const paramList = m.childForFieldName("parameters");
|
|
4504
4518
|
const parameters = [];
|
|
@@ -4549,9 +4563,7 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4549
4563
|
function extractCSharpFields(body2) {
|
|
4550
4564
|
const fields = [];
|
|
4551
4565
|
if (!body2) return fields;
|
|
4552
|
-
for (
|
|
4553
|
-
const c = body2.child(i2);
|
|
4554
|
-
if (!c) continue;
|
|
4566
|
+
for (const c of csharpMemberNodes(body2)) {
|
|
4555
4567
|
if (c.type === "field_declaration") {
|
|
4556
4568
|
let varDecl = null;
|
|
4557
4569
|
for (let k = 0; k < c.childCount; k++) {
|
|
@@ -6085,6 +6097,13 @@ function extractCalls(tree, cache, language) {
|
|
|
6085
6097
|
function buildCSharpReceiverTypeMap(tree, cache) {
|
|
6086
6098
|
const map = /* @__PURE__ */ new Map();
|
|
6087
6099
|
const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
|
|
6100
|
+
for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
|
|
6101
|
+
const nameNode = prm.childForFieldName("name");
|
|
6102
|
+
const typeNode = prm.childForFieldName("type");
|
|
6103
|
+
if (nameNode?.type !== "identifier" || !typeNode) continue;
|
|
6104
|
+
const t = getNodeText(typeNode);
|
|
6105
|
+
if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
|
|
6106
|
+
}
|
|
6088
6107
|
for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
|
|
6089
6108
|
const typeNode = vd.childForFieldName("type");
|
|
6090
6109
|
const declaredType = typeNode ? getNodeText(typeNode) : null;
|
|
@@ -6114,6 +6133,7 @@ function findFirstDescendant(node, type) {
|
|
|
6114
6133
|
}
|
|
6115
6134
|
return null;
|
|
6116
6135
|
}
|
|
6136
|
+
var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
|
|
6117
6137
|
var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
|
|
6118
6138
|
"ExecuteScalar",
|
|
6119
6139
|
"ExecuteReader",
|
|
@@ -6266,7 +6286,8 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6266
6286
|
if (recvType !== "DirectorySearcher") continue;
|
|
6267
6287
|
} else if (propName === "StatusDescription") {
|
|
6268
6288
|
const recv = exprNode ? getNodeText(exprNode) : "";
|
|
6269
|
-
|
|
6289
|
+
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
6290
|
+
if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
|
|
6270
6291
|
} else if (propName === "BaseAddress") {
|
|
6271
6292
|
const recv = exprNode ? getNodeText(exprNode) : null;
|
|
6272
6293
|
const recvType = recv ? typeMap.get(recv) : void 0;
|
|
@@ -11139,6 +11160,23 @@ var DEFAULT_SOURCES = [
|
|
|
11139
11160
|
{ method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11140
11161
|
{ method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
|
|
11141
11162
|
// Database result sources
|
|
11163
|
+
// C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
|
|
11164
|
+
// rows above one-for-one, including the severity split: console/file I/O is
|
|
11165
|
+
// `io_input` at high, a database row read is `db_input` at medium. There is
|
|
11166
|
+
// no separate confidence tier — `type` + `severity` IS the tier, exactly as
|
|
11167
|
+
// for `BufferedReader.readLine` vs `ResultSet.getString`.
|
|
11168
|
+
{ method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11169
|
+
{ method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11170
|
+
{ method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11171
|
+
{ method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11172
|
+
// Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
|
|
11173
|
+
// `OpenRead` return the channel; the read off it is covered by the
|
|
11174
|
+
// StreamReader rows above, and seeding the channel too lets a flow form when
|
|
11175
|
+
// the read is inlined.
|
|
11176
|
+
{ method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11177
|
+
{ method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11178
|
+
{ method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11179
|
+
{ method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
|
|
11142
11180
|
{ method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11143
11181
|
{ method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
11144
11182
|
{ method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
|
|
@@ -13703,8 +13741,12 @@ var DEFAULT_SINKS = [
|
|
|
13703
13741
|
{ method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13704
13742
|
{ method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13705
13743
|
{ method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13706
|
-
|
|
13707
|
-
|
|
13744
|
+
// #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
|
|
13745
|
+
// is not CWE-22. The `(string path)` overload still matches.
|
|
13746
|
+
{ method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
|
|
13747
|
+
// #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
|
|
13748
|
+
// is not CWE-22. The `(string path)` overload still matches.
|
|
13749
|
+
{ method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
|
|
13708
13750
|
// Remaining System.IO.File path APIs — class-scoped so they don't collide
|
|
13709
13751
|
// with unrelated methods. Copy/Move take a source AND destination path (both
|
|
13710
13752
|
// attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
|
|
@@ -13748,6 +13790,23 @@ var DEFAULT_SINKS = [
|
|
|
13748
13790
|
// `new RestClient(userUrl)` points every later request at attacker-controlled
|
|
13749
13791
|
// infrastructure. Ctor arg 0, taint-gated.
|
|
13750
13792
|
{ method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13793
|
+
// C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
|
|
13794
|
+
// part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
|
|
13795
|
+
// and can then execute attacker-supplied C#, so the source argument is the
|
|
13796
|
+
// injection point. The signature is
|
|
13797
|
+
// `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
|
|
13798
|
+
// so the sources begin at position 1 — position 0 is the options object.
|
|
13799
|
+
//
|
|
13800
|
+
// These rows are only useful together with the #530 StringBuilder
|
|
13801
|
+
// propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
|
|
13802
|
+
// through `sb.Append(...)` and reads it back with `ToString()`, so before
|
|
13803
|
+
// that fix the sink registered but no taint ever reached it.
|
|
13804
|
+
{ method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
|
|
13805
|
+
{ method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
|
|
13806
|
+
// Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
|
|
13807
|
+
// `Create` compiles it; either is the point where attacker text becomes code.
|
|
13808
|
+
{ method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13809
|
+
{ method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13751
13810
|
// C# code injection — dynamic script/assembly loading (CWE-94).
|
|
13752
13811
|
{ method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13753
13812
|
{ method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
@@ -15529,6 +15588,20 @@ function escapeRe(s) {
|
|
|
15529
15588
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15530
15589
|
}
|
|
15531
15590
|
var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
|
|
15591
|
+
function argIsStreamExpression(call, position) {
|
|
15592
|
+
const arg = call.arguments.find((a) => a.position === position);
|
|
15593
|
+
if (!arg) return false;
|
|
15594
|
+
const expr = (arg.expression ?? "").trim();
|
|
15595
|
+
if (!expr) return false;
|
|
15596
|
+
return (
|
|
15597
|
+
// `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
|
|
15598
|
+
/^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
|
|
15599
|
+
/\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
|
|
15600
|
+
/\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
|
|
15601
|
+
/\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
|
|
15602
|
+
/^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
|
|
15603
|
+
);
|
|
15604
|
+
}
|
|
15532
15605
|
function argIsStringLiteral(call, position) {
|
|
15533
15606
|
const arg = call.arguments.find((a) => a.position === position);
|
|
15534
15607
|
if (!arg) return false;
|
|
@@ -15766,6 +15839,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
15766
15839
|
if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
|
|
15767
15840
|
continue;
|
|
15768
15841
|
}
|
|
15842
|
+
if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
|
|
15843
|
+
continue;
|
|
15844
|
+
}
|
|
15769
15845
|
if (pattern.type === "command_injection") {
|
|
15770
15846
|
if (call.is_constructor) {
|
|
15771
15847
|
if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
|