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
@@ -4752,14 +4752,46 @@ function extractJSClassInfo(node) {
4752
4752
  end_line: node.endPosition.row + 1
4753
4753
  };
4754
4754
  }
4755
+ function extractDecoratorName(node) {
4756
+ const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
4757
+ if (!child) return null;
4758
+ if (child.type === "identifier") return getNodeText(child);
4759
+ if (child.type === "call_expression") {
4760
+ const fn = child.childForFieldName("function");
4761
+ if (fn) {
4762
+ if (fn.type === "identifier") return getNodeText(fn);
4763
+ if (fn.type === "member_expression") {
4764
+ const propNode = fn.childForFieldName("property");
4765
+ if (propNode) return getNodeText(propNode);
4766
+ }
4767
+ }
4768
+ }
4769
+ if (child.type === "member_expression") {
4770
+ const propNode = child.childForFieldName("property");
4771
+ if (propNode) return getNodeText(propNode);
4772
+ }
4773
+ return null;
4774
+ }
4755
4775
  function extractJSMethods(body2) {
4756
4776
  const methods = [];
4777
+ let pendingDecorators = [];
4757
4778
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4758
4779
  const child = body2.child(i2);
4759
4780
  if (!child) continue;
4781
+ if (child.type === "decorator") {
4782
+ const name2 = extractDecoratorName(child);
4783
+ if (name2) pendingDecorators.push(name2);
4784
+ continue;
4785
+ }
4786
+ if (child.type === "comment") continue;
4760
4787
  if (child.type === "method_definition") {
4761
- methods.push(extractJSMethodInfo(child));
4788
+ const m = extractJSMethodInfo(child);
4789
+ if (pendingDecorators.length > 0) {
4790
+ m.annotations = pendingDecorators;
4791
+ }
4792
+ methods.push(m);
4762
4793
  }
4794
+ pendingDecorators = [];
4763
4795
  }
4764
4796
  return methods;
4765
4797
  }
@@ -4921,10 +4953,18 @@ function extractJSParameters(params) {
4921
4953
  if (typeNode) {
4922
4954
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4923
4955
  }
4956
+ const decorators = [];
4957
+ for (let j = 0; j < child.childCount; j++) {
4958
+ const c = child.child(j);
4959
+ if (c && c.type === "decorator") {
4960
+ const name2 = extractDecoratorName(c);
4961
+ if (name2) decorators.push(name2);
4962
+ }
4963
+ }
4924
4964
  parameters.push({
4925
4965
  name: paramName,
4926
4966
  type: paramType,
4927
- annotations: [],
4967
+ annotations: decorators,
4928
4968
  line: child.startPosition.row + 1
4929
4969
  });
4930
4970
  }
@@ -11635,6 +11675,26 @@ var DEFAULT_SINKS = [
11635
11675
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
11636
11676
  // Sandbox/script security
11637
11677
  { method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11678
+ // Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
11679
+ // Issue #44: log.info/warn/error/debug emit the message argument and any
11680
+ // {} format arguments to the log stream. Untrusted input forwarded into
11681
+ // these calls allows log forging (newline injection) and downstream log
11682
+ // analyzer pollution. Scoped to `java` so the generic method names don't
11683
+ // collide with JS console / Python logger entries below.
11684
+ { method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11685
+ { method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11686
+ { method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11687
+ { method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11688
+ { method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11689
+ // java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
11690
+ // Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
11691
+ { method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11692
+ { method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11693
+ { method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11694
+ { method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11695
+ { method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11696
+ { method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11697
+ { method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
11638
11698
  // =========================================================================
11639
11699
  // Node.js/Express Sinks
11640
11700
  // =========================================================================
@@ -11687,13 +11747,47 @@ var DEFAULT_SINKS = [
11687
11747
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11688
11748
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11689
11749
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11690
- // Node.js NoSQL Injection (MongoDB)
11750
+ // Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
11751
+ // Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
11752
+ // fluent chains (mongoose.connection.db.collection('x').find({...})) and
11753
+ // Model.find calls because the call-site receiver type does not resolve
11754
+ // to `Collection`. Add classless+language-scoped entries for the
11755
+ // MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
11756
+ // mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
11757
+ // to avoid colliding with Array.prototype.find.
11691
11758
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11692
11759
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11693
11760
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11694
11761
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11695
11762
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11696
11763
  { method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11764
+ // Mongoose Model/Query class entries — Model.find/findOne/etc.
11765
+ { method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11766
+ { method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11767
+ { method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11768
+ { method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11769
+ { method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11770
+ { method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11771
+ { method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11772
+ { method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11773
+ { method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11774
+ { method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11775
+ { method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11776
+ { method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11777
+ // Mongoose Query class entries — chain methods returning Query
11778
+ { method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11779
+ { method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11780
+ // Classless MongoDB-specific method names (rare outside MongoDB APIs) —
11781
+ // language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
11782
+ { method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11783
+ { method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11784
+ { method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11785
+ { method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11786
+ { method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11787
+ { method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11788
+ { method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11789
+ { method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11790
+ { method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11697
11791
  // Node.js SSRF (HTTP clients)
11698
11792
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11699
11793
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11715,6 +11809,24 @@ var DEFAULT_SINKS = [
11715
11809
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11716
11810
  // node-fetch
11717
11811
  { method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11812
+ // Node.js / JavaScript Log Injection (console.*) — CWE-117
11813
+ // Issue #44: console.log/warn/error/info with tainted template literals
11814
+ // allow log forging (newline-injection) and downstream log analyzer
11815
+ // pollution. Scoped to JS/TS so the bare class `console` doesn't collide
11816
+ // with Python `console` module or Java identifiers.
11817
+ { method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11818
+ { method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11819
+ { method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11820
+ { method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11821
+ { method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11822
+ { method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11823
+ // Node.js / Express Open Redirect — CWE-601
11824
+ // Issue #46: `res.redirect(req.query.next)` did not fire because the
11825
+ // legacy `class: 'Response'` constraint depended on receiver type
11826
+ // resolution of the Express `res` parameter. Mirror Python's classless
11827
+ // pattern with a language-scoped classless entry. The method name
11828
+ // `redirect` is rare outside HTTP frameworks so the FP risk is low.
11829
+ { method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11718
11830
  // =========================================================================
11719
11831
  // Python Sinks
11720
11832
  // =========================================================================
@@ -11756,7 +11868,12 @@ var DEFAULT_SINKS = [
11756
11868
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11757
11869
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11758
11870
  // Python XSS / SSTI
11759
- { method: "render_template_string", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11871
+ // Issue #54: Flask's `render_template_string(template_str)` with an
11872
+ // attacker-controlled template string is Server-Side Template Injection
11873
+ // (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
11874
+ // (CWE-94) with critical severity to match `jinja2.Template().render()`
11875
+ // and `Template.from_string()` entries above.
11876
+ { method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11760
11877
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11761
11878
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11762
11879
  // Python SSRF
@@ -12119,6 +12236,13 @@ var DEFAULT_SANITIZERS = [
12119
12236
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
12120
12237
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
12121
12238
  { method: "normpath", class: "os.path", removes: ["path_traversal"] },
12239
+ // Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
12240
+ // functions (analogous to Java File.getCanonicalPath). Register on both
12241
+ // `os.path` and the bare `path` receiver to cover `import os.path as path`.
12242
+ { method: "realpath", class: "os.path", removes: ["path_traversal"] },
12243
+ { method: "abspath", class: "os.path", removes: ["path_traversal"] },
12244
+ { method: "realpath", class: "path", removes: ["path_traversal"] },
12245
+ { method: "abspath", class: "path", removes: ["path_traversal"] },
12122
12246
  // Python Type coercion
12123
12247
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
12124
12248
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -12267,7 +12391,7 @@ var PYTHON_TAINTED_PATTERNS = [
12267
12391
  ];
12268
12392
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
12269
12393
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
12270
- const sources = findSources(calls, types, config.sources, sourceLines);
12394
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
12271
12395
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
12272
12396
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
12273
12397
  return { sources, sinks, sanitizers };
@@ -12285,7 +12409,7 @@ function attachSourceLineCode(sources, sinks, code) {
12285
12409
  }
12286
12410
  }
12287
12411
  }
12288
- function findSources(calls, types, patterns, sourceLines) {
12412
+ function findSources(calls, types, patterns, sourceLines, language) {
12289
12413
  const sources = [];
12290
12414
  for (const call of calls) {
12291
12415
  for (const pattern of patterns) {
@@ -12338,23 +12462,29 @@ function findSources(calls, types, patterns, sourceLines) {
12338
12462
  }
12339
12463
  }
12340
12464
  }
12341
- const RUST_EXTRACTOR_TYPES = /^(?:Json|Form|Query|Path|Extension|Multipart)(?:<|$)|^(?:Body|Bytes)$/;
12465
+ const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
12342
12466
  for (const type of types) {
12343
12467
  for (const method of type.methods) {
12344
12468
  for (const param of method.parameters) {
12345
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
12346
- const paramLine = param.line ?? method.start_line;
12347
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
12348
- if (!alreadyExists) {
12349
- sources.push({
12350
- type: "http_body",
12351
- location: `${param.type} ${param.name} in ${method.name}`,
12352
- severity: "high",
12353
- line: paramLine,
12354
- confidence: 1
12355
- });
12356
- }
12357
- }
12469
+ if (!param.type) continue;
12470
+ const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
12471
+ if (!kindMatch) continue;
12472
+ const kind = kindMatch[1];
12473
+ if (kind === "Extension") continue;
12474
+ const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
12475
+ const paramLine = param.line ?? method.start_line;
12476
+ const alreadyExists = sources.some(
12477
+ (s) => s.line === paramLine && s.variable === param.name
12478
+ );
12479
+ if (alreadyExists) continue;
12480
+ sources.push({
12481
+ type: sourceType,
12482
+ location: `${param.type} ${param.name} in ${method.name}`,
12483
+ severity: "high",
12484
+ line: paramLine,
12485
+ confidence: 1,
12486
+ variable: param.name
12487
+ });
12358
12488
  }
12359
12489
  }
12360
12490
  }
@@ -12436,6 +12566,15 @@ function findSources(calls, types, patterns, sourceLines) {
12436
12566
  s.code = sourceLines[s.line - 1]?.trim();
12437
12567
  }
12438
12568
  }
12569
+ if (language === "rust" && sourceLines) {
12570
+ const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
12571
+ for (const s of result) {
12572
+ if (s.variable && s.variable.length > 0) continue;
12573
+ const lineText = sourceLines[s.line - 1] ?? "";
12574
+ const m = LET_BINDING.exec(lineText);
12575
+ if (m) s.variable = m[1];
12576
+ }
12577
+ }
12439
12578
  return result;
12440
12579
  }
12441
12580
  function isInterproceduralTaintableType(typeName) {
@@ -12541,6 +12680,20 @@ function isParameterizedQueryCall(call, pattern) {
12541
12680
  }
12542
12681
  return false;
12543
12682
  }
12683
+ function isSafePythonSubprocessCall(call, pattern, language) {
12684
+ if (language !== "python") return false;
12685
+ if (pattern.type !== "command_injection") return false;
12686
+ if (pattern.class !== "subprocess") return false;
12687
+ const arg0 = call.arguments.find((a) => a.position === 0);
12688
+ if (!arg0) return false;
12689
+ const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
12690
+ if (!expr0.startsWith("[")) return false;
12691
+ for (const a of call.arguments) {
12692
+ const e = (a.expression ?? "").trim();
12693
+ if (/^shell\s*=\s*True\b/.test(e)) return false;
12694
+ }
12695
+ return true;
12696
+ }
12544
12697
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12545
12698
  function argIsClassLiteral(call, position) {
12546
12699
  const arg = call.arguments.find((a) => a.position === position);
@@ -12557,6 +12710,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12557
12710
  if (isParameterizedQueryCall(call, pattern)) {
12558
12711
  continue;
12559
12712
  }
12713
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
12714
+ continue;
12715
+ }
12560
12716
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12561
12717
  continue;
12562
12718
  }
@@ -12959,7 +13115,12 @@ function receiverMightBeClass(receiver, className) {
12959
13115
  "controller",
12960
13116
  "task",
12961
13117
  "thread",
12962
- "job"
13118
+ "job",
13119
+ // Short Python DB abbreviation; would otherwise prefix-match obscure XSS
13120
+ // sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
13121
+ // via the CamelCase word prefix heuristic and produce an xss FP on every
13122
+ // `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
13123
+ "cur"
12963
13124
  ]);
12964
13125
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12965
13126
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12969,7 +13130,9 @@ function receiverMightBeClass(receiver, className) {
12969
13130
  }
12970
13131
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12971
13132
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12972
- return true;
13133
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
13134
+ return true;
13135
+ }
12973
13136
  }
12974
13137
  }
12975
13138
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12992,6 +13155,9 @@ function receiverMightBeClass(receiver, className) {
12992
13155
  ps: ["PreparedStatement"],
12993
13156
  rs: ["ResultSet"],
12994
13157
  template: ["JdbcTemplate"],
13158
+ cur: ["Cursor"],
13159
+ // Python DB-API cursor — see ambiguousIdentifiers note
13160
+ cursor: ["Cursor"],
12995
13161
  // I/O
12996
13162
  writer: ["PrintWriter"],
12997
13163
  out: ["PrintWriter", "OutputStream"],
@@ -17394,7 +17560,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
17394
17560
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
17395
17561
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
17396
17562
  }
17397
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
17563
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
17398
17564
  return { isFalsePositive: true, reason: "variable_not_tainted" };
17399
17565
  }
17400
17566
  return { isFalsePositive: false, reason: null };
@@ -21712,6 +21878,37 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
21712
21878
  }
21713
21879
  return tainted;
21714
21880
  }
21881
+ function buildRustTaintedVars(sourceCode, seedVars) {
21882
+ const derived = /* @__PURE__ */ new Map();
21883
+ const knownTainted = new Set(seedVars);
21884
+ const lines = sourceCode.split("\n");
21885
+ let changed = true;
21886
+ while (changed) {
21887
+ changed = false;
21888
+ for (let i2 = 0; i2 < lines.length; i2++) {
21889
+ const line = lines[i2];
21890
+ const trimmed = line.trimStart();
21891
+ if (trimmed.startsWith("//")) continue;
21892
+ const letMatch = line.match(
21893
+ /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*(.+?)(?:;|$)/
21894
+ );
21895
+ const assignMatch = !letMatch ? line.match(/^\s*([A-Za-z_]\w*)\s*=\s*(.+?)(?:;|$)/) : null;
21896
+ const m = letMatch ?? assignMatch;
21897
+ if (!m) continue;
21898
+ const lhs = m[1];
21899
+ const rhs = m[2];
21900
+ if (lhs === "if" || lhs === "while" || lhs === "for" || lhs === "match" || lhs === "return") continue;
21901
+ if (knownTainted.has(lhs)) continue;
21902
+ const ref = [...knownTainted].some((v) => new RegExp(`\\b${v}\\b`).test(rhs));
21903
+ if (ref) {
21904
+ derived.set(lhs, i2 + 1);
21905
+ knownTainted.add(lhs);
21906
+ changed = true;
21907
+ }
21908
+ }
21909
+ }
21910
+ return derived;
21911
+ }
21715
21912
  var BASH_UNTRUSTED_ENV_PATTERNS = [
21716
21913
  /^USER_INPUT$/i,
21717
21914
  /^QUERY_STRING$/i,
@@ -22111,7 +22308,20 @@ function evaluateSimpleExpression(expr, symbols) {
22111
22308
  }
22112
22309
  function isStringLiteralExpression(expr) {
22113
22310
  const trimmed = expr.trim();
22114
- return trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'");
22311
+ if (trimmed.length < 2) return false;
22312
+ const quote = trimmed[0];
22313
+ if (quote !== '"' && quote !== "'") return false;
22314
+ let i2 = 1;
22315
+ while (i2 < trimmed.length) {
22316
+ const c = trimmed[i2];
22317
+ if (c === "\\") {
22318
+ i2 += 2;
22319
+ continue;
22320
+ }
22321
+ if (c === quote) return i2 === trimmed.length - 1;
22322
+ i2++;
22323
+ }
22324
+ return false;
22115
22325
  }
22116
22326
  function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
22117
22327
  const callsByLine = /* @__PURE__ */ new Map();
@@ -22296,7 +22506,7 @@ var TaintPropagationPass = class {
22296
22506
  flows.push(f);
22297
22507
  }
22298
22508
  }
22299
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
22509
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
22300
22510
  for (const f of exprScanFlows) {
22301
22511
  if (flows.some(
22302
22512
  (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
@@ -22508,14 +22718,63 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22508
22718
  void types;
22509
22719
  return flows;
22510
22720
  }
22511
- function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code, language) {
22721
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
22512
22722
  const flows = [];
22513
22723
  const sourcesWithVar = sources.filter(
22514
22724
  (s) => typeof s.variable === "string" && s.variable.length > 0
22515
22725
  );
22516
22726
  if (sourcesWithVar.length === 0) return flows;
22727
+ const aliasSanitizedFor = /* @__PURE__ */ new Map();
22517
22728
  if (language === "python" && typeof code === "string") {
22518
22729
  const derived = buildPythonTaintedVars(code);
22730
+ if (derived.size > 0) {
22731
+ let anchor = sourcesWithVar[0];
22732
+ for (const s of sourcesWithVar) {
22733
+ if (s.line < anchor.line) anchor = s;
22734
+ }
22735
+ const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
22736
+ for (const [varName] of derived) {
22737
+ if (!varName || existingVars.has(varName)) continue;
22738
+ sourcesWithVar.push({
22739
+ ...anchor,
22740
+ variable: varName
22741
+ });
22742
+ existingVars.add(varName);
22743
+ }
22744
+ if (sanitizers && sanitizers.length > 0) {
22745
+ const sanitizersByLine = /* @__PURE__ */ new Map();
22746
+ for (const s of sanitizers) {
22747
+ const arr = sanitizersByLine.get(s.line) ?? [];
22748
+ arr.push(s);
22749
+ sanitizersByLine.set(s.line, arr);
22750
+ }
22751
+ const codeLines = code.split("\n");
22752
+ for (const [varName, originLine] of derived) {
22753
+ const lineSans = sanitizersByLine.get(originLine);
22754
+ if (!lineSans || lineSans.length === 0) continue;
22755
+ const lineText = codeLines[originLine - 1] ?? "";
22756
+ const rhsMatch = lineText.match(/^\s*\w+\s*=\s*(.+)$/);
22757
+ if (!rhsMatch) continue;
22758
+ const rhs = rhsMatch[1];
22759
+ for (const san of lineSans) {
22760
+ const sanMatch = san.method.match(/^(?:(\w+)\.)?(\w+)\(\)$/);
22761
+ if (!sanMatch) continue;
22762
+ const sanName = sanMatch[1] ? `${sanMatch[1]}.${sanMatch[2]}` : sanMatch[2];
22763
+ if (!rhs.includes(`${sanName}(`)) continue;
22764
+ let set = aliasSanitizedFor.get(varName);
22765
+ if (!set) {
22766
+ set = /* @__PURE__ */ new Set();
22767
+ aliasSanitizedFor.set(varName, set);
22768
+ }
22769
+ for (const t of san.sanitizes) set.add(t);
22770
+ }
22771
+ }
22772
+ }
22773
+ }
22774
+ }
22775
+ if (language === "rust" && typeof code === "string") {
22776
+ const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
22777
+ const derived = buildRustTaintedVars(code, seedVars);
22519
22778
  if (derived.size > 0) {
22520
22779
  let anchor = sourcesWithVar[0];
22521
22780
  for (const s of sourcesWithVar) {
@@ -22561,6 +22820,9 @@ function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code
22561
22820
  if (flows.some(
22562
22821
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
22563
22822
  )) continue;
22823
+ if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
22824
+ break;
22825
+ }
22564
22826
  flows.push({
22565
22827
  source_line: source.line,
22566
22828
  sink_line: sink.line,
@@ -22705,6 +22967,9 @@ var InterproceduralPass = class {
22705
22967
  }
22706
22968
  }
22707
22969
  }
22970
+ if (additionalSinks.length > 0) {
22971
+ attachSourceLineCode([], additionalSinks, ctx.code);
22972
+ }
22708
22973
  return { additionalSinks, additionalFlows, interprocedural };
22709
22974
  }
22710
22975
  };
@@ -26703,6 +26968,70 @@ function isPotentialPojo(type) {
26703
26968
  return first >= 65 && first <= 90;
26704
26969
  }
26705
26970
 
26971
+ // src/analysis/passes/insecure-cookie-pass.ts
26972
+ var COOKIE_RESPONSE_RECEIVERS = /* @__PURE__ */ new Set([
26973
+ "res",
26974
+ "response",
26975
+ "reply"
26976
+ ]);
26977
+ var SECURE_TRUE_RE = /\bsecure\s*:\s*true\b/;
26978
+ var HTTPONLY_TRUE_RE = /\bhttpOnly\s*:\s*true\b/i;
26979
+ var InsecureCookiePass = class {
26980
+ name = "insecure-cookie";
26981
+ category = "security";
26982
+ run(ctx) {
26983
+ const { graph, language } = ctx;
26984
+ if (language !== "javascript" && language !== "typescript") {
26985
+ return { insecureCookies: [] };
26986
+ }
26987
+ const file = graph.ir.meta.file;
26988
+ const insecureCookies = [];
26989
+ for (const call of graph.ir.calls) {
26990
+ if (call.method_name !== "cookie") continue;
26991
+ const receiver = call.receiver ?? "";
26992
+ if (!COOKIE_RESPONSE_RECEIVERS.has(receiver)) continue;
26993
+ if (call.arguments.length < 2) continue;
26994
+ const opts = call.arguments.find((a) => a.position === 2);
26995
+ const optsExpr = (opts?.expression ?? "").trim();
26996
+ const optionsPresent = optsExpr.length > 0;
26997
+ const missingSecure = !SECURE_TRUE_RE.test(optsExpr);
26998
+ const missingHttpOnly = !HTTPONLY_TRUE_RE.test(optsExpr);
26999
+ if (!missingSecure && !missingHttpOnly) continue;
27000
+ const line = call.location.line;
27001
+ insecureCookies.push({
27002
+ line,
27003
+ receiver,
27004
+ missingSecure,
27005
+ missingHttpOnly,
27006
+ optionsPresent
27007
+ });
27008
+ const missing = [];
27009
+ if (missingSecure) missing.push("`secure: true`");
27010
+ if (missingHttpOnly) missing.push("`httpOnly: true`");
27011
+ ctx.addFinding({
27012
+ id: `${this.name}-${file}-${line}`,
27013
+ pass: this.name,
27014
+ category: this.category,
27015
+ rule_id: this.name,
27016
+ cwe: "CWE-614",
27017
+ severity: "medium",
27018
+ level: "warning",
27019
+ message: `Cookie set without ${missing.join(" and ")} \u2014 vulnerable to cleartext transmission (CWE-614) and client-side JS access (CWE-1004).`,
27020
+ file,
27021
+ line,
27022
+ fix: 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.',
27023
+ evidence: {
27024
+ receiver,
27025
+ options_present: optionsPresent,
27026
+ missing_secure: missingSecure,
27027
+ missing_http_only: missingHttpOnly
27028
+ }
27029
+ });
27030
+ }
27031
+ return { insecureCookies };
27032
+ }
27033
+ };
27034
+
26706
27035
  // src/analysis/metrics/passes/size-metrics-pass.ts
26707
27036
  var SizeMetricsPass = class {
26708
27037
  name = "size-metrics";
@@ -27599,6 +27928,7 @@ async function analyze(code, filePath, language, options = {}) {
27599
27928
  if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
27600
27929
  if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
27601
27930
  if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
27931
+ if (!disabledPasses.has("insecure-cookie")) pipeline.add(new InsecureCookiePass());
27602
27932
  const { results, findings } = pipeline.run(graph, code, language, config);
27603
27933
  const sinkFilter = results.get("sink-filter");
27604
27934
  const interProc = results.get("interprocedural");