circle-ir 3.22.0 → 3.22.2
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/README.md +5 -1
- package/dist/analysis/config-loader.js +6 -6
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.js +31 -2
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/infinite-loop-pass.js +15 -0
- package/dist/analysis/passes/infinite-loop-pass.js.map +1 -1
- package/dist/analysis/passes/swallowed-exception-pass.js +16 -0
- package/dist/analysis/passes/swallowed-exception-pass.js.map +1 -1
- package/dist/analysis/passes/unhandled-exception-pass.js +23 -0
- package/dist/analysis/passes/unhandled-exception-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.js +128 -22
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +227 -20
- package/dist/core/circle-ir-core.cjs +186 -20
- package/dist/core/circle-ir-core.js +186 -20
- package/dist/core/extractors/calls.js +1 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/docs/SPEC.md +1 -1
- package/package.json +3 -1
|
@@ -6348,7 +6348,7 @@ function extractBashCommandInfo(node) {
|
|
|
6348
6348
|
for (let i2 = 0; i2 < node.childCount; i2++) {
|
|
6349
6349
|
const child = node.child(i2);
|
|
6350
6350
|
if (!child) continue;
|
|
6351
|
-
if (child === nameNode) continue;
|
|
6351
|
+
if (child === nameNode || child.id === nameNode.id) continue;
|
|
6352
6352
|
if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
|
|
6353
6353
|
continue;
|
|
6354
6354
|
}
|
|
@@ -9983,9 +9983,8 @@ var DEFAULT_SINKS = [
|
|
|
9983
9983
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
9984
9984
|
// ActiveMQ control commands
|
|
9985
9985
|
{ method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
9986
|
-
// XStream deserialization (
|
|
9987
|
-
|
|
9988
|
-
{ method: "unmarshal", class: "XStream", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
9986
|
+
// XStream deserialization — classified as CWE-502 (deserialization), not CWE-78 (command injection).
|
|
9987
|
+
// The deserialization sink entries at lines ~1059 handle this correctly.
|
|
9989
9988
|
{ method: "fromString", class: "FileConverter", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
9990
9989
|
// Plexus command line
|
|
9991
9990
|
{ method: "getPosition", class: "Commandline", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -10669,7 +10668,8 @@ var DEFAULT_SINKS = [
|
|
|
10669
10668
|
{ method: "query", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10670
10669
|
{ method: "query", class: "Pool", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10671
10670
|
{ method: "query", class: "Client", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10672
|
-
{ method:
|
|
10671
|
+
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
10672
|
+
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
10673
10673
|
{ method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0] },
|
|
10674
10674
|
// Browser DOM XSS sinks
|
|
10675
10675
|
{ method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
|
|
@@ -10866,8 +10866,8 @@ var DEFAULT_SINKS = [
|
|
|
10866
10866
|
{ method: "execute", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10867
10867
|
{ method: "query_row", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10868
10868
|
{ method: "prepare", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10869
|
-
// sqlx::query macro
|
|
10870
|
-
{ method: "query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10869
|
+
// sqlx::query macro — use class-specific pattern
|
|
10870
|
+
{ method: "query", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10871
10871
|
// rusqlite specific
|
|
10872
10872
|
{ method: "prepare", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10873
10873
|
{ method: "execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
@@ -11469,15 +11469,24 @@ function isInterproceduralTaintableType(typeName) {
|
|
|
11469
11469
|
}
|
|
11470
11470
|
function isParameterizedQueryCall(call, pattern) {
|
|
11471
11471
|
if (pattern.type !== "sql_injection") return false;
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
const
|
|
11477
|
-
if (
|
|
11472
|
+
const queryArg = call.arguments.find((a) => a.position === 0);
|
|
11473
|
+
if (queryArg) {
|
|
11474
|
+
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
11475
|
+
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
11476
|
+
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
11477
|
+
if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
|
|
11478
11478
|
return true;
|
|
11479
11479
|
}
|
|
11480
11480
|
}
|
|
11481
|
+
if (call.arguments.length >= 2) {
|
|
11482
|
+
const secondArg = call.arguments.find((a) => a.position === 1);
|
|
11483
|
+
if (secondArg?.expression) {
|
|
11484
|
+
const expr = secondArg.expression.trim();
|
|
11485
|
+
if (expr.startsWith("[")) {
|
|
11486
|
+
return true;
|
|
11487
|
+
}
|
|
11488
|
+
}
|
|
11489
|
+
}
|
|
11481
11490
|
return false;
|
|
11482
11491
|
}
|
|
11483
11492
|
function findSinks(calls, patterns, typeHierarchy) {
|
|
@@ -11603,9 +11612,130 @@ var SAFE_RECEIVERS_BY_METHOD = {
|
|
|
11603
11612
|
"stmt",
|
|
11604
11613
|
"statement",
|
|
11605
11614
|
"cursor"
|
|
11615
|
+
]),
|
|
11616
|
+
// query() is only a SQL sink when receiver is a database handle — not URL builders,
|
|
11617
|
+
// DOM selectors, GraphQL clients, DNS resolvers, etc.
|
|
11618
|
+
query: /* @__PURE__ */ new Set([
|
|
11619
|
+
"uri",
|
|
11620
|
+
"url",
|
|
11621
|
+
"builder",
|
|
11622
|
+
"uribuilder",
|
|
11623
|
+
"uricomponents",
|
|
11624
|
+
"uricomponentsbuilder",
|
|
11625
|
+
"servleturicomponentsbuilder",
|
|
11626
|
+
"httpurl",
|
|
11627
|
+
"urlbuilder",
|
|
11628
|
+
"webclient",
|
|
11629
|
+
"request",
|
|
11630
|
+
"req",
|
|
11631
|
+
"router",
|
|
11632
|
+
"route",
|
|
11633
|
+
"app",
|
|
11634
|
+
"express",
|
|
11635
|
+
"parser",
|
|
11636
|
+
"selector",
|
|
11637
|
+
"jquery",
|
|
11638
|
+
"dom",
|
|
11639
|
+
"document",
|
|
11640
|
+
"element",
|
|
11641
|
+
"xmlpath",
|
|
11642
|
+
"xpath",
|
|
11643
|
+
"dns",
|
|
11644
|
+
"resolver",
|
|
11645
|
+
"graphql",
|
|
11646
|
+
"apollo",
|
|
11647
|
+
"querybuilder",
|
|
11648
|
+
"criteria"
|
|
11649
|
+
]),
|
|
11650
|
+
// authenticate() — safe on auth framework objects (token verification, not code exec)
|
|
11651
|
+
authenticate: /* @__PURE__ */ new Set([
|
|
11652
|
+
"auth",
|
|
11653
|
+
"authenticator",
|
|
11654
|
+
"authmanager",
|
|
11655
|
+
"authprovider",
|
|
11656
|
+
"authenticationmanager",
|
|
11657
|
+
"authservice",
|
|
11658
|
+
"oauth",
|
|
11659
|
+
"token",
|
|
11660
|
+
"jwt",
|
|
11661
|
+
"passport",
|
|
11662
|
+
"session",
|
|
11663
|
+
"security",
|
|
11664
|
+
"credentials",
|
|
11665
|
+
"identityprovider",
|
|
11666
|
+
"ldap",
|
|
11667
|
+
"saml",
|
|
11668
|
+
"oidc"
|
|
11669
|
+
]),
|
|
11670
|
+
// add() is extremely generic — safe on collections, UI containers, builders, etc.
|
|
11671
|
+
add: /* @__PURE__ */ new Set([
|
|
11672
|
+
"list",
|
|
11673
|
+
"set",
|
|
11674
|
+
"map",
|
|
11675
|
+
"collection",
|
|
11676
|
+
"array",
|
|
11677
|
+
"queue",
|
|
11678
|
+
"deque",
|
|
11679
|
+
"stack",
|
|
11680
|
+
"vector",
|
|
11681
|
+
"builder",
|
|
11682
|
+
"panel",
|
|
11683
|
+
"container",
|
|
11684
|
+
"group",
|
|
11685
|
+
"layout",
|
|
11686
|
+
"menu",
|
|
11687
|
+
"toolbar",
|
|
11688
|
+
"model",
|
|
11689
|
+
"registry",
|
|
11690
|
+
"context",
|
|
11691
|
+
"config",
|
|
11692
|
+
"options",
|
|
11693
|
+
"params",
|
|
11694
|
+
"headers",
|
|
11695
|
+
"attributes",
|
|
11696
|
+
"listeners",
|
|
11697
|
+
"handlers",
|
|
11698
|
+
"filters",
|
|
11699
|
+
"interceptors",
|
|
11700
|
+
"validators",
|
|
11701
|
+
"extensions",
|
|
11702
|
+
"plugins",
|
|
11703
|
+
"modules",
|
|
11704
|
+
"components",
|
|
11705
|
+
"children",
|
|
11706
|
+
"items",
|
|
11707
|
+
"elements",
|
|
11708
|
+
"entries",
|
|
11709
|
+
"rows",
|
|
11710
|
+
"columns",
|
|
11711
|
+
"fields",
|
|
11712
|
+
"properties",
|
|
11713
|
+
"descriptors",
|
|
11714
|
+
"nodes",
|
|
11715
|
+
"actions",
|
|
11716
|
+
"results",
|
|
11717
|
+
"errors",
|
|
11718
|
+
"warnings",
|
|
11719
|
+
"messages",
|
|
11720
|
+
"notifications",
|
|
11721
|
+
"events",
|
|
11722
|
+
"subscribers",
|
|
11723
|
+
"observers",
|
|
11724
|
+
"providers",
|
|
11725
|
+
"services",
|
|
11726
|
+
"beans",
|
|
11727
|
+
"tasks",
|
|
11728
|
+
"jobs",
|
|
11729
|
+
"workers",
|
|
11730
|
+
"threads",
|
|
11731
|
+
"schedulers"
|
|
11606
11732
|
])
|
|
11607
11733
|
};
|
|
11608
|
-
function isKnownSafeReceiverForMethod(receiver, method,
|
|
11734
|
+
function isKnownSafeReceiverForMethod(receiver, method, sinkType) {
|
|
11735
|
+
const lowerMethod = method.toLowerCase();
|
|
11736
|
+
if ((lowerMethod === "fromxml" || lowerMethod === "unmarshal") && sinkType === "command_injection") {
|
|
11737
|
+
return true;
|
|
11738
|
+
}
|
|
11609
11739
|
const safeReceivers = SAFE_RECEIVERS_BY_METHOD[method];
|
|
11610
11740
|
if (!safeReceivers) return false;
|
|
11611
11741
|
const lowerReceiver = receiver.toLowerCase();
|
|
@@ -11661,6 +11791,15 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11661
11791
|
if (receiver === className) {
|
|
11662
11792
|
return true;
|
|
11663
11793
|
}
|
|
11794
|
+
if (receiver.includes("::")) {
|
|
11795
|
+
const scopePrefix = receiver.match(/^(\w+)::/);
|
|
11796
|
+
if (scopePrefix) {
|
|
11797
|
+
const typeName = scopePrefix[1];
|
|
11798
|
+
if (typeName === className || typeName.toLowerCase() === className.toLowerCase()) {
|
|
11799
|
+
return true;
|
|
11800
|
+
}
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11664
11803
|
const lowerReceiver = receiver.toLowerCase();
|
|
11665
11804
|
const lowerClass = className.toLowerCase();
|
|
11666
11805
|
if (lowerReceiver.endsWith(lowerClass) || lowerReceiver.endsWith("." + lowerClass)) {
|
|
@@ -11711,11 +11850,23 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11711
11850
|
}
|
|
11712
11851
|
}
|
|
11713
11852
|
}
|
|
11714
|
-
if (lowerClass.includes(lowerReceiver)) {
|
|
11715
|
-
|
|
11853
|
+
if (lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
11854
|
+
if (lowerReceiver.length >= 5 || lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
11855
|
+
return true;
|
|
11856
|
+
}
|
|
11716
11857
|
}
|
|
11717
|
-
if (
|
|
11718
|
-
|
|
11858
|
+
if (lowerReceiver.length >= 2) {
|
|
11859
|
+
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
11860
|
+
return true;
|
|
11861
|
+
}
|
|
11862
|
+
}
|
|
11863
|
+
if (lowerReceiver.length >= 3) {
|
|
11864
|
+
const words = className.replace(/([a-z])([A-Z])/g, "$1\0$2").toLowerCase().split("\0");
|
|
11865
|
+
for (const word of words) {
|
|
11866
|
+
if (word.startsWith(lowerReceiver) && lowerReceiver.length / word.length >= 0.4) {
|
|
11867
|
+
return true;
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11719
11870
|
}
|
|
11720
11871
|
const commonMappings = {
|
|
11721
11872
|
// HTTP/Servlet
|
|
@@ -11736,8 +11887,10 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11736
11887
|
// Process/Runtime
|
|
11737
11888
|
runtime: ["Runtime"],
|
|
11738
11889
|
pb: ["ProcessBuilder"],
|
|
11739
|
-
// Scripting
|
|
11890
|
+
// Scripting / Expression evaluation
|
|
11740
11891
|
engine: ["ScriptEngine"],
|
|
11892
|
+
ev: ["ExpressionEvaluator", "ScriptEvaluator", "ClassBodyEvaluator"],
|
|
11893
|
+
evaluator: ["ExpressionEvaluator", "ScriptEvaluator", "ClassBodyEvaluator"],
|
|
11741
11894
|
// LDAP
|
|
11742
11895
|
ctx: ["Context", "InitialContext", "DirContext", "InitialDirContext", "LdapContext"],
|
|
11743
11896
|
context: ["Context", "InitialContext", "DirContext", "InitialDirContext", "LdapContext"],
|
|
@@ -11827,12 +11980,25 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11827
11980
|
knex: ["knex"],
|
|
11828
11981
|
prisma: ["prisma"],
|
|
11829
11982
|
axios: ["axios"],
|
|
11830
|
-
fetch: ["fetch"]
|
|
11983
|
+
fetch: ["fetch"],
|
|
11984
|
+
// Go idioms (single-letter receivers)
|
|
11985
|
+
r: ["Request"],
|
|
11986
|
+
w: ["ResponseWriter"]
|
|
11831
11987
|
};
|
|
11832
11988
|
const mappings = commonMappings[lowerReceiver];
|
|
11833
11989
|
if (mappings && Array.isArray(mappings) && mappings.includes(className)) {
|
|
11834
11990
|
return true;
|
|
11835
11991
|
}
|
|
11992
|
+
const strippedReceiver = lowerReceiver.replace(/\d+$/, "");
|
|
11993
|
+
if (strippedReceiver !== lowerReceiver && strippedReceiver.length >= 2) {
|
|
11994
|
+
const strippedMappings = commonMappings[strippedReceiver];
|
|
11995
|
+
if (strippedMappings && Array.isArray(strippedMappings) && strippedMappings.includes(className)) {
|
|
11996
|
+
return true;
|
|
11997
|
+
}
|
|
11998
|
+
if (lowerClass.startsWith(strippedReceiver) || strippedReceiver.startsWith(lowerClass)) {
|
|
11999
|
+
return true;
|
|
12000
|
+
}
|
|
12001
|
+
}
|
|
11836
12002
|
return false;
|
|
11837
12003
|
}
|
|
11838
12004
|
function formatCallLocation(call) {
|
|
@@ -22605,6 +22771,18 @@ var ITERATOR_LOOP_PATTERNS = [
|
|
|
22605
22771
|
/\bfor\s*\([^)]+\s*:\s*[^)]+\)/
|
|
22606
22772
|
// Java: for (Type x : collection)
|
|
22607
22773
|
];
|
|
22774
|
+
var BOUNDED_LOOP_PATTERNS = [
|
|
22775
|
+
/\bfor\s*\([^;]*;\s*\w+\s*[<>!=]+\s*[^;]*\.length\b/,
|
|
22776
|
+
// for (i=0; i < arr.length; i++)
|
|
22777
|
+
/\bfor\s*\([^;]*;\s*\w+\s*<\s*\w+\s*;/,
|
|
22778
|
+
// for (i=0; i < N; i++)
|
|
22779
|
+
/\bfor\s*\([^;]*;\s*\w+\s*>\s*\d+\s*;/,
|
|
22780
|
+
// for (i=N; i > 0; i--)
|
|
22781
|
+
/\bfor\s*\([^;]*;\s*\w+\s*<=\s*\w+\s*;/,
|
|
22782
|
+
// for (i=0; i <= N; i++)
|
|
22783
|
+
/\bfor\s*\([^;]*;\s*\w+\s*>=\s*\d+\s*;/
|
|
22784
|
+
// for (i=N; i >= 0; i--)
|
|
22785
|
+
];
|
|
22608
22786
|
var InfiniteLoopPass = class {
|
|
22609
22787
|
name = "infinite-loop";
|
|
22610
22788
|
category = "reliability";
|
|
@@ -22672,6 +22850,8 @@ var InfiniteLoopPass = class {
|
|
|
22672
22850
|
const headerLine = codeLines[header.start_line - 1] ?? "";
|
|
22673
22851
|
const isIteratorLoop = ITERATOR_LOOP_PATTERNS.some((pattern) => pattern.test(headerLine));
|
|
22674
22852
|
if (isIteratorLoop) continue;
|
|
22853
|
+
const isBoundedLoop = BOUNDED_LOOP_PATTERNS.some((pattern) => pattern.test(headerLine));
|
|
22854
|
+
if (isBoundedLoop) continue;
|
|
22675
22855
|
reportedHeaders.add(headerId);
|
|
22676
22856
|
potentialInfiniteLoops.push({ headerLine: header.start_line, bodyEndLine: bodyEnd });
|
|
22677
22857
|
const loc = bodyStart === bodyEnd ? `line ${bodyStart}` : `lines ${bodyStart}\u2013${bodyEnd}`;
|
|
@@ -23187,6 +23367,19 @@ var SwallowedExceptionPass = class {
|
|
|
23187
23367
|
break;
|
|
23188
23368
|
}
|
|
23189
23369
|
}
|
|
23370
|
+
if (!hasAction) {
|
|
23371
|
+
const catchVarMatch = (codeLines[catchLine - 1] ?? "").match(/catch\s*\(\s*(\w+)/);
|
|
23372
|
+
if (catchVarMatch) {
|
|
23373
|
+
const catchVar = catchVarMatch[1];
|
|
23374
|
+
const forwardRe = new RegExp(`\\w+\\s*\\([^)]*\\b${catchVar}\\b`);
|
|
23375
|
+
for (let ln = catchLine + 1; ln <= catchBodyEnd && ln <= codeLines.length; ln++) {
|
|
23376
|
+
if (forwardRe.test(codeLines[ln - 1] ?? "")) {
|
|
23377
|
+
hasAction = true;
|
|
23378
|
+
break;
|
|
23379
|
+
}
|
|
23380
|
+
}
|
|
23381
|
+
}
|
|
23382
|
+
}
|
|
23190
23383
|
if (!hasAction) {
|
|
23191
23384
|
reported.add(catchLine);
|
|
23192
23385
|
swallowed.push({ line: catchLine });
|
|
@@ -23287,6 +23480,19 @@ var BroadCatchPass = class {
|
|
|
23287
23480
|
// src/analysis/passes/unhandled-exception-pass.ts
|
|
23288
23481
|
var JS_THROW_RE = /^\s*throw\s+/;
|
|
23289
23482
|
var PYTHON_RAISE_RE = /^\s*raise\b/;
|
|
23483
|
+
function isValidationThrow(lines, throwLine) {
|
|
23484
|
+
const throwText = lines[throwLine - 1] ?? "";
|
|
23485
|
+
if (!/\bthrow\s+new\s+(TypeError|RangeError|ArgumentError|ERR_\w+)\b/.test(throwText)) {
|
|
23486
|
+
return false;
|
|
23487
|
+
}
|
|
23488
|
+
for (let i2 = 1; i2 <= 3 && throwLine - i2 >= 1; i2++) {
|
|
23489
|
+
const prev = lines[throwLine - i2 - 1] ?? "";
|
|
23490
|
+
if (/\bif\s*\(/.test(prev) && /typeof|===\s*['"]undefined['"]|===\s*null|!|\.length|<\s*\d|>\s*\d/.test(prev)) {
|
|
23491
|
+
return true;
|
|
23492
|
+
}
|
|
23493
|
+
}
|
|
23494
|
+
return false;
|
|
23495
|
+
}
|
|
23290
23496
|
var JS_TRY_RE = /^\s*try\s*\{/;
|
|
23291
23497
|
var JS_CATCH_RE = /^\s*\}\s*catch\b/;
|
|
23292
23498
|
var PY_TRY_RE = /^\s*try\s*:/;
|
|
@@ -23408,6 +23614,7 @@ var UnhandledExceptionPass = class {
|
|
|
23408
23614
|
const methodInfo = graph.methodAtLine(ln);
|
|
23409
23615
|
const methodKey = methodInfo ? `${methodInfo.method.start_line}-${methodInfo.method.end_line}` : `global-${ln}`;
|
|
23410
23616
|
if (reportedMethods.has(methodKey)) continue;
|
|
23617
|
+
if (isValidationThrow(codeLines, ln)) continue;
|
|
23411
23618
|
reportedMethods.add(methodKey);
|
|
23412
23619
|
const methodName = methodInfo?.method.name ?? "<anonymous>";
|
|
23413
23620
|
unhandled.push({ line: ln, method: methodName });
|
|
@@ -6424,7 +6424,7 @@ function extractBashCommandInfo(node) {
|
|
|
6424
6424
|
for (let i2 = 0; i2 < node.childCount; i2++) {
|
|
6425
6425
|
const child = node.child(i2);
|
|
6426
6426
|
if (!child) continue;
|
|
6427
|
-
if (child === nameNode) continue;
|
|
6427
|
+
if (child === nameNode || child.id === nameNode.id) continue;
|
|
6428
6428
|
if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
|
|
6429
6429
|
continue;
|
|
6430
6430
|
}
|
|
@@ -10096,9 +10096,8 @@ var DEFAULT_SINKS = [
|
|
|
10096
10096
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10097
10097
|
// ActiveMQ control commands
|
|
10098
10098
|
{ method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10099
|
-
// XStream deserialization (
|
|
10100
|
-
|
|
10101
|
-
{ method: "unmarshal", class: "XStream", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10099
|
+
// XStream deserialization — classified as CWE-502 (deserialization), not CWE-78 (command injection).
|
|
10100
|
+
// The deserialization sink entries at lines ~1059 handle this correctly.
|
|
10102
10101
|
{ method: "fromString", class: "FileConverter", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10103
10102
|
// Plexus command line
|
|
10104
10103
|
{ method: "getPosition", class: "Commandline", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -10782,7 +10781,8 @@ var DEFAULT_SINKS = [
|
|
|
10782
10781
|
{ method: "query", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10783
10782
|
{ method: "query", class: "Pool", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10784
10783
|
{ method: "query", class: "Client", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10785
|
-
{ method:
|
|
10784
|
+
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
10785
|
+
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
10786
10786
|
{ method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0] },
|
|
10787
10787
|
// Browser DOM XSS sinks
|
|
10788
10788
|
{ method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
|
|
@@ -10979,8 +10979,8 @@ var DEFAULT_SINKS = [
|
|
|
10979
10979
|
{ method: "execute", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10980
10980
|
{ method: "query_row", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10981
10981
|
{ method: "prepare", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10982
|
-
// sqlx::query macro
|
|
10983
|
-
{ method: "query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10982
|
+
// sqlx::query macro — use class-specific pattern
|
|
10983
|
+
{ method: "query", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10984
10984
|
// rusqlite specific
|
|
10985
10985
|
{ method: "prepare", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
10986
10986
|
{ method: "execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
|
|
@@ -11495,15 +11495,24 @@ function isInterproceduralTaintableType(typeName) {
|
|
|
11495
11495
|
}
|
|
11496
11496
|
function isParameterizedQueryCall(call, pattern) {
|
|
11497
11497
|
if (pattern.type !== "sql_injection") return false;
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
const
|
|
11503
|
-
if (
|
|
11498
|
+
const queryArg = call.arguments.find((a) => a.position === 0);
|
|
11499
|
+
if (queryArg) {
|
|
11500
|
+
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
11501
|
+
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
11502
|
+
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
11503
|
+
if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
|
|
11504
11504
|
return true;
|
|
11505
11505
|
}
|
|
11506
11506
|
}
|
|
11507
|
+
if (call.arguments.length >= 2) {
|
|
11508
|
+
const secondArg = call.arguments.find((a) => a.position === 1);
|
|
11509
|
+
if (secondArg?.expression) {
|
|
11510
|
+
const expr = secondArg.expression.trim();
|
|
11511
|
+
if (expr.startsWith("[")) {
|
|
11512
|
+
return true;
|
|
11513
|
+
}
|
|
11514
|
+
}
|
|
11515
|
+
}
|
|
11507
11516
|
return false;
|
|
11508
11517
|
}
|
|
11509
11518
|
function findSinks(calls, patterns, typeHierarchy) {
|
|
@@ -11629,9 +11638,130 @@ var SAFE_RECEIVERS_BY_METHOD = {
|
|
|
11629
11638
|
"stmt",
|
|
11630
11639
|
"statement",
|
|
11631
11640
|
"cursor"
|
|
11641
|
+
]),
|
|
11642
|
+
// query() is only a SQL sink when receiver is a database handle — not URL builders,
|
|
11643
|
+
// DOM selectors, GraphQL clients, DNS resolvers, etc.
|
|
11644
|
+
query: /* @__PURE__ */ new Set([
|
|
11645
|
+
"uri",
|
|
11646
|
+
"url",
|
|
11647
|
+
"builder",
|
|
11648
|
+
"uribuilder",
|
|
11649
|
+
"uricomponents",
|
|
11650
|
+
"uricomponentsbuilder",
|
|
11651
|
+
"servleturicomponentsbuilder",
|
|
11652
|
+
"httpurl",
|
|
11653
|
+
"urlbuilder",
|
|
11654
|
+
"webclient",
|
|
11655
|
+
"request",
|
|
11656
|
+
"req",
|
|
11657
|
+
"router",
|
|
11658
|
+
"route",
|
|
11659
|
+
"app",
|
|
11660
|
+
"express",
|
|
11661
|
+
"parser",
|
|
11662
|
+
"selector",
|
|
11663
|
+
"jquery",
|
|
11664
|
+
"dom",
|
|
11665
|
+
"document",
|
|
11666
|
+
"element",
|
|
11667
|
+
"xmlpath",
|
|
11668
|
+
"xpath",
|
|
11669
|
+
"dns",
|
|
11670
|
+
"resolver",
|
|
11671
|
+
"graphql",
|
|
11672
|
+
"apollo",
|
|
11673
|
+
"querybuilder",
|
|
11674
|
+
"criteria"
|
|
11675
|
+
]),
|
|
11676
|
+
// authenticate() — safe on auth framework objects (token verification, not code exec)
|
|
11677
|
+
authenticate: /* @__PURE__ */ new Set([
|
|
11678
|
+
"auth",
|
|
11679
|
+
"authenticator",
|
|
11680
|
+
"authmanager",
|
|
11681
|
+
"authprovider",
|
|
11682
|
+
"authenticationmanager",
|
|
11683
|
+
"authservice",
|
|
11684
|
+
"oauth",
|
|
11685
|
+
"token",
|
|
11686
|
+
"jwt",
|
|
11687
|
+
"passport",
|
|
11688
|
+
"session",
|
|
11689
|
+
"security",
|
|
11690
|
+
"credentials",
|
|
11691
|
+
"identityprovider",
|
|
11692
|
+
"ldap",
|
|
11693
|
+
"saml",
|
|
11694
|
+
"oidc"
|
|
11695
|
+
]),
|
|
11696
|
+
// add() is extremely generic — safe on collections, UI containers, builders, etc.
|
|
11697
|
+
add: /* @__PURE__ */ new Set([
|
|
11698
|
+
"list",
|
|
11699
|
+
"set",
|
|
11700
|
+
"map",
|
|
11701
|
+
"collection",
|
|
11702
|
+
"array",
|
|
11703
|
+
"queue",
|
|
11704
|
+
"deque",
|
|
11705
|
+
"stack",
|
|
11706
|
+
"vector",
|
|
11707
|
+
"builder",
|
|
11708
|
+
"panel",
|
|
11709
|
+
"container",
|
|
11710
|
+
"group",
|
|
11711
|
+
"layout",
|
|
11712
|
+
"menu",
|
|
11713
|
+
"toolbar",
|
|
11714
|
+
"model",
|
|
11715
|
+
"registry",
|
|
11716
|
+
"context",
|
|
11717
|
+
"config",
|
|
11718
|
+
"options",
|
|
11719
|
+
"params",
|
|
11720
|
+
"headers",
|
|
11721
|
+
"attributes",
|
|
11722
|
+
"listeners",
|
|
11723
|
+
"handlers",
|
|
11724
|
+
"filters",
|
|
11725
|
+
"interceptors",
|
|
11726
|
+
"validators",
|
|
11727
|
+
"extensions",
|
|
11728
|
+
"plugins",
|
|
11729
|
+
"modules",
|
|
11730
|
+
"components",
|
|
11731
|
+
"children",
|
|
11732
|
+
"items",
|
|
11733
|
+
"elements",
|
|
11734
|
+
"entries",
|
|
11735
|
+
"rows",
|
|
11736
|
+
"columns",
|
|
11737
|
+
"fields",
|
|
11738
|
+
"properties",
|
|
11739
|
+
"descriptors",
|
|
11740
|
+
"nodes",
|
|
11741
|
+
"actions",
|
|
11742
|
+
"results",
|
|
11743
|
+
"errors",
|
|
11744
|
+
"warnings",
|
|
11745
|
+
"messages",
|
|
11746
|
+
"notifications",
|
|
11747
|
+
"events",
|
|
11748
|
+
"subscribers",
|
|
11749
|
+
"observers",
|
|
11750
|
+
"providers",
|
|
11751
|
+
"services",
|
|
11752
|
+
"beans",
|
|
11753
|
+
"tasks",
|
|
11754
|
+
"jobs",
|
|
11755
|
+
"workers",
|
|
11756
|
+
"threads",
|
|
11757
|
+
"schedulers"
|
|
11632
11758
|
])
|
|
11633
11759
|
};
|
|
11634
|
-
function isKnownSafeReceiverForMethod(receiver, method,
|
|
11760
|
+
function isKnownSafeReceiverForMethod(receiver, method, sinkType) {
|
|
11761
|
+
const lowerMethod = method.toLowerCase();
|
|
11762
|
+
if ((lowerMethod === "fromxml" || lowerMethod === "unmarshal") && sinkType === "command_injection") {
|
|
11763
|
+
return true;
|
|
11764
|
+
}
|
|
11635
11765
|
const safeReceivers = SAFE_RECEIVERS_BY_METHOD[method];
|
|
11636
11766
|
if (!safeReceivers) return false;
|
|
11637
11767
|
const lowerReceiver = receiver.toLowerCase();
|
|
@@ -11687,6 +11817,15 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11687
11817
|
if (receiver === className) {
|
|
11688
11818
|
return true;
|
|
11689
11819
|
}
|
|
11820
|
+
if (receiver.includes("::")) {
|
|
11821
|
+
const scopePrefix = receiver.match(/^(\w+)::/);
|
|
11822
|
+
if (scopePrefix) {
|
|
11823
|
+
const typeName = scopePrefix[1];
|
|
11824
|
+
if (typeName === className || typeName.toLowerCase() === className.toLowerCase()) {
|
|
11825
|
+
return true;
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
}
|
|
11690
11829
|
const lowerReceiver = receiver.toLowerCase();
|
|
11691
11830
|
const lowerClass = className.toLowerCase();
|
|
11692
11831
|
if (lowerReceiver.endsWith(lowerClass) || lowerReceiver.endsWith("." + lowerClass)) {
|
|
@@ -11737,11 +11876,23 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11737
11876
|
}
|
|
11738
11877
|
}
|
|
11739
11878
|
}
|
|
11740
|
-
if (lowerClass.includes(lowerReceiver)) {
|
|
11741
|
-
|
|
11879
|
+
if (lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
11880
|
+
if (lowerReceiver.length >= 5 || lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
11881
|
+
return true;
|
|
11882
|
+
}
|
|
11742
11883
|
}
|
|
11743
|
-
if (
|
|
11744
|
-
|
|
11884
|
+
if (lowerReceiver.length >= 2) {
|
|
11885
|
+
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
11886
|
+
return true;
|
|
11887
|
+
}
|
|
11888
|
+
}
|
|
11889
|
+
if (lowerReceiver.length >= 3) {
|
|
11890
|
+
const words = className.replace(/([a-z])([A-Z])/g, "$1\0$2").toLowerCase().split("\0");
|
|
11891
|
+
for (const word of words) {
|
|
11892
|
+
if (word.startsWith(lowerReceiver) && lowerReceiver.length / word.length >= 0.4) {
|
|
11893
|
+
return true;
|
|
11894
|
+
}
|
|
11895
|
+
}
|
|
11745
11896
|
}
|
|
11746
11897
|
const commonMappings = {
|
|
11747
11898
|
// HTTP/Servlet
|
|
@@ -11762,8 +11913,10 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11762
11913
|
// Process/Runtime
|
|
11763
11914
|
runtime: ["Runtime"],
|
|
11764
11915
|
pb: ["ProcessBuilder"],
|
|
11765
|
-
// Scripting
|
|
11916
|
+
// Scripting / Expression evaluation
|
|
11766
11917
|
engine: ["ScriptEngine"],
|
|
11918
|
+
ev: ["ExpressionEvaluator", "ScriptEvaluator", "ClassBodyEvaluator"],
|
|
11919
|
+
evaluator: ["ExpressionEvaluator", "ScriptEvaluator", "ClassBodyEvaluator"],
|
|
11767
11920
|
// LDAP
|
|
11768
11921
|
ctx: ["Context", "InitialContext", "DirContext", "InitialDirContext", "LdapContext"],
|
|
11769
11922
|
context: ["Context", "InitialContext", "DirContext", "InitialDirContext", "LdapContext"],
|
|
@@ -11853,12 +12006,25 @@ function receiverMightBeClass(receiver, className) {
|
|
|
11853
12006
|
knex: ["knex"],
|
|
11854
12007
|
prisma: ["prisma"],
|
|
11855
12008
|
axios: ["axios"],
|
|
11856
|
-
fetch: ["fetch"]
|
|
12009
|
+
fetch: ["fetch"],
|
|
12010
|
+
// Go idioms (single-letter receivers)
|
|
12011
|
+
r: ["Request"],
|
|
12012
|
+
w: ["ResponseWriter"]
|
|
11857
12013
|
};
|
|
11858
12014
|
const mappings = commonMappings[lowerReceiver];
|
|
11859
12015
|
if (mappings && Array.isArray(mappings) && mappings.includes(className)) {
|
|
11860
12016
|
return true;
|
|
11861
12017
|
}
|
|
12018
|
+
const strippedReceiver = lowerReceiver.replace(/\d+$/, "");
|
|
12019
|
+
if (strippedReceiver !== lowerReceiver && strippedReceiver.length >= 2) {
|
|
12020
|
+
const strippedMappings = commonMappings[strippedReceiver];
|
|
12021
|
+
if (strippedMappings && Array.isArray(strippedMappings) && strippedMappings.includes(className)) {
|
|
12022
|
+
return true;
|
|
12023
|
+
}
|
|
12024
|
+
if (lowerClass.startsWith(strippedReceiver) || strippedReceiver.startsWith(lowerClass)) {
|
|
12025
|
+
return true;
|
|
12026
|
+
}
|
|
12027
|
+
}
|
|
11862
12028
|
return false;
|
|
11863
12029
|
}
|
|
11864
12030
|
function formatCallLocation(call) {
|