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.
@@ -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 (leads to RCE via gadget chains)
9987
- { method: "fromXML", class: "XStream", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
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: "query", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0] },
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
- if (call.arguments.length < 2) return false;
11473
- const secondArg = call.arguments.find((a) => a.position === 1);
11474
- if (!secondArg) return false;
11475
- if (secondArg.expression) {
11476
- const expr = secondArg.expression.trim();
11477
- if (expr.startsWith("[")) {
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, _sinkType) {
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
- return true;
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 (lowerClass.startsWith(lowerReceiver.substring(0, 3))) {
11718
- return true;
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 (leads to RCE via gadget chains)
10100
- { method: "fromXML", class: "XStream", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
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: "query", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0] },
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
- if (call.arguments.length < 2) return false;
11499
- const secondArg = call.arguments.find((a) => a.position === 1);
11500
- if (!secondArg) return false;
11501
- if (secondArg.expression) {
11502
- const expr = secondArg.expression.trim();
11503
- if (expr.startsWith("[")) {
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, _sinkType) {
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
- return true;
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 (lowerClass.startsWith(lowerReceiver.substring(0, 3))) {
11744
- return true;
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) {