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.
- package/configs/sinks/path.yaml +0 -16
- package/configs/sources/file_sources.yaml +32 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -1
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts +45 -0
- package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts.map +1 -0
- package/dist/analysis/passes/jwt-verify-disabled-pass.js +164 -0
- package/dist/analysis/passes/jwt-verify-disabled-pass.js.map +1 -0
- package/dist/analysis/passes/weak-crypto-pass.d.ts +17 -7
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.js +179 -10
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
- package/dist/analysis/rules.d.ts.map +1 -1
- package/dist/analysis/rules.js +18 -0
- package/dist/analysis/rules.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +28 -13
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +1 -0
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +3 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +269 -11
- package/dist/core/circle-ir-core.cjs +71 -9
- package/dist/core/circle-ir-core.js +71 -9
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- 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
|
-
|
|
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 (
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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);
|