circle-ir 3.47.0 → 3.49.0

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.
Files changed (53) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +86 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  5. package/dist/analysis/constant-propagation/index.js +16 -6
  6. package/dist/analysis/constant-propagation/index.js.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.d.ts +7 -0
  8. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  9. package/dist/analysis/constant-propagation/propagator.js +81 -41
  10. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  11. package/dist/analysis/html/html-attribute-security-pass.js +14 -9
  12. package/dist/analysis/html/html-attribute-security-pass.js.map +1 -1
  13. package/dist/analysis/html/html-extractor.d.ts.map +1 -1
  14. package/dist/analysis/html/html-extractor.js +16 -11
  15. package/dist/analysis/html/html-extractor.js.map +1 -1
  16. package/dist/analysis/passes/insecure-cookie-pass.d.ts +53 -0
  17. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -0
  18. package/dist/analysis/passes/insecure-cookie-pass.js +109 -0
  19. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -0
  20. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  21. package/dist/analysis/passes/interprocedural-pass.js +7 -0
  22. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  23. package/dist/analysis/passes/language-sources-pass.d.ts +14 -0
  24. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.js +50 -0
  26. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  27. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/sink-filter-pass.js +21 -2
  29. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  30. package/dist/analysis/passes/taint-propagation-pass.js +94 -3
  31. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  32. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  33. package/dist/analysis/taint-matcher.js +117 -20
  34. package/dist/analysis/taint-matcher.js.map +1 -1
  35. package/dist/analyzer.d.ts.map +1 -1
  36. package/dist/analyzer.js +3 -0
  37. package/dist/analyzer.js.map +1 -1
  38. package/dist/browser/circle-ir.js +453 -99
  39. package/dist/core/circle-ir-core.cjs +251 -64
  40. package/dist/core/circle-ir-core.js +251 -64
  41. package/dist/core/extractors/types.js +85 -2
  42. package/dist/core/extractors/types.js.map +1 -1
  43. package/dist/core/parser.d.ts +10 -0
  44. package/dist/core/parser.d.ts.map +1 -1
  45. package/dist/core/parser.js +20 -5
  46. package/dist/core/parser.js.map +1 -1
  47. package/dist/languages/plugins/base.d.ts.map +1 -1
  48. package/dist/languages/plugins/base.js +15 -11
  49. package/dist/languages/plugins/base.js.map +1 -1
  50. package/dist/languages/plugins/java.d.ts.map +1 -1
  51. package/dist/languages/plugins/java.js +8 -4
  52. package/dist/languages/plugins/java.js.map +1 -1
  53. package/package.json +1 -1
@@ -4086,11 +4086,13 @@ async function parse(code, language) {
4086
4086
  return tree;
4087
4087
  }
4088
4088
  function walkTree(node, visitor) {
4089
- visitor(node);
4090
- for (let i2 = 0; i2 < node.childCount; i2++) {
4091
- const child = node.child(i2);
4092
- if (child) {
4093
- walkTree(child, visitor);
4089
+ const stack = [node];
4090
+ while (stack.length > 0) {
4091
+ const current = stack.pop();
4092
+ visitor(current);
4093
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
4094
+ const child = current.child(i2);
4095
+ if (child) stack.push(child);
4094
4096
  }
4095
4097
  }
4096
4098
  }
@@ -4730,14 +4732,46 @@ function extractJSClassInfo(node) {
4730
4732
  end_line: node.endPosition.row + 1
4731
4733
  };
4732
4734
  }
4735
+ function extractDecoratorName(node) {
4736
+ const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
4737
+ if (!child) return null;
4738
+ if (child.type === "identifier") return getNodeText(child);
4739
+ if (child.type === "call_expression") {
4740
+ const fn = child.childForFieldName("function");
4741
+ if (fn) {
4742
+ if (fn.type === "identifier") return getNodeText(fn);
4743
+ if (fn.type === "member_expression") {
4744
+ const propNode = fn.childForFieldName("property");
4745
+ if (propNode) return getNodeText(propNode);
4746
+ }
4747
+ }
4748
+ }
4749
+ if (child.type === "member_expression") {
4750
+ const propNode = child.childForFieldName("property");
4751
+ if (propNode) return getNodeText(propNode);
4752
+ }
4753
+ return null;
4754
+ }
4733
4755
  function extractJSMethods(body2) {
4734
4756
  const methods = [];
4757
+ let pendingDecorators = [];
4735
4758
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4736
4759
  const child = body2.child(i2);
4737
4760
  if (!child) continue;
4761
+ if (child.type === "decorator") {
4762
+ const name2 = extractDecoratorName(child);
4763
+ if (name2) pendingDecorators.push(name2);
4764
+ continue;
4765
+ }
4766
+ if (child.type === "comment") continue;
4738
4767
  if (child.type === "method_definition") {
4739
- methods.push(extractJSMethodInfo(child));
4768
+ const m = extractJSMethodInfo(child);
4769
+ if (pendingDecorators.length > 0) {
4770
+ m.annotations = pendingDecorators;
4771
+ }
4772
+ methods.push(m);
4740
4773
  }
4774
+ pendingDecorators = [];
4741
4775
  }
4742
4776
  return methods;
4743
4777
  }
@@ -4899,10 +4933,18 @@ function extractJSParameters(params) {
4899
4933
  if (typeNode) {
4900
4934
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4901
4935
  }
4936
+ const decorators = [];
4937
+ for (let j = 0; j < child.childCount; j++) {
4938
+ const c = child.child(j);
4939
+ if (c && c.type === "decorator") {
4940
+ const name2 = extractDecoratorName(c);
4941
+ if (name2) decorators.push(name2);
4942
+ }
4943
+ }
4902
4944
  parameters.push({
4903
4945
  name: paramName,
4904
4946
  type: paramType,
4905
- annotations: [],
4947
+ annotations: decorators,
4906
4948
  line: child.startPosition.row + 1
4907
4949
  });
4908
4950
  }
@@ -10949,6 +10991,26 @@ var DEFAULT_SINKS = [
10949
10991
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
10950
10992
  // Sandbox/script security
10951
10993
  { method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10994
+ // Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
10995
+ // Issue #44: log.info/warn/error/debug emit the message argument and any
10996
+ // {} format arguments to the log stream. Untrusted input forwarded into
10997
+ // these calls allows log forging (newline injection) and downstream log
10998
+ // analyzer pollution. Scoped to `java` so the generic method names don't
10999
+ // collide with JS console / Python logger entries below.
11000
+ { method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11001
+ { method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11002
+ { method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11003
+ { method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11004
+ { method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11005
+ // java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
11006
+ // Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
11007
+ { method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11008
+ { method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11009
+ { method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11010
+ { method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11011
+ { method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11012
+ { method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11013
+ { method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
10952
11014
  // =========================================================================
10953
11015
  // Node.js/Express Sinks
10954
11016
  // =========================================================================
@@ -11001,13 +11063,47 @@ var DEFAULT_SINKS = [
11001
11063
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11002
11064
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11003
11065
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11004
- // Node.js NoSQL Injection (MongoDB)
11066
+ // Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
11067
+ // Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
11068
+ // fluent chains (mongoose.connection.db.collection('x').find({...})) and
11069
+ // Model.find calls because the call-site receiver type does not resolve
11070
+ // to `Collection`. Add classless+language-scoped entries for the
11071
+ // MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
11072
+ // mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
11073
+ // to avoid colliding with Array.prototype.find.
11005
11074
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11006
11075
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11007
11076
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11008
11077
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11009
11078
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11010
11079
  { method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11080
+ // Mongoose Model/Query class entries — Model.find/findOne/etc.
11081
+ { method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11082
+ { method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11083
+ { method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11084
+ { method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11085
+ { method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11086
+ { method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11087
+ { method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11088
+ { method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11089
+ { method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11090
+ { method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11091
+ { method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11092
+ { method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11093
+ // Mongoose Query class entries — chain methods returning Query
11094
+ { method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11095
+ { method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11096
+ // Classless MongoDB-specific method names (rare outside MongoDB APIs) —
11097
+ // language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
11098
+ { method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11099
+ { method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11100
+ { method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11101
+ { method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11102
+ { method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11103
+ { method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11104
+ { method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11105
+ { method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11106
+ { method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11011
11107
  // Node.js SSRF (HTTP clients)
11012
11108
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11013
11109
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11029,6 +11125,24 @@ var DEFAULT_SINKS = [
11029
11125
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11030
11126
  // node-fetch
11031
11127
  { method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11128
+ // Node.js / JavaScript Log Injection (console.*) — CWE-117
11129
+ // Issue #44: console.log/warn/error/info with tainted template literals
11130
+ // allow log forging (newline-injection) and downstream log analyzer
11131
+ // pollution. Scoped to JS/TS so the bare class `console` doesn't collide
11132
+ // with Python `console` module or Java identifiers.
11133
+ { method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11134
+ { method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11135
+ { method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11136
+ { method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11137
+ { method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11138
+ { method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11139
+ // Node.js / Express Open Redirect — CWE-601
11140
+ // Issue #46: `res.redirect(req.query.next)` did not fire because the
11141
+ // legacy `class: 'Response'` constraint depended on receiver type
11142
+ // resolution of the Express `res` parameter. Mirror Python's classless
11143
+ // pattern with a language-scoped classless entry. The method name
11144
+ // `redirect` is rare outside HTTP frameworks so the FP risk is low.
11145
+ { method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11032
11146
  // =========================================================================
11033
11147
  // Python Sinks
11034
11148
  // =========================================================================
@@ -11070,7 +11184,12 @@ var DEFAULT_SINKS = [
11070
11184
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11071
11185
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11072
11186
  // Python XSS / SSTI
11073
- { method: "render_template_string", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11187
+ // Issue #54: Flask's `render_template_string(template_str)` with an
11188
+ // attacker-controlled template string is Server-Side Template Injection
11189
+ // (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
11190
+ // (CWE-94) with critical severity to match `jinja2.Template().render()`
11191
+ // and `Template.from_string()` entries above.
11192
+ { method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11074
11193
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11075
11194
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11076
11195
  // Python SSRF
@@ -11433,6 +11552,13 @@ var DEFAULT_SANITIZERS = [
11433
11552
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
11434
11553
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
11435
11554
  { method: "normpath", class: "os.path", removes: ["path_traversal"] },
11555
+ // Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
11556
+ // functions (analogous to Java File.getCanonicalPath). Register on both
11557
+ // `os.path` and the bare `path` receiver to cover `import os.path as path`.
11558
+ { method: "realpath", class: "os.path", removes: ["path_traversal"] },
11559
+ { method: "abspath", class: "os.path", removes: ["path_traversal"] },
11560
+ { method: "realpath", class: "path", removes: ["path_traversal"] },
11561
+ { method: "abspath", class: "path", removes: ["path_traversal"] },
11436
11562
  // Python Type coercion
11437
11563
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
11438
11564
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -11494,7 +11620,7 @@ var PYTHON_TAINTED_PATTERNS = [
11494
11620
  ];
11495
11621
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
11496
11622
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
11497
- const sources = findSources(calls, types, config.sources, sourceLines);
11623
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
11498
11624
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
11499
11625
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
11500
11626
  return { sources, sinks, sanitizers };
@@ -11512,7 +11638,7 @@ function attachSourceLineCode(sources, sinks, code) {
11512
11638
  }
11513
11639
  }
11514
11640
  }
11515
- function findSources(calls, types, patterns, sourceLines) {
11641
+ function findSources(calls, types, patterns, sourceLines, language) {
11516
11642
  const sources = [];
11517
11643
  for (const call of calls) {
11518
11644
  for (const pattern of patterns) {
@@ -11565,23 +11691,29 @@ function findSources(calls, types, patterns, sourceLines) {
11565
11691
  }
11566
11692
  }
11567
11693
  }
11568
- const RUST_EXTRACTOR_TYPES = /^(?:Json|Form|Query|Path|Extension|Multipart)(?:<|$)|^(?:Body|Bytes)$/;
11694
+ const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
11569
11695
  for (const type of types) {
11570
11696
  for (const method of type.methods) {
11571
11697
  for (const param of method.parameters) {
11572
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
11573
- const paramLine = param.line ?? method.start_line;
11574
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
11575
- if (!alreadyExists) {
11576
- sources.push({
11577
- type: "http_body",
11578
- location: `${param.type} ${param.name} in ${method.name}`,
11579
- severity: "high",
11580
- line: paramLine,
11581
- confidence: 1
11582
- });
11583
- }
11584
- }
11698
+ if (!param.type) continue;
11699
+ const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
11700
+ if (!kindMatch) continue;
11701
+ const kind = kindMatch[1];
11702
+ if (kind === "Extension") continue;
11703
+ const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
11704
+ const paramLine = param.line ?? method.start_line;
11705
+ const alreadyExists = sources.some(
11706
+ (s) => s.line === paramLine && s.variable === param.name
11707
+ );
11708
+ if (alreadyExists) continue;
11709
+ sources.push({
11710
+ type: sourceType,
11711
+ location: `${param.type} ${param.name} in ${method.name}`,
11712
+ severity: "high",
11713
+ line: paramLine,
11714
+ confidence: 1,
11715
+ variable: param.name
11716
+ });
11585
11717
  }
11586
11718
  }
11587
11719
  }
@@ -11663,6 +11795,15 @@ function findSources(calls, types, patterns, sourceLines) {
11663
11795
  s.code = sourceLines[s.line - 1]?.trim();
11664
11796
  }
11665
11797
  }
11798
+ if (language === "rust" && sourceLines) {
11799
+ const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
11800
+ for (const s of result) {
11801
+ if (s.variable && s.variable.length > 0) continue;
11802
+ const lineText = sourceLines[s.line - 1] ?? "";
11803
+ const m = LET_BINDING.exec(lineText);
11804
+ if (m) s.variable = m[1];
11805
+ }
11806
+ }
11666
11807
  return result;
11667
11808
  }
11668
11809
  function isInterproceduralTaintableType(typeName) {
@@ -11768,6 +11909,20 @@ function isParameterizedQueryCall(call, pattern) {
11768
11909
  }
11769
11910
  return false;
11770
11911
  }
11912
+ function isSafePythonSubprocessCall(call, pattern, language) {
11913
+ if (language !== "python") return false;
11914
+ if (pattern.type !== "command_injection") return false;
11915
+ if (pattern.class !== "subprocess") return false;
11916
+ const arg0 = call.arguments.find((a) => a.position === 0);
11917
+ if (!arg0) return false;
11918
+ const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
11919
+ if (!expr0.startsWith("[")) return false;
11920
+ for (const a of call.arguments) {
11921
+ const e = (a.expression ?? "").trim();
11922
+ if (/^shell\s*=\s*True\b/.test(e)) return false;
11923
+ }
11924
+ return true;
11925
+ }
11771
11926
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
11772
11927
  function argIsClassLiteral(call, position) {
11773
11928
  const arg = call.arguments.find((a) => a.position === position);
@@ -11784,6 +11939,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
11784
11939
  if (isParameterizedQueryCall(call, pattern)) {
11785
11940
  continue;
11786
11941
  }
11942
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
11943
+ continue;
11944
+ }
11787
11945
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
11788
11946
  continue;
11789
11947
  }
@@ -12186,7 +12344,12 @@ function receiverMightBeClass(receiver, className) {
12186
12344
  "controller",
12187
12345
  "task",
12188
12346
  "thread",
12189
- "job"
12347
+ "job",
12348
+ // Short Python DB abbreviation; would otherwise prefix-match obscure XSS
12349
+ // sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
12350
+ // via the CamelCase word prefix heuristic and produce an xss FP on every
12351
+ // `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
12352
+ "cur"
12190
12353
  ]);
12191
12354
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12192
12355
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12196,7 +12359,9 @@ function receiverMightBeClass(receiver, className) {
12196
12359
  }
12197
12360
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12198
12361
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12199
- return true;
12362
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
12363
+ return true;
12364
+ }
12200
12365
  }
12201
12366
  }
12202
12367
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12219,6 +12384,9 @@ function receiverMightBeClass(receiver, className) {
12219
12384
  ps: ["PreparedStatement"],
12220
12385
  rs: ["ResultSet"],
12221
12386
  template: ["JdbcTemplate"],
12387
+ cur: ["Cursor"],
12388
+ // Python DB-API cursor — see ambiguousIdentifiers note
12389
+ cursor: ["Cursor"],
12222
12390
  // I/O
12223
12391
  writer: ["PrintWriter"],
12224
12392
  out: ["PrintWriter", "OutputStream"],
@@ -13906,8 +14074,10 @@ var ConstantPropagator = class _ConstantPropagator {
13906
14074
  * These are variables declared directly in the class body, not inside methods.
13907
14075
  */
13908
14076
  collectClassFields(root) {
13909
- const traverse = (n, inClass, inMethod) => {
13910
- if (!n) return;
14077
+ const stack = [root];
14078
+ while (stack.length > 0) {
14079
+ const n = stack.pop();
14080
+ if (!n) continue;
13911
14081
  if (n.type === "class_body") {
13912
14082
  for (const child of n.children) {
13913
14083
  if (child.type === "field_declaration") {
@@ -13921,32 +14091,28 @@ var ConstantPropagator = class _ConstantPropagator {
13921
14091
  }
13922
14092
  }
13923
14093
  }
13924
- if (child.type === "method_declaration" || child.type === "constructor_declaration") {
13925
- traverse(child, true, true);
13926
- } else {
13927
- traverse(child, true, false);
13928
- }
14094
+ stack.push(child);
13929
14095
  }
13930
- return;
14096
+ continue;
13931
14097
  }
13932
14098
  for (const child of n.children) {
13933
- traverse(child, inClass, inMethod);
14099
+ stack.push(child);
13934
14100
  }
13935
- };
13936
- traverse(root, false, false);
14101
+ }
13937
14102
  }
13938
14103
  findAllMethods(node) {
13939
14104
  const methods = [];
13940
- const traverse = (n) => {
13941
- if (!n) return;
14105
+ const stack = [node];
14106
+ while (stack.length > 0) {
14107
+ const n = stack.pop();
14108
+ if (!n) continue;
13942
14109
  if (n.type === "method_declaration" || n.type === "function_declaration") {
13943
14110
  methods.push(n);
13944
14111
  }
13945
14112
  for (const child of n.children) {
13946
- if (child) traverse(child);
14113
+ if (child) stack.push(child);
13947
14114
  }
13948
- };
13949
- traverse(node);
14115
+ }
13950
14116
  return methods;
13951
14117
  }
13952
14118
  getMethodName(method) {
@@ -13997,9 +14163,24 @@ var ConstantPropagator = class _ConstantPropagator {
13997
14163
  // AST Visitor
13998
14164
  // ===========================================================================
13999
14165
  visit(node) {
14166
+ const stack = [node];
14167
+ while (stack.length > 0) {
14168
+ const current = stack.pop();
14169
+ if (this.visitOne(current)) continue;
14170
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
14171
+ stack.push(current.children[i2]);
14172
+ }
14173
+ }
14174
+ }
14175
+ /**
14176
+ * Visit a single node. Returns true if the handler already descended into
14177
+ * children (and the caller should NOT push them), false to fall through to
14178
+ * the default pre-order descent.
14179
+ */
14180
+ visitOne(node) {
14000
14181
  const line = getNodeLine(node);
14001
14182
  if (this.unreachableLines.has(line)) {
14002
- return;
14183
+ return true;
14003
14184
  }
14004
14185
  if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
14005
14186
  this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
@@ -14008,43 +14189,41 @@ var ConstantPropagator = class _ConstantPropagator {
14008
14189
  case "method_declaration":
14009
14190
  case "constructor_declaration":
14010
14191
  this.handleMethodDeclaration(node);
14011
- return;
14192
+ return true;
14012
14193
  // Don't visit children directly, handleMethodDeclaration does it
14013
14194
  case "local_variable_declaration":
14014
14195
  this.handleVariableDeclaration(node);
14015
- break;
14196
+ return false;
14016
14197
  case "assignment_expression":
14017
14198
  this.handleAssignment(node);
14018
- break;
14199
+ return false;
14019
14200
  case "update_expression":
14020
14201
  this.handleUpdateExpression(node);
14021
- break;
14202
+ return false;
14022
14203
  case "if_statement":
14023
14204
  this.handleIfStatement(node);
14024
- return;
14205
+ return true;
14025
14206
  case "switch_expression":
14026
14207
  case "switch_statement":
14027
14208
  this.handleSwitch(node);
14028
- return;
14209
+ return true;
14029
14210
  case "ternary_expression":
14030
14211
  this.handleTernary(node);
14031
- break;
14212
+ return false;
14032
14213
  case "expression_statement":
14033
14214
  this.handleExpressionStatement(node);
14034
- break;
14215
+ return false;
14035
14216
  case "for_statement":
14036
14217
  case "enhanced_for_statement":
14037
14218
  case "while_statement":
14038
14219
  case "do_statement":
14039
14220
  this.handleLoopStatement(node);
14040
- return;
14221
+ return true;
14041
14222
  case "synchronized_statement":
14042
14223
  this.handleSynchronizedStatement(node);
14043
- return;
14224
+ return true;
14044
14225
  default:
14045
- for (const child of node.children) {
14046
- this.visit(child);
14047
- }
14226
+ return false;
14048
14227
  }
14049
14228
  }
14050
14229
  /**
@@ -14812,6 +14991,19 @@ var ConstantPropagator = class _ConstantPropagator {
14812
14991
  return null;
14813
14992
  }
14814
14993
  isTaintedExpression(node) {
14994
+ const stack = [node];
14995
+ while (stack.length > 0) {
14996
+ const current = stack.pop();
14997
+ const result = this.isTaintedExpressionStep(current);
14998
+ if (result === true) return true;
14999
+ if (result === false) continue;
15000
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
15001
+ stack.push(current.children[i2]);
15002
+ }
15003
+ }
15004
+ return false;
15005
+ }
15006
+ isTaintedExpressionStep(node) {
14815
15007
  const text = getNodeText2(node, this.source);
14816
15008
  if (node.type === "method_invocation") {
14817
15009
  const nameNode = node.childForFieldName("name");
@@ -15071,12 +15263,7 @@ var ConstantPropagator = class _ConstantPropagator {
15071
15263
  }
15072
15264
  return isTainted;
15073
15265
  }
15074
- for (const child of node.children) {
15075
- if (this.isTaintedExpression(child)) {
15076
- return true;
15077
- }
15078
- }
15079
- return false;
15266
+ return void 0;
15080
15267
  }
15081
15268
  checkCollectionTaint(node) {
15082
15269
  const objectNode = node.childForFieldName("object");
@@ -15247,7 +15434,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
15247
15434
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
15248
15435
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
15249
15436
  }
15250
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
15437
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
15251
15438
  return { isFalsePositive: true, reason: "variable_not_tainted" };
15252
15439
  }
15253
15440
  return { isFalsePositive: false, reason: null };
@@ -560,18 +560,90 @@ function extractJSClassInfo(node) {
560
560
  end_line: node.endPosition.row + 1,
561
561
  };
562
562
  }
563
+ /**
564
+ * Extract the name of a `decorator` node (TypeScript / NestJS / Angular etc).
565
+ *
566
+ * The grammar permits three shapes:
567
+ * @Foo → decorator > identifier
568
+ * @Foo('bar') → decorator > call_expression > identifier
569
+ * @ns.Foo → decorator > member_expression (use .property)
570
+ * @ns.Foo('bar') → decorator > call_expression > member_expression
571
+ */
572
+ function extractDecoratorName(node) {
573
+ // tree-sitter-typescript stores the decorator expression as namedChild(0)
574
+ const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
575
+ if (!child)
576
+ return null;
577
+ if (child.type === 'identifier')
578
+ return getNodeText(child);
579
+ if (child.type === 'call_expression') {
580
+ const fn = child.childForFieldName('function');
581
+ if (fn) {
582
+ if (fn.type === 'identifier')
583
+ return getNodeText(fn);
584
+ if (fn.type === 'member_expression') {
585
+ const propNode = fn.childForFieldName('property');
586
+ if (propNode)
587
+ return getNodeText(propNode);
588
+ }
589
+ }
590
+ }
591
+ if (child.type === 'member_expression') {
592
+ const propNode = child.childForFieldName('property');
593
+ if (propNode)
594
+ return getNodeText(propNode);
595
+ }
596
+ return null;
597
+ }
563
598
  /**
564
599
  * Extract JavaScript methods from a class body.
600
+ *
601
+ * Method-level decorators (TS / NestJS / Angular) appear as preceding
602
+ * `decorator` siblings of `method_definition` inside `class_body`. We accumulate
603
+ * them as we walk children and attach to the very next `method_definition`.
604
+ *
605
+ * IMPORTANT: `pendingDecorators` is reset on ANY non-decorator class member
606
+ * (field, accessor, abstract signature, …), not just method_definition. A
607
+ * decorator preceding a field like
608
+ *
609
+ * @Inject('USER_REPO') private repo: Repository<User>;
610
+ * @Get('search') async search(...) { ... }
611
+ *
612
+ * belongs to the field, not the method below. Failing to reset after the
613
+ * field_definition would silently transfer `Inject` onto `search.annotations`,
614
+ * polluting the IR consumed by taint-matcher.ts (`matchesAnnotation` against
615
+ * `pattern.method_annotation`).
565
616
  */
566
617
  function extractJSMethods(body) {
567
618
  const methods = [];
619
+ let pendingDecorators = [];
568
620
  for (let i = 0; i < body.childCount; i++) {
569
621
  const child = body.child(i);
570
622
  if (!child)
571
623
  continue;
624
+ if (child.type === 'decorator') {
625
+ const name = extractDecoratorName(child);
626
+ if (name)
627
+ pendingDecorators.push(name);
628
+ continue;
629
+ }
630
+ // Tree-sitter emits comments as anonymous children of `class_body`.
631
+ // A `// note` line between a decorator and its method must NOT clear
632
+ // the pending decorator list — skip without resetting.
633
+ if (child.type === 'comment')
634
+ continue;
572
635
  if (child.type === 'method_definition') {
573
- methods.push(extractJSMethodInfo(child));
636
+ const m = extractJSMethodInfo(child);
637
+ if (pendingDecorators.length > 0) {
638
+ m.annotations = pendingDecorators;
639
+ }
640
+ methods.push(m);
574
641
  }
642
+ // Reset pending decorators on ANY non-decorator, non-comment child
643
+ // (method, field, accessor, abstract_method_signature,
644
+ // public_field_definition, …). Decorators only ever apply to the
645
+ // immediately-following member.
646
+ pendingDecorators = [];
575
647
  }
576
648
  return methods;
577
649
  }
@@ -776,10 +848,21 @@ function extractJSParameters(params) {
776
848
  // type_annotation includes the leading ':'; strip it for storage parity with other languages
777
849
  paramType = getNodeText(typeNode).replace(/^:\s*/, '');
778
850
  }
851
+ // Parameter decorators (NestJS @Query/@Param/@Body, Angular @Inject, etc.)
852
+ // appear as `decorator` children of the required_parameter node itself.
853
+ const decorators = [];
854
+ for (let j = 0; j < child.childCount; j++) {
855
+ const c = child.child(j);
856
+ if (c && c.type === 'decorator') {
857
+ const name = extractDecoratorName(c);
858
+ if (name)
859
+ decorators.push(name);
860
+ }
861
+ }
779
862
  parameters.push({
780
863
  name: paramName,
781
864
  type: paramType,
782
- annotations: [],
865
+ annotations: decorators,
783
866
  line: child.startPosition.row + 1,
784
867
  });
785
868
  }