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
@@ -4152,11 +4152,13 @@ async function parse(code, language) {
4152
4152
  return tree;
4153
4153
  }
4154
4154
  function walkTree(node, visitor) {
4155
- visitor(node);
4156
- for (let i2 = 0; i2 < node.childCount; i2++) {
4157
- const child = node.child(i2);
4158
- if (child) {
4159
- walkTree(child, visitor);
4155
+ const stack = [node];
4156
+ while (stack.length > 0) {
4157
+ const current = stack.pop();
4158
+ visitor(current);
4159
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
4160
+ const child = current.child(i2);
4161
+ if (child) stack.push(child);
4160
4162
  }
4161
4163
  }
4162
4164
  }
@@ -4796,14 +4798,46 @@ function extractJSClassInfo(node) {
4796
4798
  end_line: node.endPosition.row + 1
4797
4799
  };
4798
4800
  }
4801
+ function extractDecoratorName(node) {
4802
+ const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
4803
+ if (!child) return null;
4804
+ if (child.type === "identifier") return getNodeText(child);
4805
+ if (child.type === "call_expression") {
4806
+ const fn = child.childForFieldName("function");
4807
+ if (fn) {
4808
+ if (fn.type === "identifier") return getNodeText(fn);
4809
+ if (fn.type === "member_expression") {
4810
+ const propNode = fn.childForFieldName("property");
4811
+ if (propNode) return getNodeText(propNode);
4812
+ }
4813
+ }
4814
+ }
4815
+ if (child.type === "member_expression") {
4816
+ const propNode = child.childForFieldName("property");
4817
+ if (propNode) return getNodeText(propNode);
4818
+ }
4819
+ return null;
4820
+ }
4799
4821
  function extractJSMethods(body2) {
4800
4822
  const methods = [];
4823
+ let pendingDecorators = [];
4801
4824
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4802
4825
  const child = body2.child(i2);
4803
4826
  if (!child) continue;
4827
+ if (child.type === "decorator") {
4828
+ const name2 = extractDecoratorName(child);
4829
+ if (name2) pendingDecorators.push(name2);
4830
+ continue;
4831
+ }
4832
+ if (child.type === "comment") continue;
4804
4833
  if (child.type === "method_definition") {
4805
- methods.push(extractJSMethodInfo(child));
4834
+ const m = extractJSMethodInfo(child);
4835
+ if (pendingDecorators.length > 0) {
4836
+ m.annotations = pendingDecorators;
4837
+ }
4838
+ methods.push(m);
4806
4839
  }
4840
+ pendingDecorators = [];
4807
4841
  }
4808
4842
  return methods;
4809
4843
  }
@@ -4965,10 +4999,18 @@ function extractJSParameters(params) {
4965
4999
  if (typeNode) {
4966
5000
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4967
5001
  }
5002
+ const decorators = [];
5003
+ for (let j = 0; j < child.childCount; j++) {
5004
+ const c = child.child(j);
5005
+ if (c && c.type === "decorator") {
5006
+ const name2 = extractDecoratorName(c);
5007
+ if (name2) decorators.push(name2);
5008
+ }
5009
+ }
4968
5010
  parameters.push({
4969
5011
  name: paramName,
4970
5012
  type: paramType,
4971
- annotations: [],
5013
+ annotations: decorators,
4972
5014
  line: child.startPosition.row + 1
4973
5015
  });
4974
5016
  }
@@ -11015,6 +11057,26 @@ var DEFAULT_SINKS = [
11015
11057
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
11016
11058
  // Sandbox/script security
11017
11059
  { method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11060
+ // Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
11061
+ // Issue #44: log.info/warn/error/debug emit the message argument and any
11062
+ // {} format arguments to the log stream. Untrusted input forwarded into
11063
+ // these calls allows log forging (newline injection) and downstream log
11064
+ // analyzer pollution. Scoped to `java` so the generic method names don't
11065
+ // collide with JS console / Python logger entries below.
11066
+ { method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11067
+ { method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11068
+ { method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11069
+ { method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11070
+ { method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11071
+ // java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
11072
+ // Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
11073
+ { method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11074
+ { method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11075
+ { method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11076
+ { method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11077
+ { method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11078
+ { method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11079
+ { method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
11018
11080
  // =========================================================================
11019
11081
  // Node.js/Express Sinks
11020
11082
  // =========================================================================
@@ -11067,13 +11129,47 @@ var DEFAULT_SINKS = [
11067
11129
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11068
11130
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11069
11131
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11070
- // Node.js NoSQL Injection (MongoDB)
11132
+ // Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
11133
+ // Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
11134
+ // fluent chains (mongoose.connection.db.collection('x').find({...})) and
11135
+ // Model.find calls because the call-site receiver type does not resolve
11136
+ // to `Collection`. Add classless+language-scoped entries for the
11137
+ // MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
11138
+ // mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
11139
+ // to avoid colliding with Array.prototype.find.
11071
11140
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11072
11141
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11073
11142
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11074
11143
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11075
11144
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11076
11145
  { method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11146
+ // Mongoose Model/Query class entries — Model.find/findOne/etc.
11147
+ { method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11148
+ { method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11149
+ { method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11150
+ { method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11151
+ { method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11152
+ { method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11153
+ { method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11154
+ { method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11155
+ { method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11156
+ { method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11157
+ { method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11158
+ { method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11159
+ // Mongoose Query class entries — chain methods returning Query
11160
+ { method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11161
+ { method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11162
+ // Classless MongoDB-specific method names (rare outside MongoDB APIs) —
11163
+ // language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
11164
+ { method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11165
+ { method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11166
+ { method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11167
+ { method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11168
+ { method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11169
+ { method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11170
+ { method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11171
+ { method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11172
+ { method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11077
11173
  // Node.js SSRF (HTTP clients)
11078
11174
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11079
11175
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11095,6 +11191,24 @@ var DEFAULT_SINKS = [
11095
11191
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11096
11192
  // node-fetch
11097
11193
  { method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11194
+ // Node.js / JavaScript Log Injection (console.*) — CWE-117
11195
+ // Issue #44: console.log/warn/error/info with tainted template literals
11196
+ // allow log forging (newline-injection) and downstream log analyzer
11197
+ // pollution. Scoped to JS/TS so the bare class `console` doesn't collide
11198
+ // with Python `console` module or Java identifiers.
11199
+ { method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11200
+ { method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11201
+ { method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11202
+ { method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11203
+ { method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11204
+ { method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11205
+ // Node.js / Express Open Redirect — CWE-601
11206
+ // Issue #46: `res.redirect(req.query.next)` did not fire because the
11207
+ // legacy `class: 'Response'` constraint depended on receiver type
11208
+ // resolution of the Express `res` parameter. Mirror Python's classless
11209
+ // pattern with a language-scoped classless entry. The method name
11210
+ // `redirect` is rare outside HTTP frameworks so the FP risk is low.
11211
+ { method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11098
11212
  // =========================================================================
11099
11213
  // Python Sinks
11100
11214
  // =========================================================================
@@ -11136,7 +11250,12 @@ var DEFAULT_SINKS = [
11136
11250
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11137
11251
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11138
11252
  // Python XSS / SSTI
11139
- { method: "render_template_string", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11253
+ // Issue #54: Flask's `render_template_string(template_str)` with an
11254
+ // attacker-controlled template string is Server-Side Template Injection
11255
+ // (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
11256
+ // (CWE-94) with critical severity to match `jinja2.Template().render()`
11257
+ // and `Template.from_string()` entries above.
11258
+ { method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11140
11259
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11141
11260
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11142
11261
  // Python SSRF
@@ -11499,6 +11618,13 @@ var DEFAULT_SANITIZERS = [
11499
11618
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
11500
11619
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
11501
11620
  { method: "normpath", class: "os.path", removes: ["path_traversal"] },
11621
+ // Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
11622
+ // functions (analogous to Java File.getCanonicalPath). Register on both
11623
+ // `os.path` and the bare `path` receiver to cover `import os.path as path`.
11624
+ { method: "realpath", class: "os.path", removes: ["path_traversal"] },
11625
+ { method: "abspath", class: "os.path", removes: ["path_traversal"] },
11626
+ { method: "realpath", class: "path", removes: ["path_traversal"] },
11627
+ { method: "abspath", class: "path", removes: ["path_traversal"] },
11502
11628
  // Python Type coercion
11503
11629
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
11504
11630
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -11560,7 +11686,7 @@ var PYTHON_TAINTED_PATTERNS = [
11560
11686
  ];
11561
11687
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
11562
11688
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
11563
- const sources = findSources(calls, types, config.sources, sourceLines);
11689
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
11564
11690
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
11565
11691
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
11566
11692
  return { sources, sinks, sanitizers };
@@ -11578,7 +11704,7 @@ function attachSourceLineCode(sources, sinks, code) {
11578
11704
  }
11579
11705
  }
11580
11706
  }
11581
- function findSources(calls, types, patterns, sourceLines) {
11707
+ function findSources(calls, types, patterns, sourceLines, language) {
11582
11708
  const sources = [];
11583
11709
  for (const call of calls) {
11584
11710
  for (const pattern of patterns) {
@@ -11631,23 +11757,29 @@ function findSources(calls, types, patterns, sourceLines) {
11631
11757
  }
11632
11758
  }
11633
11759
  }
11634
- const RUST_EXTRACTOR_TYPES = /^(?:Json|Form|Query|Path|Extension|Multipart)(?:<|$)|^(?:Body|Bytes)$/;
11760
+ const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
11635
11761
  for (const type of types) {
11636
11762
  for (const method of type.methods) {
11637
11763
  for (const param of method.parameters) {
11638
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
11639
- const paramLine = param.line ?? method.start_line;
11640
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
11641
- if (!alreadyExists) {
11642
- sources.push({
11643
- type: "http_body",
11644
- location: `${param.type} ${param.name} in ${method.name}`,
11645
- severity: "high",
11646
- line: paramLine,
11647
- confidence: 1
11648
- });
11649
- }
11650
- }
11764
+ if (!param.type) continue;
11765
+ const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
11766
+ if (!kindMatch) continue;
11767
+ const kind = kindMatch[1];
11768
+ if (kind === "Extension") continue;
11769
+ const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
11770
+ const paramLine = param.line ?? method.start_line;
11771
+ const alreadyExists = sources.some(
11772
+ (s) => s.line === paramLine && s.variable === param.name
11773
+ );
11774
+ if (alreadyExists) continue;
11775
+ sources.push({
11776
+ type: sourceType,
11777
+ location: `${param.type} ${param.name} in ${method.name}`,
11778
+ severity: "high",
11779
+ line: paramLine,
11780
+ confidence: 1,
11781
+ variable: param.name
11782
+ });
11651
11783
  }
11652
11784
  }
11653
11785
  }
@@ -11729,6 +11861,15 @@ function findSources(calls, types, patterns, sourceLines) {
11729
11861
  s.code = sourceLines[s.line - 1]?.trim();
11730
11862
  }
11731
11863
  }
11864
+ if (language === "rust" && sourceLines) {
11865
+ const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
11866
+ for (const s of result) {
11867
+ if (s.variable && s.variable.length > 0) continue;
11868
+ const lineText = sourceLines[s.line - 1] ?? "";
11869
+ const m = LET_BINDING.exec(lineText);
11870
+ if (m) s.variable = m[1];
11871
+ }
11872
+ }
11732
11873
  return result;
11733
11874
  }
11734
11875
  function isInterproceduralTaintableType(typeName) {
@@ -11834,6 +11975,20 @@ function isParameterizedQueryCall(call, pattern) {
11834
11975
  }
11835
11976
  return false;
11836
11977
  }
11978
+ function isSafePythonSubprocessCall(call, pattern, language) {
11979
+ if (language !== "python") return false;
11980
+ if (pattern.type !== "command_injection") return false;
11981
+ if (pattern.class !== "subprocess") return false;
11982
+ const arg0 = call.arguments.find((a) => a.position === 0);
11983
+ if (!arg0) return false;
11984
+ const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
11985
+ if (!expr0.startsWith("[")) return false;
11986
+ for (const a of call.arguments) {
11987
+ const e = (a.expression ?? "").trim();
11988
+ if (/^shell\s*=\s*True\b/.test(e)) return false;
11989
+ }
11990
+ return true;
11991
+ }
11837
11992
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
11838
11993
  function argIsClassLiteral(call, position) {
11839
11994
  const arg = call.arguments.find((a) => a.position === position);
@@ -11850,6 +12005,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
11850
12005
  if (isParameterizedQueryCall(call, pattern)) {
11851
12006
  continue;
11852
12007
  }
12008
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
12009
+ continue;
12010
+ }
11853
12011
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
11854
12012
  continue;
11855
12013
  }
@@ -12252,7 +12410,12 @@ function receiverMightBeClass(receiver, className) {
12252
12410
  "controller",
12253
12411
  "task",
12254
12412
  "thread",
12255
- "job"
12413
+ "job",
12414
+ // Short Python DB abbreviation; would otherwise prefix-match obscure XSS
12415
+ // sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
12416
+ // via the CamelCase word prefix heuristic and produce an xss FP on every
12417
+ // `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
12418
+ "cur"
12256
12419
  ]);
12257
12420
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12258
12421
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12262,7 +12425,9 @@ function receiverMightBeClass(receiver, className) {
12262
12425
  }
12263
12426
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12264
12427
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12265
- return true;
12428
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
12429
+ return true;
12430
+ }
12266
12431
  }
12267
12432
  }
12268
12433
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12285,6 +12450,9 @@ function receiverMightBeClass(receiver, className) {
12285
12450
  ps: ["PreparedStatement"],
12286
12451
  rs: ["ResultSet"],
12287
12452
  template: ["JdbcTemplate"],
12453
+ cur: ["Cursor"],
12454
+ // Python DB-API cursor — see ambiguousIdentifiers note
12455
+ cursor: ["Cursor"],
12288
12456
  // I/O
12289
12457
  writer: ["PrintWriter"],
12290
12458
  out: ["PrintWriter", "OutputStream"],
@@ -13972,8 +14140,10 @@ var ConstantPropagator = class _ConstantPropagator {
13972
14140
  * These are variables declared directly in the class body, not inside methods.
13973
14141
  */
13974
14142
  collectClassFields(root) {
13975
- const traverse = (n, inClass, inMethod) => {
13976
- if (!n) return;
14143
+ const stack = [root];
14144
+ while (stack.length > 0) {
14145
+ const n = stack.pop();
14146
+ if (!n) continue;
13977
14147
  if (n.type === "class_body") {
13978
14148
  for (const child of n.children) {
13979
14149
  if (child.type === "field_declaration") {
@@ -13987,32 +14157,28 @@ var ConstantPropagator = class _ConstantPropagator {
13987
14157
  }
13988
14158
  }
13989
14159
  }
13990
- if (child.type === "method_declaration" || child.type === "constructor_declaration") {
13991
- traverse(child, true, true);
13992
- } else {
13993
- traverse(child, true, false);
13994
- }
14160
+ stack.push(child);
13995
14161
  }
13996
- return;
14162
+ continue;
13997
14163
  }
13998
14164
  for (const child of n.children) {
13999
- traverse(child, inClass, inMethod);
14165
+ stack.push(child);
14000
14166
  }
14001
- };
14002
- traverse(root, false, false);
14167
+ }
14003
14168
  }
14004
14169
  findAllMethods(node) {
14005
14170
  const methods = [];
14006
- const traverse = (n) => {
14007
- if (!n) return;
14171
+ const stack = [node];
14172
+ while (stack.length > 0) {
14173
+ const n = stack.pop();
14174
+ if (!n) continue;
14008
14175
  if (n.type === "method_declaration" || n.type === "function_declaration") {
14009
14176
  methods.push(n);
14010
14177
  }
14011
14178
  for (const child of n.children) {
14012
- if (child) traverse(child);
14179
+ if (child) stack.push(child);
14013
14180
  }
14014
- };
14015
- traverse(node);
14181
+ }
14016
14182
  return methods;
14017
14183
  }
14018
14184
  getMethodName(method) {
@@ -14063,9 +14229,24 @@ var ConstantPropagator = class _ConstantPropagator {
14063
14229
  // AST Visitor
14064
14230
  // ===========================================================================
14065
14231
  visit(node) {
14232
+ const stack = [node];
14233
+ while (stack.length > 0) {
14234
+ const current = stack.pop();
14235
+ if (this.visitOne(current)) continue;
14236
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
14237
+ stack.push(current.children[i2]);
14238
+ }
14239
+ }
14240
+ }
14241
+ /**
14242
+ * Visit a single node. Returns true if the handler already descended into
14243
+ * children (and the caller should NOT push them), false to fall through to
14244
+ * the default pre-order descent.
14245
+ */
14246
+ visitOne(node) {
14066
14247
  const line = getNodeLine(node);
14067
14248
  if (this.unreachableLines.has(line)) {
14068
- return;
14249
+ return true;
14069
14250
  }
14070
14251
  if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
14071
14252
  this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
@@ -14074,43 +14255,41 @@ var ConstantPropagator = class _ConstantPropagator {
14074
14255
  case "method_declaration":
14075
14256
  case "constructor_declaration":
14076
14257
  this.handleMethodDeclaration(node);
14077
- return;
14258
+ return true;
14078
14259
  // Don't visit children directly, handleMethodDeclaration does it
14079
14260
  case "local_variable_declaration":
14080
14261
  this.handleVariableDeclaration(node);
14081
- break;
14262
+ return false;
14082
14263
  case "assignment_expression":
14083
14264
  this.handleAssignment(node);
14084
- break;
14265
+ return false;
14085
14266
  case "update_expression":
14086
14267
  this.handleUpdateExpression(node);
14087
- break;
14268
+ return false;
14088
14269
  case "if_statement":
14089
14270
  this.handleIfStatement(node);
14090
- return;
14271
+ return true;
14091
14272
  case "switch_expression":
14092
14273
  case "switch_statement":
14093
14274
  this.handleSwitch(node);
14094
- return;
14275
+ return true;
14095
14276
  case "ternary_expression":
14096
14277
  this.handleTernary(node);
14097
- break;
14278
+ return false;
14098
14279
  case "expression_statement":
14099
14280
  this.handleExpressionStatement(node);
14100
- break;
14281
+ return false;
14101
14282
  case "for_statement":
14102
14283
  case "enhanced_for_statement":
14103
14284
  case "while_statement":
14104
14285
  case "do_statement":
14105
14286
  this.handleLoopStatement(node);
14106
- return;
14287
+ return true;
14107
14288
  case "synchronized_statement":
14108
14289
  this.handleSynchronizedStatement(node);
14109
- return;
14290
+ return true;
14110
14291
  default:
14111
- for (const child of node.children) {
14112
- this.visit(child);
14113
- }
14292
+ return false;
14114
14293
  }
14115
14294
  }
14116
14295
  /**
@@ -14878,6 +15057,19 @@ var ConstantPropagator = class _ConstantPropagator {
14878
15057
  return null;
14879
15058
  }
14880
15059
  isTaintedExpression(node) {
15060
+ const stack = [node];
15061
+ while (stack.length > 0) {
15062
+ const current = stack.pop();
15063
+ const result = this.isTaintedExpressionStep(current);
15064
+ if (result === true) return true;
15065
+ if (result === false) continue;
15066
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
15067
+ stack.push(current.children[i2]);
15068
+ }
15069
+ }
15070
+ return false;
15071
+ }
15072
+ isTaintedExpressionStep(node) {
14881
15073
  const text = getNodeText2(node, this.source);
14882
15074
  if (node.type === "method_invocation") {
14883
15075
  const nameNode = node.childForFieldName("name");
@@ -15137,12 +15329,7 @@ var ConstantPropagator = class _ConstantPropagator {
15137
15329
  }
15138
15330
  return isTainted;
15139
15331
  }
15140
- for (const child of node.children) {
15141
- if (this.isTaintedExpression(child)) {
15142
- return true;
15143
- }
15144
- }
15145
- return false;
15332
+ return void 0;
15146
15333
  }
15147
15334
  checkCollectionTaint(node) {
15148
15335
  const objectNode = node.childForFieldName("object");
@@ -15313,7 +15500,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
15313
15500
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
15314
15501
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
15315
15502
  }
15316
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
15503
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
15317
15504
  return { isFalsePositive: true, reason: "variable_not_tainted" };
15318
15505
  }
15319
15506
  return { isFalsePositive: false, reason: null };