circle-ir 3.52.0 → 3.54.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 (31) hide show
  1. package/configs/sinks/path.yaml +0 -16
  2. package/configs/sources/file_sources.yaml +32 -0
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +59 -1
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts +45 -0
  7. package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts.map +1 -0
  8. package/dist/analysis/passes/jwt-verify-disabled-pass.js +164 -0
  9. package/dist/analysis/passes/jwt-verify-disabled-pass.js.map +1 -0
  10. package/dist/analysis/passes/weak-crypto-pass.d.ts +17 -7
  11. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/weak-crypto-pass.js +179 -10
  13. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  14. package/dist/analysis/rules.d.ts.map +1 -1
  15. package/dist/analysis/rules.js +18 -0
  16. package/dist/analysis/rules.js.map +1 -1
  17. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  18. package/dist/analysis/taint-matcher.js +28 -13
  19. package/dist/analysis/taint-matcher.js.map +1 -1
  20. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  21. package/dist/analysis/taint-propagation.js +1 -0
  22. package/dist/analysis/taint-propagation.js.map +1 -1
  23. package/dist/analyzer.d.ts.map +1 -1
  24. package/dist/analyzer.js +3 -0
  25. package/dist/analyzer.js.map +1 -1
  26. package/dist/browser/circle-ir.js +269 -11
  27. package/dist/core/circle-ir-core.cjs +71 -9
  28. package/dist/core/circle-ir-core.js +71 -9
  29. package/dist/types/index.d.ts +1 -1
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/package.json +1 -1
@@ -10621,6 +10621,13 @@ var DEFAULT_SOURCES = [
10621
10621
  { method: "getFileName", class: "BodyPart", type: "file_input", severity: "high", return_tainted: true },
10622
10622
  { method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
10623
10623
  { method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
10624
+ // Archive entry names (Zip-Slip / Tar-Slip CWE-22, issue #52)
10625
+ // entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
10626
+ // is a classic Zip-Slip vulnerability.
10627
+ { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
10628
+ { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10629
+ { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10630
+ { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10624
10631
  // Command line arguments
10625
10632
  { method: "getArgs", type: "io_input", severity: "high", return_tainted: true },
10626
10633
  { method: "getOptionValue", class: "CommandLine", type: "io_input", severity: "high", return_tainted: true },
@@ -11055,7 +11062,7 @@ var DEFAULT_SINKS = [
11055
11062
  { method: "staticFileLocation", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11056
11063
  // Zip/archive handling
11057
11064
  { method: "getEntry", class: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11058
- { method: "getName", class: "ZipEntry", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [] },
11065
+ // ZipEntry.getName moved to file_sources.yaml as a taint SOURCE (type=archive_entry, issue #52)
11059
11066
  // Resource loading classes (various frameworks)
11060
11067
  { method: "ClassPathResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11061
11068
  { method: "FileSystemResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -12090,7 +12097,58 @@ var DEFAULT_SINKS = [
12090
12097
  { method: "from_str", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
12091
12098
  { method: "from_reader", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
12092
12099
  { method: "from_str", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
12093
- { method: "from_slice", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] }
12100
+ { method: "from_slice", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
12101
+ // =========================================================================
12102
+ // ReDoS sinks (CWE-1333) — issue #86 / Sprint 5
12103
+ // =========================================================================
12104
+ // First argument of regex compile/match functions is the pattern. Tainted
12105
+ // patterns enable catastrophic-backtracking DoS.
12106
+ // Python: re.{match,search,compile,findall,fullmatch,sub,subn,split}
12107
+ { method: "match", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12108
+ { method: "search", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12109
+ { method: "fullmatch", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12110
+ { method: "compile", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12111
+ { method: "findall", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12112
+ { method: "finditer", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12113
+ { method: "sub", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12114
+ { method: "subn", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12115
+ { method: "split", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
12116
+ // Java: Pattern.compile / Pattern.matches; String.matches/replaceAll/replaceFirst/split
12117
+ { method: "compile", class: "Pattern", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12118
+ { method: "matches", class: "Pattern", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12119
+ { method: "matches", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12120
+ { method: "replaceAll", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12121
+ { method: "replaceFirst", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12122
+ { method: "split", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
12123
+ // JS/TS: new RegExp(pat) ctor; receiver_type === 'RegExp'. Also string.match
12124
+ // and string.matchAll, replace, search take a regex/string pattern.
12125
+ { method: "RegExp", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12126
+ // Go: regexp.Compile / MustCompile / Match / MatchString
12127
+ { method: "Compile", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
12128
+ { method: "MustCompile", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
12129
+ { method: "Match", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
12130
+ { method: "MatchString", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
12131
+ // =========================================================================
12132
+ // Format-string sinks (CWE-134) — issue #86 / Sprint 5
12133
+ // =========================================================================
12134
+ // First argument is the format string. Tainted format strings enable
12135
+ // information disclosure and (for C-style runtimes) memory writes.
12136
+ // Java: String.format / Formatter.format / printf / format on PrintStream
12137
+ // (note: printf/format on PrintWriter/PrintStream are already XSS sinks above)
12138
+ { method: "format", class: "String", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
12139
+ { method: "format", class: "Formatter", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
12140
+ { method: "printf", class: "System.out", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
12141
+ // NOTE: Python `userFmt.format(...)` and `userFmt % args` require
12142
+ // receiver-taint or operator-LHS-taint tracking — the format string is the
12143
+ // receiver, not an argument. Deferred to Sprint 6 (#86 follow-up).
12144
+ // C-style: printf / fprintf / sprintf / snprintf via ctypes/cffi.
12145
+ { method: "printf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["python"] },
12146
+ { method: "fprintf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [1], languages: ["python"] },
12147
+ // Go: fmt.Sprintf/Printf/Fprintf/Errorf — format string is first/second arg
12148
+ { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12149
+ { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12150
+ { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12151
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
12094
12152
  ];
12095
12153
  var DEFAULT_SANITIZERS = [
12096
12154
  // SQL Injection - proper parameter binding sanitizes input
@@ -12738,10 +12796,11 @@ function matchesSourcePattern(call, pattern) {
12738
12796
  return false;
12739
12797
  }
12740
12798
  if (pattern.class && pattern.class !== "constructor") {
12741
- if (!call.receiver) {
12799
+ if (call.receiver_type && call.receiver_type === pattern.class) {
12800
+ } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12801
+ } else if (!call.receiver) {
12742
12802
  return false;
12743
- }
12744
- if (!receiverMightBeClass(call.receiver, pattern.class)) {
12803
+ } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
12745
12804
  return false;
12746
12805
  }
12747
12806
  }
@@ -12999,13 +13058,14 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
12999
13058
  if (pattern.class === "constructor") {
13000
13059
  return true;
13001
13060
  }
13002
- if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
13061
+ if (call.receiver_type && call.receiver_type === pattern.class) {
13062
+ } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
13063
+ } else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
13003
13064
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
13004
13065
  return true;
13005
13066
  }
13006
13067
  return false;
13007
- }
13008
- if (!call.receiver) {
13068
+ } else if (!call.receiver && !call.receiver_type) {
13009
13069
  return false;
13010
13070
  }
13011
13071
  }
@@ -14771,7 +14831,9 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
14771
14831
  "xxe",
14772
14832
  "deserialization",
14773
14833
  "code_injection",
14774
- "mybatis_mapper_call"
14834
+ "mybatis_mapper_call",
14835
+ "redos",
14836
+ "format_string"
14775
14837
  ]);
14776
14838
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
14777
14839
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -27347,6 +27409,50 @@ function literalAlgo2(call, position) {
27347
27409
  const cleaned = stripQuotes5(raw);
27348
27410
  return cleaned || null;
27349
27411
  }
27412
+ function detectStaticIvJava(call) {
27413
+ const arg = call.arguments.find((a) => a.position === 0);
27414
+ if (!arg) return null;
27415
+ const expr = (arg.literal ?? arg.expression ?? "").trim();
27416
+ if (!expr) return null;
27417
+ if (/^new\s+byte\s*\[[^\]]*\]\s*$/.test(expr)) {
27418
+ return `zero-filled ${expr}`;
27419
+ }
27420
+ if (/^new\s+byte\s*\[\s*\]\s*\{[^}]*\}\s*$/.test(expr)) {
27421
+ return `literal byte[] initializer`;
27422
+ }
27423
+ if (/^"[^"]*"\.getBytes\s*\(/.test(expr)) {
27424
+ return `literal string .getBytes()`;
27425
+ }
27426
+ if (/^"[^"]*"$/.test(expr)) {
27427
+ return `literal string`;
27428
+ }
27429
+ return null;
27430
+ }
27431
+ function isJavaCtor(call, className) {
27432
+ if (call.is_constructor === true) return true;
27433
+ if (call.receiver) return false;
27434
+ if (call.receiver_type === className) return true;
27435
+ if ((call.receiver_type_fqn ?? "").endsWith("." + className)) return true;
27436
+ return false;
27437
+ }
27438
+ function detectHardcodedKeyJava(call) {
27439
+ const arg = call.arguments.find((a) => a.position === 0);
27440
+ if (!arg) return null;
27441
+ const expr = (arg.literal ?? arg.expression ?? "").trim();
27442
+ if (!expr) return null;
27443
+ if (/^"[^"]*"\.getBytes\s*\(/.test(expr)) return `literal string .getBytes()`;
27444
+ if (/^new\s+byte\s*\[\s*\]\s*\{[^}]*\}\s*$/.test(expr)) return `literal byte[] initializer`;
27445
+ if (/^"[^"]*"$/.test(expr)) return `literal string`;
27446
+ return null;
27447
+ }
27448
+ var ISSUE_CWE = {
27449
+ "weak-cipher": "CWE-327",
27450
+ "ecb-mode": "CWE-327",
27451
+ "deprecated-api": "CWE-327",
27452
+ "static-iv": "CWE-329",
27453
+ "hardcoded-key": "CWE-321",
27454
+ "weak-rsa-key": "CWE-326"
27455
+ };
27350
27456
  var WeakCryptoPass = class {
27351
27457
  name = "weak-crypto";
27352
27458
  category = "security";
@@ -27365,13 +27471,13 @@ var WeakCryptoPass = class {
27365
27471
  pass: this.name,
27366
27472
  category: this.category,
27367
27473
  rule_id: this.name,
27368
- cwe: "CWE-327",
27474
+ cwe: ISSUE_CWE[det.issue],
27369
27475
  severity: "high",
27370
27476
  level: "error",
27371
27477
  message,
27372
27478
  file,
27373
27479
  line,
27374
- fix: "Use AES-GCM (authenticated) or ChaCha20-Poly1305. Avoid DES, 3DES, RC2, RC4, Blowfish, and ECB mode. For asymmetric encryption use RSA-OAEP with \u22652048-bit keys or modern curve-based schemes.",
27480
+ fix: this.buildFix(det.issue),
27375
27481
  evidence: { ...det, language }
27376
27482
  });
27377
27483
  }
@@ -27386,10 +27492,28 @@ var WeakCryptoPass = class {
27386
27492
  return `ECB block-cipher mode used via \`${det.api}\` (\`${det.detail}\`). ECB leaks plaintext structure (identical blocks \u2192 identical ciphertext) and is not semantically secure.`;
27387
27493
  case "deprecated-api":
27388
27494
  return `Deprecated crypto API \`${det.api}\` used (no IV: \`${det.detail}\`). This API derives the key/IV from a password in an insecure way.`;
27495
+ case "static-iv":
27496
+ return `Static or zero-valued IV passed to \`${det.api}\` (\`${det.detail}\`). Reusing a fixed IV with CBC/CTR/GCM breaks confidentiality and, for GCM, can leak the authentication key.`;
27497
+ case "hardcoded-key":
27498
+ return `Hardcoded symmetric key material passed to \`${det.api}\` (\`${det.detail}\`). Keys embedded in source code are trivially recoverable from binaries and shared across deployments \u2014 they provide no confidentiality.`;
27499
+ case "weak-rsa-key":
27500
+ return `Weak RSA key size \`${det.detail}\` requested via \`${det.api}\`. RSA keys below 2048 bits are factorable and not compliant with NIST SP 800-57 / FIPS 186-5.`;
27389
27501
  default:
27390
27502
  return `Weak cryptography: ${det.detail} (${det.api})`;
27391
27503
  }
27392
27504
  }
27505
+ buildFix(issue) {
27506
+ switch (issue) {
27507
+ case "static-iv":
27508
+ return "Generate a fresh random IV per message using SecureRandom: `byte[] iv = new byte[12]; SecureRandom.getInstanceStrong().nextBytes(iv); new IvParameterSpec(iv);` and prepend it to the ciphertext.";
27509
+ case "hardcoded-key":
27510
+ return "Load the key from a secure key management system (HSM, KMS, Vault) or platform keystore. Never embed key material in source code.";
27511
+ case "weak-rsa-key":
27512
+ return "Initialize KeyPairGenerator with at least 2048 bits (preferably 3072 or 4096) for RSA, or switch to EC keys (P-256+).";
27513
+ default:
27514
+ return "Use AES-GCM (authenticated) or ChaCha20-Poly1305. Avoid DES, 3DES, RC2, RC4, Blowfish, and ECB mode. For asymmetric encryption use RSA-OAEP with \u22652048-bit keys or modern curve-based schemes.";
27515
+ }
27516
+ }
27393
27517
  detect(call, language) {
27394
27518
  const method = call.method_name;
27395
27519
  const receiver = call.receiver ?? "";
@@ -27405,6 +27529,33 @@ var WeakCryptoPass = class {
27405
27529
  if (ecb) out2.push({ issue: "ecb-mode", detail: spec, api });
27406
27530
  }
27407
27531
  }
27532
+ if (method === "IvParameterSpec" && isJavaCtor(call, "IvParameterSpec")) {
27533
+ const ivDetail = detectStaticIvJava(call);
27534
+ if (ivDetail) {
27535
+ out2.push({ issue: "static-iv", detail: ivDetail, api: "new IvParameterSpec" });
27536
+ }
27537
+ }
27538
+ if (method === "SecretKeySpec" && isJavaCtor(call, "SecretKeySpec")) {
27539
+ const keyDetail = detectHardcodedKeyJava(call);
27540
+ if (keyDetail) {
27541
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: "new SecretKeySpec" });
27542
+ }
27543
+ }
27544
+ if (method === "initialize") {
27545
+ const isKpg = call.receiver_type === "KeyPairGenerator" || (call.receiver_type_fqn ?? "").endsWith(".KeyPairGenerator");
27546
+ if (isKpg) {
27547
+ const sizeArg = call.arguments.find((a) => a.position === 0);
27548
+ const expr = (sizeArg?.literal ?? sizeArg?.expression ?? "").trim();
27549
+ const n = parseInt(expr, 10);
27550
+ if (Number.isFinite(n) && n > 0 && n < 2048) {
27551
+ out2.push({
27552
+ issue: "weak-rsa-key",
27553
+ detail: String(n),
27554
+ api: "KeyPairGenerator.initialize"
27555
+ });
27556
+ }
27557
+ }
27558
+ }
27408
27559
  return out2;
27409
27560
  }
27410
27561
  if (language === "python") {
@@ -27830,6 +27981,112 @@ var TlsVerifyDisabledPass = class {
27830
27981
  }
27831
27982
  };
27832
27983
 
27984
+ // src/analysis/passes/jwt-verify-disabled-pass.ts
27985
+ var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
27986
+ var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
27987
+ var PY_ALG_NONE_RE = /\balgorithms\s*=\s*[\[\(]\s*["']none["']/i;
27988
+ var JS_ALG_NONE_RE = /\balgorithms\s*:\s*\[\s*["']none["']/i;
27989
+ var JwtVerifyDisabledPass = class {
27990
+ name = "jwt-verify-disabled";
27991
+ category = "security";
27992
+ run(ctx) {
27993
+ const { graph, language } = ctx;
27994
+ const file = graph.ir.meta.file;
27995
+ const findings = [];
27996
+ for (const call of graph.ir.calls) {
27997
+ const detections = this.detect(call, language);
27998
+ for (const det of detections) {
27999
+ const line = call.location.line;
28000
+ findings.push({ line, language, ...det });
28001
+ ctx.addFinding({
28002
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28003
+ pass: this.name,
28004
+ category: this.category,
28005
+ rule_id: this.name,
28006
+ cwe: "CWE-347",
28007
+ severity: "critical",
28008
+ level: "error",
28009
+ message: `JWT signature verification disabled via \`${det.pattern}\` in \`${det.api}\`. Any attacker can forge a token with arbitrary claims (user id, roles, expiry) since the signature is not checked.`,
28010
+ file,
28011
+ line,
28012
+ fix: this.fixFor(language),
28013
+ evidence: { ...det, language }
28014
+ });
28015
+ }
28016
+ }
28017
+ return { findings };
28018
+ }
28019
+ detect(call, language) {
28020
+ const method = call.method_name;
28021
+ const receiver = call.receiver ?? "";
28022
+ const out2 = [];
28023
+ if (language === "python") {
28024
+ if (receiver === "jwt" && method === "decode") {
28025
+ for (const arg of call.arguments) {
28026
+ const expr = (arg.expression ?? "").trim();
28027
+ if (!expr) continue;
28028
+ if (PY_VERIFY_SIGNATURE_FALSE_RE.test(expr)) {
28029
+ out2.push({ pattern: "verify_signature: False", api: "jwt.decode" });
28030
+ }
28031
+ if (PY_VERIFY_KW_FALSE_RE.test(expr)) {
28032
+ out2.push({ pattern: "verify=False", api: "jwt.decode" });
28033
+ }
28034
+ if (PY_ALG_NONE_RE.test(expr)) {
28035
+ out2.push({ pattern: "algorithms=['none']", api: "jwt.decode" });
28036
+ }
28037
+ }
28038
+ }
28039
+ return out2;
28040
+ }
28041
+ if (language === "javascript" || language === "typescript") {
28042
+ if (receiver === "jwt" && method === "verify") {
28043
+ for (const arg of call.arguments) {
28044
+ const expr = (arg.expression ?? "").trim();
28045
+ if (!expr) continue;
28046
+ if (JS_ALG_NONE_RE.test(expr)) {
28047
+ out2.push({ pattern: "algorithms: ['none']", api: "jwt.verify" });
28048
+ }
28049
+ if (/\bverify\s*:\s*false\b/i.test(expr)) {
28050
+ out2.push({ pattern: "verify: false", api: "jwt.verify" });
28051
+ }
28052
+ }
28053
+ const keyArg = call.arguments.find((a) => a.position === 1);
28054
+ const keyExpr = (keyArg?.expression ?? keyArg?.literal ?? "").trim();
28055
+ if (keyExpr === "null" || keyExpr === "undefined" || keyExpr === '""' || keyExpr === "''" || keyExpr === "``") {
28056
+ out2.push({ pattern: `empty key (${keyExpr || "missing"})`, api: "jwt.verify" });
28057
+ }
28058
+ }
28059
+ return out2;
28060
+ }
28061
+ if (language === "java") {
28062
+ if (method === "require" && (receiver === "JWT" || receiver.endsWith(".JWT"))) {
28063
+ const arg = call.arguments.find((a) => a.position === 0);
28064
+ const expr = (arg?.expression ?? "").trim();
28065
+ if (/\bAlgorithm\s*\.\s*none\s*\(/.test(expr)) {
28066
+ out2.push({ pattern: "Algorithm.none()", api: "JWT.require" });
28067
+ }
28068
+ }
28069
+ if (method === "parse" && receiver.includes("parser")) {
28070
+ out2.push({ pattern: "parse() instead of parseClaimsJws()", api: "Jwts.parser().parse" });
28071
+ }
28072
+ return out2;
28073
+ }
28074
+ return out2;
28075
+ }
28076
+ fixFor(language) {
28077
+ if (language === "python") {
28078
+ return 'Always pass `options={"verify_signature": True}` (the default in PyJWT 2.0+) and a concrete `algorithms=["HS256"|"RS256"]` list. Never accept `none`.';
28079
+ }
28080
+ if (language === "javascript" || language === "typescript") {
28081
+ return 'Call `jwt.verify(token, secret, { algorithms: ["HS256" | "RS256"] })` with a non-empty key. Never use `algorithms: ["none"]` or pass null/empty as the secret.';
28082
+ }
28083
+ if (language === "java") {
28084
+ return "For auth0/java-jwt: use `JWT.require(Algorithm.HMAC256(secret))` or an RSA algorithm. For jjwt: call `parseClaimsJws(token)` (signature enforced) rather than `parse(token)` (signature ignored).";
28085
+ }
28086
+ return "Enforce JWT signature verification with a concrete algorithm (HS256/RS256/ES256). Never accept `alg: none`.";
28087
+ }
28088
+ };
28089
+
27833
28090
  // src/analysis/metrics/passes/size-metrics-pass.ts
27834
28091
  var SizeMetricsPass = class {
27835
28092
  name = "size-metrics";
@@ -28731,6 +28988,7 @@ async function analyze(code, filePath, language, options = {}) {
28731
28988
  if (!disabledPasses.has("weak-crypto")) pipeline.add(new WeakCryptoPass());
28732
28989
  if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
28733
28990
  if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
28991
+ if (!disabledPasses.has("jwt-verify-disabled")) pipeline.add(new JwtVerifyDisabledPass());
28734
28992
  const { results, findings } = pipeline.run(graph, code, language, config);
28735
28993
  const sinkFilter = results.get("sink-filter");
28736
28994
  const interProc = results.get("interprocedural");
@@ -10003,6 +10003,13 @@ var DEFAULT_SOURCES = [
10003
10003
  { method: "getFileName", class: "BodyPart", type: "file_input", severity: "high", return_tainted: true },
10004
10004
  { method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
10005
10005
  { method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
10006
+ // Archive entry names (Zip-Slip / Tar-Slip CWE-22, issue #52)
10007
+ // entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
10008
+ // is a classic Zip-Slip vulnerability.
10009
+ { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
10010
+ { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10011
+ { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10012
+ { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
10006
10013
  // Command line arguments
10007
10014
  { method: "getArgs", type: "io_input", severity: "high", return_tainted: true },
10008
10015
  { method: "getOptionValue", class: "CommandLine", type: "io_input", severity: "high", return_tainted: true },
@@ -10437,7 +10444,7 @@ var DEFAULT_SINKS = [
10437
10444
  { method: "staticFileLocation", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10438
10445
  // Zip/archive handling
10439
10446
  { method: "getEntry", class: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10440
- { method: "getName", class: "ZipEntry", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [] },
10447
+ // ZipEntry.getName moved to file_sources.yaml as a taint SOURCE (type=archive_entry, issue #52)
10441
10448
  // Resource loading classes (various frameworks)
10442
10449
  { method: "ClassPathResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10443
10450
  { method: "FileSystemResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -11472,7 +11479,58 @@ var DEFAULT_SINKS = [
11472
11479
  { method: "from_str", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
11473
11480
  { method: "from_reader", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
11474
11481
  { method: "from_str", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
11475
- { method: "from_slice", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] }
11482
+ { method: "from_slice", class: "serde_json", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
11483
+ // =========================================================================
11484
+ // ReDoS sinks (CWE-1333) — issue #86 / Sprint 5
11485
+ // =========================================================================
11486
+ // First argument of regex compile/match functions is the pattern. Tainted
11487
+ // patterns enable catastrophic-backtracking DoS.
11488
+ // Python: re.{match,search,compile,findall,fullmatch,sub,subn,split}
11489
+ { method: "match", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11490
+ { method: "search", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11491
+ { method: "fullmatch", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11492
+ { method: "compile", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11493
+ { method: "findall", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11494
+ { method: "finditer", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11495
+ { method: "sub", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11496
+ { method: "subn", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11497
+ { method: "split", class: "re", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["python"] },
11498
+ // Java: Pattern.compile / Pattern.matches; String.matches/replaceAll/replaceFirst/split
11499
+ { method: "compile", class: "Pattern", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11500
+ { method: "matches", class: "Pattern", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11501
+ { method: "matches", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11502
+ { method: "replaceAll", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11503
+ { method: "replaceFirst", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11504
+ { method: "split", class: "String", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["java"] },
11505
+ // JS/TS: new RegExp(pat) ctor; receiver_type === 'RegExp'. Also string.match
11506
+ // and string.matchAll, replace, search take a regex/string pattern.
11507
+ { method: "RegExp", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11508
+ // Go: regexp.Compile / MustCompile / Match / MatchString
11509
+ { method: "Compile", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
11510
+ { method: "MustCompile", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
11511
+ { method: "Match", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
11512
+ { method: "MatchString", class: "regexp", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["go"] },
11513
+ // =========================================================================
11514
+ // Format-string sinks (CWE-134) — issue #86 / Sprint 5
11515
+ // =========================================================================
11516
+ // First argument is the format string. Tainted format strings enable
11517
+ // information disclosure and (for C-style runtimes) memory writes.
11518
+ // Java: String.format / Formatter.format / printf / format on PrintStream
11519
+ // (note: printf/format on PrintWriter/PrintStream are already XSS sinks above)
11520
+ { method: "format", class: "String", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
11521
+ { method: "format", class: "Formatter", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
11522
+ { method: "printf", class: "System.out", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
11523
+ // NOTE: Python `userFmt.format(...)` and `userFmt % args` require
11524
+ // receiver-taint or operator-LHS-taint tracking — the format string is the
11525
+ // receiver, not an argument. Deferred to Sprint 6 (#86 follow-up).
11526
+ // C-style: printf / fprintf / sprintf / snprintf via ctypes/cffi.
11527
+ { method: "printf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["python"] },
11528
+ { method: "fprintf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [1], languages: ["python"] },
11529
+ // Go: fmt.Sprintf/Printf/Fprintf/Errorf — format string is first/second arg
11530
+ { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11531
+ { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11532
+ { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11533
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
11476
11534
  ];
11477
11535
  var DEFAULT_SANITIZERS = [
11478
11536
  // SQL Injection - proper parameter binding sanitizes input
@@ -12033,10 +12091,11 @@ function matchesSourcePattern(call, pattern) {
12033
12091
  return false;
12034
12092
  }
12035
12093
  if (pattern.class && pattern.class !== "constructor") {
12036
- if (!call.receiver) {
12094
+ if (call.receiver_type && call.receiver_type === pattern.class) {
12095
+ } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12096
+ } else if (!call.receiver) {
12037
12097
  return false;
12038
- }
12039
- if (!receiverMightBeClass(call.receiver, pattern.class)) {
12098
+ } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
12040
12099
  return false;
12041
12100
  }
12042
12101
  }
@@ -12294,13 +12353,14 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
12294
12353
  if (pattern.class === "constructor") {
12295
12354
  return true;
12296
12355
  }
12297
- if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
12356
+ if (call.receiver_type && call.receiver_type === pattern.class) {
12357
+ } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12358
+ } else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
12298
12359
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
12299
12360
  return true;
12300
12361
  }
12301
12362
  return false;
12302
- }
12303
- if (!call.receiver) {
12363
+ } else if (!call.receiver && !call.receiver_type) {
12304
12364
  return false;
12305
12365
  }
12306
12366
  }
@@ -13124,7 +13184,9 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
13124
13184
  "xxe",
13125
13185
  "deserialization",
13126
13186
  "code_injection",
13127
- "mybatis_mapper_call"
13187
+ "mybatis_mapper_call",
13188
+ "redos",
13189
+ "format_string"
13128
13190
  ]);
13129
13191
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
13130
13192
  const sanitizersAtTarget = sanitizersByLine.get(toLine);