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
@@ -4798,14 +4798,46 @@ function extractJSClassInfo(node) {
4798
4798
  end_line: node.endPosition.row + 1
4799
4799
  };
4800
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
+ }
4801
4821
  function extractJSMethods(body2) {
4802
4822
  const methods = [];
4823
+ let pendingDecorators = [];
4803
4824
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4804
4825
  const child = body2.child(i2);
4805
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;
4806
4833
  if (child.type === "method_definition") {
4807
- methods.push(extractJSMethodInfo(child));
4834
+ const m = extractJSMethodInfo(child);
4835
+ if (pendingDecorators.length > 0) {
4836
+ m.annotations = pendingDecorators;
4837
+ }
4838
+ methods.push(m);
4808
4839
  }
4840
+ pendingDecorators = [];
4809
4841
  }
4810
4842
  return methods;
4811
4843
  }
@@ -4967,10 +4999,18 @@ function extractJSParameters(params) {
4967
4999
  if (typeNode) {
4968
5000
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4969
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
+ }
4970
5010
  parameters.push({
4971
5011
  name: paramName,
4972
5012
  type: paramType,
4973
- annotations: [],
5013
+ annotations: decorators,
4974
5014
  line: child.startPosition.row + 1
4975
5015
  });
4976
5016
  }
@@ -11017,6 +11057,26 @@ var DEFAULT_SINKS = [
11017
11057
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
11018
11058
  // Sandbox/script security
11019
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"] },
11020
11080
  // =========================================================================
11021
11081
  // Node.js/Express Sinks
11022
11082
  // =========================================================================
@@ -11069,13 +11129,47 @@ var DEFAULT_SINKS = [
11069
11129
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11070
11130
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11071
11131
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11072
- // 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.
11073
11140
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11074
11141
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11075
11142
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11076
11143
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11077
11144
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11078
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"] },
11079
11173
  // Node.js SSRF (HTTP clients)
11080
11174
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11081
11175
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11097,6 +11191,24 @@ var DEFAULT_SINKS = [
11097
11191
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11098
11192
  // node-fetch
11099
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"] },
11100
11212
  // =========================================================================
11101
11213
  // Python Sinks
11102
11214
  // =========================================================================
@@ -11138,7 +11250,12 @@ var DEFAULT_SINKS = [
11138
11250
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11139
11251
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11140
11252
  // Python XSS / SSTI
11141
- { 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"] },
11142
11259
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11143
11260
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11144
11261
  // Python SSRF
@@ -11501,6 +11618,13 @@ var DEFAULT_SANITIZERS = [
11501
11618
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
11502
11619
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
11503
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"] },
11504
11628
  // Python Type coercion
11505
11629
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
11506
11630
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -11562,7 +11686,7 @@ var PYTHON_TAINTED_PATTERNS = [
11562
11686
  ];
11563
11687
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
11564
11688
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
11565
- const sources = findSources(calls, types, config.sources, sourceLines);
11689
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
11566
11690
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
11567
11691
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
11568
11692
  return { sources, sinks, sanitizers };
@@ -11580,7 +11704,7 @@ function attachSourceLineCode(sources, sinks, code) {
11580
11704
  }
11581
11705
  }
11582
11706
  }
11583
- function findSources(calls, types, patterns, sourceLines) {
11707
+ function findSources(calls, types, patterns, sourceLines, language) {
11584
11708
  const sources = [];
11585
11709
  for (const call of calls) {
11586
11710
  for (const pattern of patterns) {
@@ -11633,23 +11757,29 @@ function findSources(calls, types, patterns, sourceLines) {
11633
11757
  }
11634
11758
  }
11635
11759
  }
11636
- 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)(?:<|$)/;
11637
11761
  for (const type of types) {
11638
11762
  for (const method of type.methods) {
11639
11763
  for (const param of method.parameters) {
11640
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
11641
- const paramLine = param.line ?? method.start_line;
11642
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
11643
- if (!alreadyExists) {
11644
- sources.push({
11645
- type: "http_body",
11646
- location: `${param.type} ${param.name} in ${method.name}`,
11647
- severity: "high",
11648
- line: paramLine,
11649
- confidence: 1
11650
- });
11651
- }
11652
- }
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
+ });
11653
11783
  }
11654
11784
  }
11655
11785
  }
@@ -11731,6 +11861,15 @@ function findSources(calls, types, patterns, sourceLines) {
11731
11861
  s.code = sourceLines[s.line - 1]?.trim();
11732
11862
  }
11733
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
+ }
11734
11873
  return result;
11735
11874
  }
11736
11875
  function isInterproceduralTaintableType(typeName) {
@@ -11836,6 +11975,20 @@ function isParameterizedQueryCall(call, pattern) {
11836
11975
  }
11837
11976
  return false;
11838
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
+ }
11839
11992
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
11840
11993
  function argIsClassLiteral(call, position) {
11841
11994
  const arg = call.arguments.find((a) => a.position === position);
@@ -11852,6 +12005,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
11852
12005
  if (isParameterizedQueryCall(call, pattern)) {
11853
12006
  continue;
11854
12007
  }
12008
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
12009
+ continue;
12010
+ }
11855
12011
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
11856
12012
  continue;
11857
12013
  }
@@ -12254,7 +12410,12 @@ function receiverMightBeClass(receiver, className) {
12254
12410
  "controller",
12255
12411
  "task",
12256
12412
  "thread",
12257
- "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"
12258
12419
  ]);
12259
12420
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12260
12421
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12264,7 +12425,9 @@ function receiverMightBeClass(receiver, className) {
12264
12425
  }
12265
12426
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12266
12427
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12267
- return true;
12428
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
12429
+ return true;
12430
+ }
12268
12431
  }
12269
12432
  }
12270
12433
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12287,6 +12450,9 @@ function receiverMightBeClass(receiver, className) {
12287
12450
  ps: ["PreparedStatement"],
12288
12451
  rs: ["ResultSet"],
12289
12452
  template: ["JdbcTemplate"],
12453
+ cur: ["Cursor"],
12454
+ // Python DB-API cursor — see ambiguousIdentifiers note
12455
+ cursor: ["Cursor"],
12290
12456
  // I/O
12291
12457
  writer: ["PrintWriter"],
12292
12458
  out: ["PrintWriter", "OutputStream"],
@@ -15334,7 +15500,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
15334
15500
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
15335
15501
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
15336
15502
  }
15337
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
15503
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
15338
15504
  return { isFalsePositive: true, reason: "variable_not_tainted" };
15339
15505
  }
15340
15506
  return { isFalsePositive: false, reason: null };