circle-ir 3.51.0 → 3.53.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 +17 -20
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +23 -23
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +169 -79
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +52 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js +247 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -0
- package/dist/analysis/passes/weak-crypto-pass.d.ts +59 -0
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -0
- package/dist/analysis/passes/weak-crypto-pass.js +392 -0
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts +45 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -0
- package/dist/analysis/passes/weak-hash-pass.js +150 -0
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -0
- package/dist/analysis/passes/weak-random-pass.d.ts +53 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -0
- package/dist/analysis/passes/weak-random-pass.js +181 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -0
- 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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +12 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +852 -57
- package/dist/core/circle-ir-core.cjs +25 -26
- package/dist/core/circle-ir-core.js +25 -26
- 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] },
|
|
@@ -11532,26 +11539,16 @@ var DEFAULT_SINKS = [
|
|
|
11532
11539
|
{ method: "get", class: "WebClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
11533
11540
|
{ method: "post", class: "WebClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
11534
11541
|
// =============================================================================
|
|
11535
|
-
//
|
|
11542
|
+
// Config / Absence Vulnerabilities (handled by dedicated pattern passes)
|
|
11536
11543
|
// =============================================================================
|
|
11537
|
-
//
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
// Weak Hash (CWE-328) - MD5/SHA1 are cryptographically broken
|
|
11546
|
-
// Note: Detection requires checking algorithm argument - handled in runner
|
|
11547
|
-
{ method: "getInstance", class: "MessageDigest", type: "weak_hash", cwe: "CWE-328", severity: "medium", arg_positions: [0] },
|
|
11548
|
-
// Weak Crypto (CWE-327) - DES/RC4/Blowfish are weak ciphers
|
|
11549
|
-
// Note: Detection requires checking algorithm argument - handled in runner
|
|
11550
|
-
{ method: "getInstance", class: "Cipher", type: "weak_crypto", cwe: "CWE-327", severity: "high", arg_positions: [0] },
|
|
11551
|
-
{ method: "getInstance", class: "KeyGenerator", type: "weak_crypto", cwe: "CWE-327", severity: "high", arg_positions: [0] },
|
|
11552
|
-
// Insecure Cookie (CWE-614) - cookies without secure/httpOnly flags
|
|
11553
|
-
// Note: Detection requires checking if setSecure(true)/setHttpOnly(true) called - handled in runner
|
|
11554
|
-
{ method: "Cookie", class: "constructor", type: "insecure_cookie", cwe: "CWE-614", severity: "medium", arg_positions: [] },
|
|
11544
|
+
// weak_random → WeakRandomPass (src/analysis/passes/weak-random-pass.ts)
|
|
11545
|
+
// weak_hash → WeakHashPass (src/analysis/passes/weak-hash-pass.ts)
|
|
11546
|
+
// weak_crypto → WeakCryptoPass (src/analysis/passes/weak-crypto-pass.ts)
|
|
11547
|
+
// insecure_cookie → InsecureCookiePass (src/analysis/passes/insecure-cookie-pass.ts)
|
|
11548
|
+
// tls_verify_disabled → TlsVerifyDisabledPass
|
|
11549
|
+
// These patterns are detected by call-site literal inspection, not taint flow,
|
|
11550
|
+
// so they are NOT registered here as sinks (they could never match a "tainted
|
|
11551
|
+
// value flowing into a sink" because the bad value is a hard-coded constant).
|
|
11555
11552
|
// Trust Boundary (CWE-501) - using untrusted data as session attribute NAME
|
|
11556
11553
|
// The vulnerability is attacker controlling which key to use, not the value
|
|
11557
11554
|
{ method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
|
|
@@ -12748,10 +12745,11 @@ function matchesSourcePattern(call, pattern) {
|
|
|
12748
12745
|
return false;
|
|
12749
12746
|
}
|
|
12750
12747
|
if (pattern.class && pattern.class !== "constructor") {
|
|
12751
|
-
if (
|
|
12748
|
+
if (call.receiver_type && call.receiver_type === pattern.class) {
|
|
12749
|
+
} else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
|
|
12750
|
+
} else if (!call.receiver) {
|
|
12752
12751
|
return false;
|
|
12753
|
-
}
|
|
12754
|
-
if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
12752
|
+
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
12755
12753
|
return false;
|
|
12756
12754
|
}
|
|
12757
12755
|
}
|
|
@@ -13009,13 +13007,14 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
13009
13007
|
if (pattern.class === "constructor") {
|
|
13010
13008
|
return true;
|
|
13011
13009
|
}
|
|
13012
|
-
if (call.
|
|
13010
|
+
if (call.receiver_type && call.receiver_type === pattern.class) {
|
|
13011
|
+
} else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
|
|
13012
|
+
} else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
|
|
13013
13013
|
if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
|
|
13014
13014
|
return true;
|
|
13015
13015
|
}
|
|
13016
13016
|
return false;
|
|
13017
|
-
}
|
|
13018
|
-
if (!call.receiver) {
|
|
13017
|
+
} else if (!call.receiver && !call.receiver_type) {
|
|
13019
13018
|
return false;
|
|
13020
13019
|
}
|
|
13021
13020
|
}
|
|
@@ -27081,59 +27080,851 @@ var COOKIE_RESPONSE_RECEIVERS = /* @__PURE__ */ new Set([
|
|
|
27081
27080
|
]);
|
|
27082
27081
|
var SECURE_TRUE_RE = /\bsecure\s*:\s*true\b/;
|
|
27083
27082
|
var HTTPONLY_TRUE_RE = /\bhttpOnly\s*:\s*true\b/i;
|
|
27083
|
+
var PY_SET_COOKIE_RECEIVERS = /* @__PURE__ */ new Set([
|
|
27084
|
+
"response",
|
|
27085
|
+
"resp",
|
|
27086
|
+
"res"
|
|
27087
|
+
]);
|
|
27088
|
+
var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
|
|
27089
|
+
var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
|
|
27090
|
+
var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
|
|
27091
|
+
var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
|
|
27084
27092
|
var InsecureCookiePass = class {
|
|
27085
27093
|
name = "insecure-cookie";
|
|
27086
27094
|
category = "security";
|
|
27087
27095
|
run(ctx) {
|
|
27088
|
-
const { graph, language } = ctx;
|
|
27089
|
-
if (language !== "javascript" && language !== "typescript") {
|
|
27090
|
-
return { insecureCookies: [] };
|
|
27091
|
-
}
|
|
27096
|
+
const { graph, language, code } = ctx;
|
|
27092
27097
|
const file = graph.ir.meta.file;
|
|
27093
27098
|
const insecureCookies = [];
|
|
27099
|
+
if (language === "javascript" || language === "typescript") {
|
|
27100
|
+
for (const call of graph.ir.calls) {
|
|
27101
|
+
const det = this.detectJs(call);
|
|
27102
|
+
if (!det) continue;
|
|
27103
|
+
insecureCookies.push(det);
|
|
27104
|
+
this.emit(ctx, file, det, "js");
|
|
27105
|
+
}
|
|
27106
|
+
} else if (language === "python") {
|
|
27107
|
+
for (const call of graph.ir.calls) {
|
|
27108
|
+
const det = this.detectPython(call);
|
|
27109
|
+
if (!det) continue;
|
|
27110
|
+
insecureCookies.push(det);
|
|
27111
|
+
this.emit(ctx, file, det, "python");
|
|
27112
|
+
}
|
|
27113
|
+
} else if (language === "java") {
|
|
27114
|
+
const hasSetSecureTrue = JAVA_SET_SECURE_TRUE_RE.test(code);
|
|
27115
|
+
const hasSetHttpOnlyTrue = JAVA_SET_HTTPONLY_TRUE_RE.test(code);
|
|
27116
|
+
for (const call of graph.ir.calls) {
|
|
27117
|
+
const det = this.detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue);
|
|
27118
|
+
if (!det) continue;
|
|
27119
|
+
insecureCookies.push(det);
|
|
27120
|
+
this.emit(ctx, file, det, "java");
|
|
27121
|
+
}
|
|
27122
|
+
}
|
|
27123
|
+
return { insecureCookies };
|
|
27124
|
+
}
|
|
27125
|
+
// ---------------- JS / TS ----------------
|
|
27126
|
+
detectJs(call) {
|
|
27127
|
+
if (call.method_name !== "cookie") return null;
|
|
27128
|
+
const receiver = call.receiver ?? "";
|
|
27129
|
+
if (!COOKIE_RESPONSE_RECEIVERS.has(receiver)) return null;
|
|
27130
|
+
if (call.arguments.length < 2) return null;
|
|
27131
|
+
const opts = call.arguments.find((a) => a.position === 2);
|
|
27132
|
+
const optsExpr = (opts?.expression ?? "").trim();
|
|
27133
|
+
const optionsPresent = optsExpr.length > 0;
|
|
27134
|
+
const missingSecure = !SECURE_TRUE_RE.test(optsExpr);
|
|
27135
|
+
const missingHttpOnly = !HTTPONLY_TRUE_RE.test(optsExpr);
|
|
27136
|
+
if (!missingSecure && !missingHttpOnly) return null;
|
|
27137
|
+
return {
|
|
27138
|
+
line: call.location.line,
|
|
27139
|
+
receiver,
|
|
27140
|
+
missingSecure,
|
|
27141
|
+
missingHttpOnly,
|
|
27142
|
+
optionsPresent
|
|
27143
|
+
};
|
|
27144
|
+
}
|
|
27145
|
+
// ---------------- Python ----------------
|
|
27146
|
+
detectPython(call) {
|
|
27147
|
+
if (call.method_name !== "set_cookie") return null;
|
|
27148
|
+
const receiver = call.receiver ?? "";
|
|
27149
|
+
if (!PY_SET_COOKIE_RECEIVERS.has(receiver)) return null;
|
|
27150
|
+
const argsBlob = call.arguments.map((a) => a.expression ?? "").join(", ");
|
|
27151
|
+
const missingSecure = !PY_SECURE_TRUE_RE.test(argsBlob);
|
|
27152
|
+
const missingHttpOnly = !PY_HTTPONLY_TRUE_RE.test(argsBlob);
|
|
27153
|
+
if (!missingSecure && !missingHttpOnly) return null;
|
|
27154
|
+
return {
|
|
27155
|
+
line: call.location.line,
|
|
27156
|
+
receiver,
|
|
27157
|
+
missingSecure,
|
|
27158
|
+
missingHttpOnly,
|
|
27159
|
+
optionsPresent: call.arguments.length >= 2
|
|
27160
|
+
};
|
|
27161
|
+
}
|
|
27162
|
+
// ---------------- Java ----------------
|
|
27163
|
+
detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue) {
|
|
27164
|
+
if (call.method_name !== "Cookie") return null;
|
|
27165
|
+
const looksLikeCtor = call.is_constructor || !call.receiver && call.receiver_type === "Cookie" || (call.resolution?.target ?? "").endsWith(".<init>");
|
|
27166
|
+
if (!looksLikeCtor) return null;
|
|
27167
|
+
if (call.arguments.length < 2) return null;
|
|
27168
|
+
const missingSecure = !hasSetSecureTrue;
|
|
27169
|
+
const missingHttpOnly = !hasSetHttpOnlyTrue;
|
|
27170
|
+
if (!missingSecure && !missingHttpOnly) return null;
|
|
27171
|
+
return {
|
|
27172
|
+
line: call.location.line,
|
|
27173
|
+
receiver: "new Cookie",
|
|
27174
|
+
missingSecure,
|
|
27175
|
+
missingHttpOnly,
|
|
27176
|
+
optionsPresent: false
|
|
27177
|
+
};
|
|
27178
|
+
}
|
|
27179
|
+
emit(ctx, file, det, flavor) {
|
|
27180
|
+
const missing = [];
|
|
27181
|
+
if (det.missingSecure) missing.push(flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : "`setSecure(true)`");
|
|
27182
|
+
if (det.missingHttpOnly) missing.push(flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : "`setHttpOnly(true)`");
|
|
27183
|
+
const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response.";
|
|
27184
|
+
ctx.addFinding({
|
|
27185
|
+
id: `${this.name}-${file}-${det.line}`,
|
|
27186
|
+
pass: this.name,
|
|
27187
|
+
category: this.category,
|
|
27188
|
+
rule_id: this.name,
|
|
27189
|
+
cwe: "CWE-614",
|
|
27190
|
+
severity: "medium",
|
|
27191
|
+
level: "warning",
|
|
27192
|
+
message: `Cookie set without ${missing.join(" and ")} \u2014 vulnerable to cleartext transmission (CWE-614) and client-side JS access (CWE-1004).`,
|
|
27193
|
+
file,
|
|
27194
|
+
line: det.line,
|
|
27195
|
+
fix,
|
|
27196
|
+
evidence: {
|
|
27197
|
+
receiver: det.receiver,
|
|
27198
|
+
options_present: det.optionsPresent,
|
|
27199
|
+
missing_secure: det.missingSecure,
|
|
27200
|
+
missing_http_only: det.missingHttpOnly
|
|
27201
|
+
}
|
|
27202
|
+
});
|
|
27203
|
+
}
|
|
27204
|
+
};
|
|
27205
|
+
|
|
27206
|
+
// src/analysis/passes/weak-hash-pass.ts
|
|
27207
|
+
var WEAK_HASH_NAMES = /* @__PURE__ */ new Set([
|
|
27208
|
+
"md2",
|
|
27209
|
+
"md4",
|
|
27210
|
+
"md5",
|
|
27211
|
+
"sha-1",
|
|
27212
|
+
"sha1"
|
|
27213
|
+
]);
|
|
27214
|
+
var COMMONS_DIGEST_METHODS = /* @__PURE__ */ new Set([
|
|
27215
|
+
"md2",
|
|
27216
|
+
"md2Hex",
|
|
27217
|
+
"md5",
|
|
27218
|
+
"md5Hex",
|
|
27219
|
+
"sha1",
|
|
27220
|
+
"sha1Hex",
|
|
27221
|
+
// Apache Commons also has the misnamed `sha(...)` which is SHA-1
|
|
27222
|
+
"sha",
|
|
27223
|
+
"shaHex"
|
|
27224
|
+
]);
|
|
27225
|
+
var PY_HASHLIB_WEAK = /* @__PURE__ */ new Set(["md5", "sha1", "md4", "md2", "new"]);
|
|
27226
|
+
function stripQuotes4(s) {
|
|
27227
|
+
const trimmed = s.trim();
|
|
27228
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith("`") && trimmed.endsWith("`")) {
|
|
27229
|
+
return trimmed.slice(1, -1);
|
|
27230
|
+
}
|
|
27231
|
+
return trimmed;
|
|
27232
|
+
}
|
|
27233
|
+
function literalAlgo(call, position) {
|
|
27234
|
+
const arg = call.arguments.find((a) => a.position === position);
|
|
27235
|
+
if (!arg) return null;
|
|
27236
|
+
const raw = arg.literal ?? arg.expression ?? "";
|
|
27237
|
+
const cleaned = stripQuotes4(raw).toLowerCase();
|
|
27238
|
+
return cleaned || null;
|
|
27239
|
+
}
|
|
27240
|
+
var WeakHashPass = class {
|
|
27241
|
+
name = "weak-hash";
|
|
27242
|
+
category = "security";
|
|
27243
|
+
run(ctx) {
|
|
27244
|
+
const { graph, language } = ctx;
|
|
27245
|
+
const file = graph.ir.meta.file;
|
|
27246
|
+
const findings = [];
|
|
27094
27247
|
for (const call of graph.ir.calls) {
|
|
27095
|
-
|
|
27096
|
-
|
|
27097
|
-
|
|
27098
|
-
if (call.arguments.length < 2) continue;
|
|
27099
|
-
const opts = call.arguments.find((a) => a.position === 2);
|
|
27100
|
-
const optsExpr = (opts?.expression ?? "").trim();
|
|
27101
|
-
const optionsPresent = optsExpr.length > 0;
|
|
27102
|
-
const missingSecure = !SECURE_TRUE_RE.test(optsExpr);
|
|
27103
|
-
const missingHttpOnly = !HTTPONLY_TRUE_RE.test(optsExpr);
|
|
27104
|
-
if (!missingSecure && !missingHttpOnly) continue;
|
|
27248
|
+
const detection = this.detect(call, language);
|
|
27249
|
+
if (!detection) continue;
|
|
27250
|
+
const { algorithm, api } = detection;
|
|
27105
27251
|
const line = call.location.line;
|
|
27106
|
-
|
|
27252
|
+
findings.push({ line, language, algorithm, api });
|
|
27253
|
+
ctx.addFinding({
|
|
27254
|
+
id: `${this.name}-${file}-${line}`,
|
|
27255
|
+
pass: this.name,
|
|
27256
|
+
category: this.category,
|
|
27257
|
+
rule_id: this.name,
|
|
27258
|
+
cwe: "CWE-328",
|
|
27259
|
+
severity: "medium",
|
|
27260
|
+
level: "warning",
|
|
27261
|
+
message: `Weak hash algorithm \`${algorithm.toUpperCase()}\` used via \`${api}\`. MD2/MD4/MD5/SHA-1 are cryptographically broken and must not be used for passwords, signatures, integrity checks, or anywhere collision resistance is required.`,
|
|
27262
|
+
file,
|
|
27107
27263
|
line,
|
|
27108
|
-
|
|
27109
|
-
|
|
27110
|
-
missingHttpOnly,
|
|
27111
|
-
optionsPresent
|
|
27264
|
+
fix: "Use SHA-256 or stronger (SHA-384, SHA-512, SHA-3). For passwords, use a password-hashing function: bcrypt, scrypt, Argon2, or PBKDF2.",
|
|
27265
|
+
evidence: { algorithm, api, language }
|
|
27112
27266
|
});
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27267
|
+
}
|
|
27268
|
+
return { findings };
|
|
27269
|
+
}
|
|
27270
|
+
detect(call, language) {
|
|
27271
|
+
const method = call.method_name;
|
|
27272
|
+
const receiver = call.receiver ?? "";
|
|
27273
|
+
if (language === "java") {
|
|
27274
|
+
if (method === "getInstance" && (receiver === "MessageDigest" || receiver.endsWith(".MessageDigest"))) {
|
|
27275
|
+
const algo = literalAlgo(call, 0);
|
|
27276
|
+
if (algo && WEAK_HASH_NAMES.has(algo)) {
|
|
27277
|
+
return { algorithm: algo, api: "MessageDigest.getInstance" };
|
|
27278
|
+
}
|
|
27279
|
+
}
|
|
27280
|
+
if (COMMONS_DIGEST_METHODS.has(method) && (receiver === "DigestUtils" || receiver.endsWith(".DigestUtils"))) {
|
|
27281
|
+
const algoFromMethod = method.toLowerCase().replace(/hex$/, "");
|
|
27282
|
+
const normalized = algoFromMethod === "sha" ? "sha1" : algoFromMethod;
|
|
27283
|
+
return { algorithm: normalized, api: `DigestUtils.${method}` };
|
|
27284
|
+
}
|
|
27285
|
+
return null;
|
|
27286
|
+
}
|
|
27287
|
+
if (language === "python") {
|
|
27288
|
+
if ((receiver === "hashlib" || receiver.endsWith(".hashlib")) && PY_HASHLIB_WEAK.has(method)) {
|
|
27289
|
+
if (method === "new") {
|
|
27290
|
+
const algo = literalAlgo(call, 0);
|
|
27291
|
+
if (algo && WEAK_HASH_NAMES.has(algo)) {
|
|
27292
|
+
return { algorithm: algo, api: "hashlib.new" };
|
|
27293
|
+
}
|
|
27294
|
+
return null;
|
|
27295
|
+
}
|
|
27296
|
+
return { algorithm: method, api: `hashlib.${method}` };
|
|
27297
|
+
}
|
|
27298
|
+
return null;
|
|
27299
|
+
}
|
|
27300
|
+
if (language === "javascript" || language === "typescript") {
|
|
27301
|
+
if ((method === "createHash" || method === "createHmac") && receiver === "crypto") {
|
|
27302
|
+
const algo = literalAlgo(call, 0);
|
|
27303
|
+
if (algo && WEAK_HASH_NAMES.has(algo)) {
|
|
27304
|
+
return { algorithm: algo, api: `crypto.${method}` };
|
|
27305
|
+
}
|
|
27306
|
+
}
|
|
27307
|
+
return null;
|
|
27308
|
+
}
|
|
27309
|
+
if (language === "go") {
|
|
27310
|
+
const isWeakPkg = receiver === "md5" || receiver === "sha1";
|
|
27311
|
+
if (isWeakPkg && (method === "New" || method === "Sum")) {
|
|
27312
|
+
return { algorithm: receiver, api: `${receiver}.${method}` };
|
|
27313
|
+
}
|
|
27314
|
+
return null;
|
|
27315
|
+
}
|
|
27316
|
+
return null;
|
|
27317
|
+
}
|
|
27318
|
+
};
|
|
27319
|
+
|
|
27320
|
+
// src/analysis/passes/weak-crypto-pass.ts
|
|
27321
|
+
var WEAK_CIPHER_BASES = /* @__PURE__ */ new Set([
|
|
27322
|
+
"des",
|
|
27323
|
+
"3des",
|
|
27324
|
+
"desede",
|
|
27325
|
+
"tripledes",
|
|
27326
|
+
"rc2",
|
|
27327
|
+
"rc4",
|
|
27328
|
+
"arc4",
|
|
27329
|
+
"blowfish",
|
|
27330
|
+
"bf",
|
|
27331
|
+
"idea",
|
|
27332
|
+
"seed",
|
|
27333
|
+
"cast5"
|
|
27334
|
+
]);
|
|
27335
|
+
function classifyJavaCipherSpec(spec) {
|
|
27336
|
+
const parts2 = spec.split("/").map((p) => p.trim().toLowerCase());
|
|
27337
|
+
const base = parts2[0] ?? "";
|
|
27338
|
+
const mode = parts2[1] ?? "";
|
|
27339
|
+
const result = {};
|
|
27340
|
+
if (WEAK_CIPHER_BASES.has(base)) result.weakBase = base;
|
|
27341
|
+
if (mode === "ecb") result.ecb = true;
|
|
27342
|
+
if (parts2.length === 1 && base === "aes") result.ecb = true;
|
|
27343
|
+
return result;
|
|
27344
|
+
}
|
|
27345
|
+
function stripQuotes5(s) {
|
|
27346
|
+
const t = s.trim();
|
|
27347
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") || t.startsWith("`") && t.endsWith("`")) {
|
|
27348
|
+
return t.slice(1, -1);
|
|
27349
|
+
}
|
|
27350
|
+
return t;
|
|
27351
|
+
}
|
|
27352
|
+
function literalAlgo2(call, position) {
|
|
27353
|
+
const arg = call.arguments.find((a) => a.position === position);
|
|
27354
|
+
if (!arg) return null;
|
|
27355
|
+
const raw = arg.literal ?? arg.expression ?? "";
|
|
27356
|
+
const cleaned = stripQuotes5(raw);
|
|
27357
|
+
return cleaned || null;
|
|
27358
|
+
}
|
|
27359
|
+
function detectStaticIvJava(call) {
|
|
27360
|
+
const arg = call.arguments.find((a) => a.position === 0);
|
|
27361
|
+
if (!arg) return null;
|
|
27362
|
+
const expr = (arg.literal ?? arg.expression ?? "").trim();
|
|
27363
|
+
if (!expr) return null;
|
|
27364
|
+
if (/^new\s+byte\s*\[[^\]]*\]\s*$/.test(expr)) {
|
|
27365
|
+
return `zero-filled ${expr}`;
|
|
27366
|
+
}
|
|
27367
|
+
if (/^new\s+byte\s*\[\s*\]\s*\{[^}]*\}\s*$/.test(expr)) {
|
|
27368
|
+
return `literal byte[] initializer`;
|
|
27369
|
+
}
|
|
27370
|
+
if (/^"[^"]*"\.getBytes\s*\(/.test(expr)) {
|
|
27371
|
+
return `literal string .getBytes()`;
|
|
27372
|
+
}
|
|
27373
|
+
if (/^"[^"]*"$/.test(expr)) {
|
|
27374
|
+
return `literal string`;
|
|
27375
|
+
}
|
|
27376
|
+
return null;
|
|
27377
|
+
}
|
|
27378
|
+
function isJavaCtor(call, className) {
|
|
27379
|
+
if (call.is_constructor === true) return true;
|
|
27380
|
+
if (call.receiver) return false;
|
|
27381
|
+
if (call.receiver_type === className) return true;
|
|
27382
|
+
if ((call.receiver_type_fqn ?? "").endsWith("." + className)) return true;
|
|
27383
|
+
return false;
|
|
27384
|
+
}
|
|
27385
|
+
function detectHardcodedKeyJava(call) {
|
|
27386
|
+
const arg = call.arguments.find((a) => a.position === 0);
|
|
27387
|
+
if (!arg) return null;
|
|
27388
|
+
const expr = (arg.literal ?? arg.expression ?? "").trim();
|
|
27389
|
+
if (!expr) return null;
|
|
27390
|
+
if (/^"[^"]*"\.getBytes\s*\(/.test(expr)) return `literal string .getBytes()`;
|
|
27391
|
+
if (/^new\s+byte\s*\[\s*\]\s*\{[^}]*\}\s*$/.test(expr)) return `literal byte[] initializer`;
|
|
27392
|
+
if (/^"[^"]*"$/.test(expr)) return `literal string`;
|
|
27393
|
+
return null;
|
|
27394
|
+
}
|
|
27395
|
+
var ISSUE_CWE = {
|
|
27396
|
+
"weak-cipher": "CWE-327",
|
|
27397
|
+
"ecb-mode": "CWE-327",
|
|
27398
|
+
"deprecated-api": "CWE-327",
|
|
27399
|
+
"static-iv": "CWE-329",
|
|
27400
|
+
"hardcoded-key": "CWE-321",
|
|
27401
|
+
"weak-rsa-key": "CWE-326"
|
|
27402
|
+
};
|
|
27403
|
+
var WeakCryptoPass = class {
|
|
27404
|
+
name = "weak-crypto";
|
|
27405
|
+
category = "security";
|
|
27406
|
+
run(ctx) {
|
|
27407
|
+
const { graph, language } = ctx;
|
|
27408
|
+
const file = graph.ir.meta.file;
|
|
27409
|
+
const findings = [];
|
|
27410
|
+
for (const call of graph.ir.calls) {
|
|
27411
|
+
const detections = this.detect(call, language);
|
|
27412
|
+
for (const det of detections) {
|
|
27413
|
+
const line = call.location.line;
|
|
27414
|
+
findings.push({ line, language, ...det });
|
|
27415
|
+
const message = this.buildMessage(det);
|
|
27416
|
+
ctx.addFinding({
|
|
27417
|
+
id: `${this.name}-${file}-${line}-${det.issue}`,
|
|
27418
|
+
pass: this.name,
|
|
27419
|
+
category: this.category,
|
|
27420
|
+
rule_id: this.name,
|
|
27421
|
+
cwe: ISSUE_CWE[det.issue],
|
|
27422
|
+
severity: "high",
|
|
27423
|
+
level: "error",
|
|
27424
|
+
message,
|
|
27425
|
+
file,
|
|
27426
|
+
line,
|
|
27427
|
+
fix: this.buildFix(det.issue),
|
|
27428
|
+
evidence: { ...det, language }
|
|
27429
|
+
});
|
|
27430
|
+
}
|
|
27431
|
+
}
|
|
27432
|
+
return { findings };
|
|
27433
|
+
}
|
|
27434
|
+
buildMessage(det) {
|
|
27435
|
+
switch (det.issue) {
|
|
27436
|
+
case "weak-cipher":
|
|
27437
|
+
return `Weak symmetric cipher \`${det.detail.toUpperCase()}\` used via \`${det.api}\`. DES, 3DES, RC2, RC4, Blowfish, and IDEA/SEED/CAST5 are deprecated and broken at modern key sizes.`;
|
|
27438
|
+
case "ecb-mode":
|
|
27439
|
+
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.`;
|
|
27440
|
+
case "deprecated-api":
|
|
27441
|
+
return `Deprecated crypto API \`${det.api}\` used (no IV: \`${det.detail}\`). This API derives the key/IV from a password in an insecure way.`;
|
|
27442
|
+
case "static-iv":
|
|
27443
|
+
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.`;
|
|
27444
|
+
case "hardcoded-key":
|
|
27445
|
+
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.`;
|
|
27446
|
+
case "weak-rsa-key":
|
|
27447
|
+
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.`;
|
|
27448
|
+
default:
|
|
27449
|
+
return `Weak cryptography: ${det.detail} (${det.api})`;
|
|
27450
|
+
}
|
|
27451
|
+
}
|
|
27452
|
+
buildFix(issue) {
|
|
27453
|
+
switch (issue) {
|
|
27454
|
+
case "static-iv":
|
|
27455
|
+
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.";
|
|
27456
|
+
case "hardcoded-key":
|
|
27457
|
+
return "Load the key from a secure key management system (HSM, KMS, Vault) or platform keystore. Never embed key material in source code.";
|
|
27458
|
+
case "weak-rsa-key":
|
|
27459
|
+
return "Initialize KeyPairGenerator with at least 2048 bits (preferably 3072 or 4096) for RSA, or switch to EC keys (P-256+).";
|
|
27460
|
+
default:
|
|
27461
|
+
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.";
|
|
27462
|
+
}
|
|
27463
|
+
}
|
|
27464
|
+
detect(call, language) {
|
|
27465
|
+
const method = call.method_name;
|
|
27466
|
+
const receiver = call.receiver ?? "";
|
|
27467
|
+
const out2 = [];
|
|
27468
|
+
if (language === "java") {
|
|
27469
|
+
const isCipherFactory = method === "getInstance" && (receiver === "Cipher" || receiver.endsWith(".Cipher") || receiver === "KeyGenerator" || receiver.endsWith(".KeyGenerator"));
|
|
27470
|
+
if (isCipherFactory) {
|
|
27471
|
+
const spec = literalAlgo2(call, 0);
|
|
27472
|
+
if (spec) {
|
|
27473
|
+
const { weakBase, ecb } = classifyJavaCipherSpec(spec);
|
|
27474
|
+
const api = `${receiver}.getInstance`;
|
|
27475
|
+
if (weakBase) out2.push({ issue: "weak-cipher", detail: weakBase, api });
|
|
27476
|
+
if (ecb) out2.push({ issue: "ecb-mode", detail: spec, api });
|
|
27477
|
+
}
|
|
27478
|
+
}
|
|
27479
|
+
if (method === "IvParameterSpec" && isJavaCtor(call, "IvParameterSpec")) {
|
|
27480
|
+
const ivDetail = detectStaticIvJava(call);
|
|
27481
|
+
if (ivDetail) {
|
|
27482
|
+
out2.push({ issue: "static-iv", detail: ivDetail, api: "new IvParameterSpec" });
|
|
27483
|
+
}
|
|
27484
|
+
}
|
|
27485
|
+
if (method === "SecretKeySpec" && isJavaCtor(call, "SecretKeySpec")) {
|
|
27486
|
+
const keyDetail = detectHardcodedKeyJava(call);
|
|
27487
|
+
if (keyDetail) {
|
|
27488
|
+
out2.push({ issue: "hardcoded-key", detail: keyDetail, api: "new SecretKeySpec" });
|
|
27489
|
+
}
|
|
27490
|
+
}
|
|
27491
|
+
if (method === "initialize") {
|
|
27492
|
+
const isKpg = call.receiver_type === "KeyPairGenerator" || (call.receiver_type_fqn ?? "").endsWith(".KeyPairGenerator");
|
|
27493
|
+
if (isKpg) {
|
|
27494
|
+
const sizeArg = call.arguments.find((a) => a.position === 0);
|
|
27495
|
+
const expr = (sizeArg?.literal ?? sizeArg?.expression ?? "").trim();
|
|
27496
|
+
const n = parseInt(expr, 10);
|
|
27497
|
+
if (Number.isFinite(n) && n > 0 && n < 2048) {
|
|
27498
|
+
out2.push({
|
|
27499
|
+
issue: "weak-rsa-key",
|
|
27500
|
+
detail: String(n),
|
|
27501
|
+
api: "KeyPairGenerator.initialize"
|
|
27502
|
+
});
|
|
27503
|
+
}
|
|
27504
|
+
}
|
|
27505
|
+
}
|
|
27506
|
+
return out2;
|
|
27507
|
+
}
|
|
27508
|
+
if (language === "python") {
|
|
27509
|
+
if (method === "new") {
|
|
27510
|
+
const rcvLower = receiver.toLowerCase();
|
|
27511
|
+
const lastSeg = rcvLower.split(".").pop() ?? rcvLower;
|
|
27512
|
+
if (WEAK_CIPHER_BASES.has(lastSeg)) {
|
|
27513
|
+
out2.push({ issue: "weak-cipher", detail: lastSeg, api: `${receiver}.new` });
|
|
27514
|
+
}
|
|
27515
|
+
if (lastSeg === "aes" || lastSeg.endsWith(".aes")) {
|
|
27516
|
+
const mode = call.arguments.find((a) => a.position === 1);
|
|
27517
|
+
const modeExpr = (mode?.expression ?? "").trim();
|
|
27518
|
+
if (/\bMODE_ECB\b/.test(modeExpr)) {
|
|
27519
|
+
out2.push({ issue: "ecb-mode", detail: "AES.MODE_ECB", api: `${receiver}.new` });
|
|
27520
|
+
}
|
|
27521
|
+
}
|
|
27522
|
+
}
|
|
27523
|
+
const isHazmatAlgos = receiver === "algorithms" || receiver.endsWith(".algorithms");
|
|
27524
|
+
if (isHazmatAlgos) {
|
|
27525
|
+
const m = method.toLowerCase();
|
|
27526
|
+
const normalized = m === "tripledes" ? "3des" : m;
|
|
27527
|
+
if (WEAK_CIPHER_BASES.has(normalized)) {
|
|
27528
|
+
out2.push({ issue: "weak-cipher", detail: normalized, api: `algorithms.${method}` });
|
|
27529
|
+
}
|
|
27530
|
+
}
|
|
27531
|
+
return out2;
|
|
27532
|
+
}
|
|
27533
|
+
if (language === "javascript" || language === "typescript") {
|
|
27534
|
+
if (method === "createCipher" && receiver === "crypto") {
|
|
27535
|
+
const algo = literalAlgo2(call, 0) ?? "<unknown>";
|
|
27536
|
+
out2.push({ issue: "deprecated-api", detail: algo, api: "crypto.createCipher" });
|
|
27537
|
+
}
|
|
27538
|
+
if (method === "createCipheriv" && receiver === "crypto") {
|
|
27539
|
+
const algo = literalAlgo2(call, 0);
|
|
27540
|
+
if (algo) {
|
|
27541
|
+
const lower = algo.toLowerCase();
|
|
27542
|
+
const parts2 = lower.split("-");
|
|
27543
|
+
const base = parts2[0];
|
|
27544
|
+
const mode = parts2[parts2.length - 1];
|
|
27545
|
+
let normalizedBase = base;
|
|
27546
|
+
if (base === "bf") normalizedBase = "blowfish";
|
|
27547
|
+
if (base === "desede" || base === "des-ede3" || base === "des3") normalizedBase = "3des";
|
|
27548
|
+
if (WEAK_CIPHER_BASES.has(normalizedBase)) {
|
|
27549
|
+
out2.push({ issue: "weak-cipher", detail: normalizedBase, api: "crypto.createCipheriv" });
|
|
27550
|
+
}
|
|
27551
|
+
if (mode === "ecb") {
|
|
27552
|
+
out2.push({ issue: "ecb-mode", detail: lower, api: "crypto.createCipheriv" });
|
|
27553
|
+
}
|
|
27554
|
+
}
|
|
27555
|
+
}
|
|
27556
|
+
return out2;
|
|
27557
|
+
}
|
|
27558
|
+
if (language === "go") {
|
|
27559
|
+
if (receiver === "des" && (method === "NewCipher" || method === "NewTripleDESCipher")) {
|
|
27560
|
+
const base = method === "NewTripleDESCipher" ? "3des" : "des";
|
|
27561
|
+
out2.push({ issue: "weak-cipher", detail: base, api: `des.${method}` });
|
|
27562
|
+
}
|
|
27563
|
+
if (receiver === "rc4" && method === "NewCipher") {
|
|
27564
|
+
out2.push({ issue: "weak-cipher", detail: "rc4", api: "rc4.NewCipher" });
|
|
27565
|
+
}
|
|
27566
|
+
if ((method === "NewECBEncrypter" || method === "NewECBDecrypter") && receiver === "cipher") {
|
|
27567
|
+
out2.push({ issue: "ecb-mode", detail: method, api: `cipher.${method}` });
|
|
27568
|
+
}
|
|
27569
|
+
return out2;
|
|
27570
|
+
}
|
|
27571
|
+
return out2;
|
|
27572
|
+
}
|
|
27573
|
+
};
|
|
27574
|
+
|
|
27575
|
+
// src/analysis/passes/weak-random-pass.ts
|
|
27576
|
+
var JAVA_RANDOM_METHODS = /* @__PURE__ */ new Set([
|
|
27577
|
+
"nextInt",
|
|
27578
|
+
"nextLong",
|
|
27579
|
+
"nextFloat",
|
|
27580
|
+
"nextDouble",
|
|
27581
|
+
"nextBoolean",
|
|
27582
|
+
"nextBytes",
|
|
27583
|
+
"nextGaussian",
|
|
27584
|
+
"ints",
|
|
27585
|
+
"longs",
|
|
27586
|
+
"doubles"
|
|
27587
|
+
]);
|
|
27588
|
+
var PY_RANDOM_FUNCS = /* @__PURE__ */ new Set([
|
|
27589
|
+
"random",
|
|
27590
|
+
"randint",
|
|
27591
|
+
"choice",
|
|
27592
|
+
"uniform",
|
|
27593
|
+
"shuffle",
|
|
27594
|
+
"getrandbits",
|
|
27595
|
+
"sample",
|
|
27596
|
+
"choices",
|
|
27597
|
+
"randrange",
|
|
27598
|
+
"seed",
|
|
27599
|
+
"gauss",
|
|
27600
|
+
"normalvariate",
|
|
27601
|
+
"expovariate",
|
|
27602
|
+
"paretovariate",
|
|
27603
|
+
"weibullvariate",
|
|
27604
|
+
"triangular",
|
|
27605
|
+
"lognormvariate",
|
|
27606
|
+
"vonmisesvariate",
|
|
27607
|
+
"betavariate",
|
|
27608
|
+
"gammavariate"
|
|
27609
|
+
]);
|
|
27610
|
+
var GO_RAND_FUNCS = /* @__PURE__ */ new Set([
|
|
27611
|
+
"Int",
|
|
27612
|
+
"Intn",
|
|
27613
|
+
"Int31",
|
|
27614
|
+
"Int31n",
|
|
27615
|
+
"Int63",
|
|
27616
|
+
"Int63n",
|
|
27617
|
+
"Float32",
|
|
27618
|
+
"Float64",
|
|
27619
|
+
"NormFloat64",
|
|
27620
|
+
"ExpFloat64",
|
|
27621
|
+
"Perm",
|
|
27622
|
+
"Shuffle",
|
|
27623
|
+
"Read",
|
|
27624
|
+
"Uint32",
|
|
27625
|
+
"Uint64",
|
|
27626
|
+
"Seed",
|
|
27627
|
+
"New",
|
|
27628
|
+
"NewSource"
|
|
27629
|
+
]);
|
|
27630
|
+
var WeakRandomPass = class {
|
|
27631
|
+
name = "weak-random";
|
|
27632
|
+
category = "security";
|
|
27633
|
+
run(ctx) {
|
|
27634
|
+
const { graph, language } = ctx;
|
|
27635
|
+
const file = graph.ir.meta.file;
|
|
27636
|
+
const findings = [];
|
|
27637
|
+
for (const call of graph.ir.calls) {
|
|
27638
|
+
const api = this.detect(call, language, ctx);
|
|
27639
|
+
if (!api) continue;
|
|
27640
|
+
const line = call.location.line;
|
|
27641
|
+
findings.push({ line, language, api });
|
|
27116
27642
|
ctx.addFinding({
|
|
27117
27643
|
id: `${this.name}-${file}-${line}`,
|
|
27118
27644
|
pass: this.name,
|
|
27119
27645
|
category: this.category,
|
|
27120
27646
|
rule_id: this.name,
|
|
27121
|
-
cwe: "CWE-
|
|
27647
|
+
cwe: "CWE-330",
|
|
27122
27648
|
severity: "medium",
|
|
27123
27649
|
level: "warning",
|
|
27124
|
-
message: `
|
|
27650
|
+
message: `Non-cryptographic random generator \`${api}\` used. The output of this PRNG is predictable and must not be used for security-sensitive values (tokens, session IDs, keys, salts, password reset codes, OTPs).`,
|
|
27125
27651
|
file,
|
|
27126
27652
|
line,
|
|
27127
|
-
fix:
|
|
27128
|
-
evidence: {
|
|
27129
|
-
|
|
27130
|
-
|
|
27131
|
-
|
|
27132
|
-
|
|
27653
|
+
fix: this.fixFor(language),
|
|
27654
|
+
evidence: { api, language }
|
|
27655
|
+
});
|
|
27656
|
+
}
|
|
27657
|
+
return { findings };
|
|
27658
|
+
}
|
|
27659
|
+
fixFor(language) {
|
|
27660
|
+
switch (language) {
|
|
27661
|
+
case "java":
|
|
27662
|
+
return "Use `java.security.SecureRandom`. Example: `SecureRandom sr = new SecureRandom(); byte[] b = new byte[32]; sr.nextBytes(b);`";
|
|
27663
|
+
case "python":
|
|
27664
|
+
return "Use the `secrets` module (`secrets.token_bytes`, `secrets.token_hex`, `secrets.choice`, `secrets.randbelow`).";
|
|
27665
|
+
case "javascript":
|
|
27666
|
+
case "typescript":
|
|
27667
|
+
return "Use `crypto.randomBytes(n)` (Node.js) or `crypto.getRandomValues(typedArray)` (browser).";
|
|
27668
|
+
case "go":
|
|
27669
|
+
return "Use `crypto/rand` instead of `math/rand`. Example: `b := make([]byte, 32); _, _ = rand.Read(b)` (where `rand` is `crypto/rand`).";
|
|
27670
|
+
default:
|
|
27671
|
+
return "Use a cryptographically secure random generator from your standard library.";
|
|
27672
|
+
}
|
|
27673
|
+
}
|
|
27674
|
+
detect(call, language, ctx) {
|
|
27675
|
+
const method = call.method_name;
|
|
27676
|
+
const receiver = call.receiver ?? "";
|
|
27677
|
+
if (language === "java") {
|
|
27678
|
+
if (call.is_constructor) {
|
|
27679
|
+
const ctor = method;
|
|
27680
|
+
if (ctor === "Random") return "new Random";
|
|
27681
|
+
if (ctor === "SplittableRandom") return "new SplittableRandom";
|
|
27682
|
+
}
|
|
27683
|
+
if (method === "random" && (receiver === "Math" || receiver.endsWith(".Math"))) {
|
|
27684
|
+
return "Math.random";
|
|
27685
|
+
}
|
|
27686
|
+
if (JAVA_RANDOM_METHODS.has(method)) {
|
|
27687
|
+
const rt = call.receiver_type ?? "";
|
|
27688
|
+
if (rt === "Random" || rt === "SplittableRandom" || rt === "ThreadLocalRandom") {
|
|
27689
|
+
return `${rt}.${method}`;
|
|
27133
27690
|
}
|
|
27691
|
+
}
|
|
27692
|
+
if (JAVA_RANDOM_METHODS.has(method) && /ThreadLocalRandom\.current\(\)/.test(receiver)) {
|
|
27693
|
+
return `ThreadLocalRandom.current.${method}`;
|
|
27694
|
+
}
|
|
27695
|
+
return null;
|
|
27696
|
+
}
|
|
27697
|
+
if (language === "python") {
|
|
27698
|
+
if ((receiver === "random" || receiver.endsWith(".random")) && PY_RANDOM_FUNCS.has(method)) {
|
|
27699
|
+
return `random.${method}`;
|
|
27700
|
+
}
|
|
27701
|
+
return null;
|
|
27702
|
+
}
|
|
27703
|
+
if (language === "javascript" || language === "typescript") {
|
|
27704
|
+
if (method === "random" && (receiver === "Math" || receiver.endsWith(".Math"))) {
|
|
27705
|
+
return "Math.random";
|
|
27706
|
+
}
|
|
27707
|
+
return null;
|
|
27708
|
+
}
|
|
27709
|
+
if (language === "go") {
|
|
27710
|
+
if (receiver === "rand" && GO_RAND_FUNCS.has(method)) {
|
|
27711
|
+
if (this.goMathRandIsActive(ctx)) {
|
|
27712
|
+
return `rand.${method}`;
|
|
27713
|
+
}
|
|
27714
|
+
}
|
|
27715
|
+
return null;
|
|
27716
|
+
}
|
|
27717
|
+
return null;
|
|
27718
|
+
}
|
|
27719
|
+
/**
|
|
27720
|
+
* Returns true when `math/rand` is the active `rand` identifier in this
|
|
27721
|
+
* file (i.e. `math/rand` imported unaliased; `crypto/rand` is either not
|
|
27722
|
+
* imported or imported under an alias).
|
|
27723
|
+
*/
|
|
27724
|
+
goMathRandIsActive(ctx) {
|
|
27725
|
+
const imports = ctx.graph.ir.imports ?? [];
|
|
27726
|
+
let mathRandUnaliased = false;
|
|
27727
|
+
let cryptoRandUnaliased = false;
|
|
27728
|
+
for (const imp of imports) {
|
|
27729
|
+
const pkg = imp.from_package ?? imp.imported_name;
|
|
27730
|
+
const alias = imp.alias;
|
|
27731
|
+
if (pkg === "math/rand" && (!alias || alias === "rand")) {
|
|
27732
|
+
mathRandUnaliased = true;
|
|
27733
|
+
}
|
|
27734
|
+
if (pkg === "crypto/rand" && (!alias || alias === "rand")) {
|
|
27735
|
+
cryptoRandUnaliased = true;
|
|
27736
|
+
}
|
|
27737
|
+
}
|
|
27738
|
+
return mathRandUnaliased && !cryptoRandUnaliased;
|
|
27739
|
+
}
|
|
27740
|
+
};
|
|
27741
|
+
|
|
27742
|
+
// src/analysis/passes/tls-verify-disabled-pass.ts
|
|
27743
|
+
var PY_HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
27744
|
+
"get",
|
|
27745
|
+
"post",
|
|
27746
|
+
"put",
|
|
27747
|
+
"delete",
|
|
27748
|
+
"patch",
|
|
27749
|
+
"head",
|
|
27750
|
+
"options",
|
|
27751
|
+
"request",
|
|
27752
|
+
"send"
|
|
27753
|
+
]);
|
|
27754
|
+
var PY_HTTP_RECEIVERS = /* @__PURE__ */ new Set([
|
|
27755
|
+
"requests",
|
|
27756
|
+
"httpx"
|
|
27757
|
+
]);
|
|
27758
|
+
var VERIFY_FALSE_RE = /\bverify\s*=\s*False\b/;
|
|
27759
|
+
var REJECT_UNAUTHORIZED_FALSE_RE = /\brejectUnauthorized\s*:\s*false\b/;
|
|
27760
|
+
var INSECURE_SKIP_VERIFY_TRUE_RE = /\bInsecureSkipVerify\s*:\s*true\b/;
|
|
27761
|
+
var HOSTNAME_LAMBDA_TRUE_RE = /\(\s*\w+\s*,\s*\w+\s*\)\s*->\s*true\b/;
|
|
27762
|
+
var ALLOW_ALL_HOSTNAME_VERIFIERS = /* @__PURE__ */ new Set([
|
|
27763
|
+
"NoopHostnameVerifier.INSTANCE",
|
|
27764
|
+
"new AllowAllHostnameVerifier()",
|
|
27765
|
+
"new NoopHostnameVerifier()"
|
|
27766
|
+
]);
|
|
27767
|
+
var TlsVerifyDisabledPass = class {
|
|
27768
|
+
name = "tls-verify-disabled";
|
|
27769
|
+
category = "security";
|
|
27770
|
+
run(ctx) {
|
|
27771
|
+
const { graph, language, code } = ctx;
|
|
27772
|
+
const file = graph.ir.meta.file;
|
|
27773
|
+
const findings = [];
|
|
27774
|
+
for (const call of graph.ir.calls) {
|
|
27775
|
+
const det = this.detectCall(call, language);
|
|
27776
|
+
if (!det) continue;
|
|
27777
|
+
const line = call.location.line;
|
|
27778
|
+
findings.push({ line, language, ...det });
|
|
27779
|
+
ctx.addFinding({
|
|
27780
|
+
id: `${this.name}-${file}-${line}-${det.pattern}`,
|
|
27781
|
+
pass: this.name,
|
|
27782
|
+
category: this.category,
|
|
27783
|
+
rule_id: this.name,
|
|
27784
|
+
cwe: "CWE-295",
|
|
27785
|
+
severity: "high",
|
|
27786
|
+
level: "error",
|
|
27787
|
+
message: `TLS certificate verification disabled via \`${det.pattern}\` in \`${det.api}\`. The connection becomes vulnerable to active man-in-the-middle attacks \u2014 any attacker on the network path can present a forged certificate.`,
|
|
27788
|
+
file,
|
|
27789
|
+
line,
|
|
27790
|
+
fix: this.fixFor(language, det.pattern),
|
|
27791
|
+
evidence: { ...det, language }
|
|
27134
27792
|
});
|
|
27135
27793
|
}
|
|
27136
|
-
|
|
27794
|
+
for (const extra of this.detectSourceText(code, language)) {
|
|
27795
|
+
const dupKey = `${extra.line}-${extra.pattern}`;
|
|
27796
|
+
if (findings.some((f) => `${f.line}-${f.pattern}` === dupKey)) continue;
|
|
27797
|
+
findings.push({ ...extra, language });
|
|
27798
|
+
ctx.addFinding({
|
|
27799
|
+
id: `${this.name}-${file}-${extra.line}-${extra.pattern}`,
|
|
27800
|
+
pass: this.name,
|
|
27801
|
+
category: this.category,
|
|
27802
|
+
rule_id: this.name,
|
|
27803
|
+
cwe: "CWE-295",
|
|
27804
|
+
severity: "high",
|
|
27805
|
+
level: "error",
|
|
27806
|
+
message: `TLS certificate verification disabled via \`${extra.pattern}\` (${extra.api}). Vulnerable to active man-in-the-middle.`,
|
|
27807
|
+
file,
|
|
27808
|
+
line: extra.line,
|
|
27809
|
+
fix: this.fixFor(language, extra.pattern),
|
|
27810
|
+
evidence: { ...extra, language }
|
|
27811
|
+
});
|
|
27812
|
+
}
|
|
27813
|
+
return { findings };
|
|
27814
|
+
}
|
|
27815
|
+
detectCall(call, language) {
|
|
27816
|
+
const method = call.method_name;
|
|
27817
|
+
const receiver = call.receiver ?? "";
|
|
27818
|
+
if (language === "python") {
|
|
27819
|
+
if (PY_HTTP_RECEIVERS.has(receiver) && PY_HTTP_METHODS.has(method)) {
|
|
27820
|
+
for (const arg of call.arguments) {
|
|
27821
|
+
const expr = (arg.expression ?? "").trim();
|
|
27822
|
+
if (VERIFY_FALSE_RE.test(expr)) {
|
|
27823
|
+
return { pattern: "verify=False", api: `${receiver}.${method}` };
|
|
27824
|
+
}
|
|
27825
|
+
}
|
|
27826
|
+
}
|
|
27827
|
+
if (method === "_create_unverified_context" && receiver === "ssl") {
|
|
27828
|
+
return { pattern: "ssl._create_unverified_context", api: "ssl._create_unverified_context()" };
|
|
27829
|
+
}
|
|
27830
|
+
if (receiver === "httpx" && method === "Client") {
|
|
27831
|
+
for (const arg of call.arguments) {
|
|
27832
|
+
if (VERIFY_FALSE_RE.test(arg.expression ?? "")) {
|
|
27833
|
+
return { pattern: "verify=False", api: "httpx.Client" };
|
|
27834
|
+
}
|
|
27835
|
+
}
|
|
27836
|
+
}
|
|
27837
|
+
return null;
|
|
27838
|
+
}
|
|
27839
|
+
if (language === "javascript" || language === "typescript") {
|
|
27840
|
+
const lastSeg = method.includes(".") ? method.split(".").pop() ?? "" : method;
|
|
27841
|
+
const arglooks = method === "request" || method === "get" || method === "post" || method === "create" || method === "Agent" || method === "fetch" || lastSeg === "Agent" || lastSeg === "request" || lastSeg === "create";
|
|
27842
|
+
if (arglooks) {
|
|
27843
|
+
for (const arg of call.arguments) {
|
|
27844
|
+
if (REJECT_UNAUTHORIZED_FALSE_RE.test(arg.expression ?? "")) {
|
|
27845
|
+
return { pattern: "rejectUnauthorized: false", api: `${receiver || "(global)"}.${method}` };
|
|
27846
|
+
}
|
|
27847
|
+
}
|
|
27848
|
+
}
|
|
27849
|
+
return null;
|
|
27850
|
+
}
|
|
27851
|
+
if (language === "java") {
|
|
27852
|
+
if (method === "setHostnameVerifier") {
|
|
27853
|
+
const arg = call.arguments.find((a) => a.position === 0);
|
|
27854
|
+
const expr = (arg?.expression ?? "").trim();
|
|
27855
|
+
if (HOSTNAME_LAMBDA_TRUE_RE.test(expr)) {
|
|
27856
|
+
return { pattern: "(h,s) -> true", api: "setHostnameVerifier" };
|
|
27857
|
+
}
|
|
27858
|
+
for (const v of ALLOW_ALL_HOSTNAME_VERIFIERS) {
|
|
27859
|
+
if (expr === v || expr.replace(/\s+/g, "") === v.replace(/\s+/g, "")) {
|
|
27860
|
+
return { pattern: v, api: "setHostnameVerifier" };
|
|
27861
|
+
}
|
|
27862
|
+
}
|
|
27863
|
+
}
|
|
27864
|
+
return null;
|
|
27865
|
+
}
|
|
27866
|
+
return null;
|
|
27867
|
+
}
|
|
27868
|
+
detectSourceText(code, language) {
|
|
27869
|
+
const out2 = [];
|
|
27870
|
+
const lines = code.split("\n");
|
|
27871
|
+
if (language === "go") {
|
|
27872
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27873
|
+
if (INSECURE_SKIP_VERIFY_TRUE_RE.test(lines[i2])) {
|
|
27874
|
+
out2.push({
|
|
27875
|
+
line: i2 + 1,
|
|
27876
|
+
pattern: "InsecureSkipVerify: true",
|
|
27877
|
+
api: "tls.Config"
|
|
27878
|
+
});
|
|
27879
|
+
}
|
|
27880
|
+
}
|
|
27881
|
+
}
|
|
27882
|
+
if (language === "python") {
|
|
27883
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27884
|
+
const l = lines[i2];
|
|
27885
|
+
if (/ssl\._create_default_https_context\s*=\s*ssl\._create_unverified_context/.test(l)) {
|
|
27886
|
+
out2.push({
|
|
27887
|
+
line: i2 + 1,
|
|
27888
|
+
pattern: "ssl._create_default_https_context = _create_unverified_context",
|
|
27889
|
+
api: "ssl module override"
|
|
27890
|
+
});
|
|
27891
|
+
}
|
|
27892
|
+
}
|
|
27893
|
+
}
|
|
27894
|
+
if (language === "javascript" || language === "typescript") {
|
|
27895
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27896
|
+
const l = lines[i2];
|
|
27897
|
+
if (/process\.env\.NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/.test(l)) {
|
|
27898
|
+
out2.push({
|
|
27899
|
+
line: i2 + 1,
|
|
27900
|
+
pattern: "NODE_TLS_REJECT_UNAUTHORIZED=0",
|
|
27901
|
+
api: "process.env"
|
|
27902
|
+
});
|
|
27903
|
+
}
|
|
27904
|
+
}
|
|
27905
|
+
}
|
|
27906
|
+
return out2;
|
|
27907
|
+
}
|
|
27908
|
+
fixFor(language, pattern) {
|
|
27909
|
+
if (pattern.includes("InsecureSkipVerify")) {
|
|
27910
|
+
return "Remove `InsecureSkipVerify: true` \u2014 let Go verify the cert. If you need to trust a private CA, set `RootCAs` to a `*x509.CertPool` containing that CA.";
|
|
27911
|
+
}
|
|
27912
|
+
if (pattern.includes("verify=False")) {
|
|
27913
|
+
return "Remove `verify=False`. To trust a private CA, pass `verify='/path/to/ca.pem'`.";
|
|
27914
|
+
}
|
|
27915
|
+
if (pattern.includes("rejectUnauthorized")) {
|
|
27916
|
+
return "Remove `rejectUnauthorized: false`. To trust a private CA, set the `ca` option to the CA cert(s). Never disable TLS verification globally.";
|
|
27917
|
+
}
|
|
27918
|
+
if (pattern.includes("NODE_TLS_REJECT_UNAUTHORIZED")) {
|
|
27919
|
+
return "Remove the `NODE_TLS_REJECT_UNAUTHORIZED=0` assignment \u2014 it globally disables TLS verification for every outbound HTTPS request.";
|
|
27920
|
+
}
|
|
27921
|
+
if (language === "java") {
|
|
27922
|
+
return "Do not use an always-true HostnameVerifier or AllowAllHostnameVerifier. Use the JVM's default verifier; for self-signed certs add the cert to a custom TrustManager that validates the chain.";
|
|
27923
|
+
}
|
|
27924
|
+
if (pattern.includes("ssl._create_unverified_context")) {
|
|
27925
|
+
return "Do not use `_create_unverified_context()`. Use `ssl.create_default_context()`.";
|
|
27926
|
+
}
|
|
27927
|
+
return "Restore TLS certificate and hostname verification.";
|
|
27137
27928
|
}
|
|
27138
27929
|
};
|
|
27139
27930
|
|
|
@@ -28034,6 +28825,10 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
28034
28825
|
if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
|
|
28035
28826
|
if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
|
|
28036
28827
|
if (!disabledPasses.has("insecure-cookie")) pipeline.add(new InsecureCookiePass());
|
|
28828
|
+
if (!disabledPasses.has("weak-hash")) pipeline.add(new WeakHashPass());
|
|
28829
|
+
if (!disabledPasses.has("weak-crypto")) pipeline.add(new WeakCryptoPass());
|
|
28830
|
+
if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
|
|
28831
|
+
if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
|
|
28037
28832
|
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
28038
28833
|
const sinkFilter = results.get("sink-filter");
|
|
28039
28834
|
const interProc = results.get("interprocedural");
|