circle-ir 3.48.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 (34) 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/passes/insecure-cookie-pass.d.ts +53 -0
  8. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -0
  9. package/dist/analysis/passes/insecure-cookie-pass.js +109 -0
  10. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -0
  11. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/interprocedural-pass.js +7 -0
  13. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  14. package/dist/analysis/passes/language-sources-pass.d.ts +14 -0
  15. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  16. package/dist/analysis/passes/language-sources-pass.js +50 -0
  17. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/sink-filter-pass.js +21 -2
  20. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  21. package/dist/analysis/passes/taint-propagation-pass.js +94 -3
  22. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  23. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  24. package/dist/analysis/taint-matcher.js +117 -20
  25. package/dist/analysis/taint-matcher.js.map +1 -1
  26. package/dist/analyzer.d.ts.map +1 -1
  27. package/dist/analyzer.js +3 -0
  28. package/dist/analyzer.js.map +1 -1
  29. package/dist/browser/circle-ir.js +356 -26
  30. package/dist/core/circle-ir-core.cjs +189 -23
  31. package/dist/core/circle-ir-core.js +189 -23
  32. package/dist/core/extractors/types.js +85 -2
  33. package/dist/core/extractors/types.js.map +1 -1
  34. package/package.json +1 -1
@@ -4732,14 +4732,46 @@ function extractJSClassInfo(node) {
4732
4732
  end_line: node.endPosition.row + 1
4733
4733
  };
4734
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
+ }
4735
4755
  function extractJSMethods(body2) {
4736
4756
  const methods = [];
4757
+ let pendingDecorators = [];
4737
4758
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4738
4759
  const child = body2.child(i2);
4739
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;
4740
4767
  if (child.type === "method_definition") {
4741
- methods.push(extractJSMethodInfo(child));
4768
+ const m = extractJSMethodInfo(child);
4769
+ if (pendingDecorators.length > 0) {
4770
+ m.annotations = pendingDecorators;
4771
+ }
4772
+ methods.push(m);
4742
4773
  }
4774
+ pendingDecorators = [];
4743
4775
  }
4744
4776
  return methods;
4745
4777
  }
@@ -4901,10 +4933,18 @@ function extractJSParameters(params) {
4901
4933
  if (typeNode) {
4902
4934
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4903
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
+ }
4904
4944
  parameters.push({
4905
4945
  name: paramName,
4906
4946
  type: paramType,
4907
- annotations: [],
4947
+ annotations: decorators,
4908
4948
  line: child.startPosition.row + 1
4909
4949
  });
4910
4950
  }
@@ -10951,6 +10991,26 @@ var DEFAULT_SINKS = [
10951
10991
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
10952
10992
  // Sandbox/script security
10953
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"] },
10954
11014
  // =========================================================================
10955
11015
  // Node.js/Express Sinks
10956
11016
  // =========================================================================
@@ -11003,13 +11063,47 @@ var DEFAULT_SINKS = [
11003
11063
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11004
11064
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11005
11065
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11006
- // 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.
11007
11074
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11008
11075
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11009
11076
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11010
11077
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11011
11078
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11012
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"] },
11013
11107
  // Node.js SSRF (HTTP clients)
11014
11108
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11015
11109
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11031,6 +11125,24 @@ var DEFAULT_SINKS = [
11031
11125
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11032
11126
  // node-fetch
11033
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"] },
11034
11146
  // =========================================================================
11035
11147
  // Python Sinks
11036
11148
  // =========================================================================
@@ -11072,7 +11184,12 @@ var DEFAULT_SINKS = [
11072
11184
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11073
11185
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11074
11186
  // Python XSS / SSTI
11075
- { 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"] },
11076
11193
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11077
11194
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11078
11195
  // Python SSRF
@@ -11435,6 +11552,13 @@ var DEFAULT_SANITIZERS = [
11435
11552
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
11436
11553
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
11437
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"] },
11438
11562
  // Python Type coercion
11439
11563
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
11440
11564
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -11496,7 +11620,7 @@ var PYTHON_TAINTED_PATTERNS = [
11496
11620
  ];
11497
11621
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
11498
11622
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
11499
- const sources = findSources(calls, types, config.sources, sourceLines);
11623
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
11500
11624
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
11501
11625
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
11502
11626
  return { sources, sinks, sanitizers };
@@ -11514,7 +11638,7 @@ function attachSourceLineCode(sources, sinks, code) {
11514
11638
  }
11515
11639
  }
11516
11640
  }
11517
- function findSources(calls, types, patterns, sourceLines) {
11641
+ function findSources(calls, types, patterns, sourceLines, language) {
11518
11642
  const sources = [];
11519
11643
  for (const call of calls) {
11520
11644
  for (const pattern of patterns) {
@@ -11567,23 +11691,29 @@ function findSources(calls, types, patterns, sourceLines) {
11567
11691
  }
11568
11692
  }
11569
11693
  }
11570
- 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)(?:<|$)/;
11571
11695
  for (const type of types) {
11572
11696
  for (const method of type.methods) {
11573
11697
  for (const param of method.parameters) {
11574
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
11575
- const paramLine = param.line ?? method.start_line;
11576
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
11577
- if (!alreadyExists) {
11578
- sources.push({
11579
- type: "http_body",
11580
- location: `${param.type} ${param.name} in ${method.name}`,
11581
- severity: "high",
11582
- line: paramLine,
11583
- confidence: 1
11584
- });
11585
- }
11586
- }
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
+ });
11587
11717
  }
11588
11718
  }
11589
11719
  }
@@ -11665,6 +11795,15 @@ function findSources(calls, types, patterns, sourceLines) {
11665
11795
  s.code = sourceLines[s.line - 1]?.trim();
11666
11796
  }
11667
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
+ }
11668
11807
  return result;
11669
11808
  }
11670
11809
  function isInterproceduralTaintableType(typeName) {
@@ -11770,6 +11909,20 @@ function isParameterizedQueryCall(call, pattern) {
11770
11909
  }
11771
11910
  return false;
11772
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
+ }
11773
11926
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
11774
11927
  function argIsClassLiteral(call, position) {
11775
11928
  const arg = call.arguments.find((a) => a.position === position);
@@ -11786,6 +11939,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
11786
11939
  if (isParameterizedQueryCall(call, pattern)) {
11787
11940
  continue;
11788
11941
  }
11942
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
11943
+ continue;
11944
+ }
11789
11945
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
11790
11946
  continue;
11791
11947
  }
@@ -12188,7 +12344,12 @@ function receiverMightBeClass(receiver, className) {
12188
12344
  "controller",
12189
12345
  "task",
12190
12346
  "thread",
12191
- "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"
12192
12353
  ]);
12193
12354
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12194
12355
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12198,7 +12359,9 @@ function receiverMightBeClass(receiver, className) {
12198
12359
  }
12199
12360
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12200
12361
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12201
- return true;
12362
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
12363
+ return true;
12364
+ }
12202
12365
  }
12203
12366
  }
12204
12367
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12221,6 +12384,9 @@ function receiverMightBeClass(receiver, className) {
12221
12384
  ps: ["PreparedStatement"],
12222
12385
  rs: ["ResultSet"],
12223
12386
  template: ["JdbcTemplate"],
12387
+ cur: ["Cursor"],
12388
+ // Python DB-API cursor — see ambiguousIdentifiers note
12389
+ cursor: ["Cursor"],
12224
12390
  // I/O
12225
12391
  writer: ["PrintWriter"],
12226
12392
  out: ["PrintWriter", "OutputStream"],
@@ -15268,7 +15434,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
15268
15434
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
15269
15435
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
15270
15436
  }
15271
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
15437
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
15272
15438
  return { isFalsePositive: true, reason: "variable_not_tainted" };
15273
15439
  }
15274
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
  }