circle-ir 3.68.0 → 3.70.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/dist/analysis/passes/cache-no-vary-pass.d.ts +40 -0
- package/dist/analysis/passes/cache-no-vary-pass.d.ts.map +1 -0
- package/dist/analysis/passes/cache-no-vary-pass.js +347 -0
- package/dist/analysis/passes/cache-no-vary-pass.js.map +1 -0
- package/dist/analysis/passes/module-side-effect-pass.d.ts +68 -0
- package/dist/analysis/passes/module-side-effect-pass.d.ts.map +1 -0
- package/dist/analysis/passes/module-side-effect-pass.js +319 -0
- package/dist/analysis/passes/module-side-effect-pass.js.map +1 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +6 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +496 -0
- package/package.json +1 -1
|
@@ -29413,6 +29413,500 @@ var TlsVerifyDisabledPass = class {
|
|
|
29413
29413
|
}
|
|
29414
29414
|
};
|
|
29415
29415
|
|
|
29416
|
+
// src/analysis/passes/module-side-effect-pass.ts
|
|
29417
|
+
var JS_EXEC_METHODS = /* @__PURE__ */ new Set([
|
|
29418
|
+
"exec",
|
|
29419
|
+
"spawn",
|
|
29420
|
+
"execSync",
|
|
29421
|
+
"spawnSync",
|
|
29422
|
+
"execFile",
|
|
29423
|
+
"execFileSync"
|
|
29424
|
+
]);
|
|
29425
|
+
var JS_EXEC_RECEIVERS = /* @__PURE__ */ new Set([
|
|
29426
|
+
"child_process",
|
|
29427
|
+
"cp"
|
|
29428
|
+
]);
|
|
29429
|
+
var JS_NETWORK_RECEIVER_METHOD = /* @__PURE__ */ new Set([
|
|
29430
|
+
"https:request",
|
|
29431
|
+
"http:request",
|
|
29432
|
+
"https:get",
|
|
29433
|
+
"http:get"
|
|
29434
|
+
]);
|
|
29435
|
+
var JS_NETWORK_MAYBE = /* @__PURE__ */ new Set([
|
|
29436
|
+
"fetch"
|
|
29437
|
+
]);
|
|
29438
|
+
var JS_ENV_SIGNAL_RE = /\bprocess\.env\b|\bos\.homedir\b|\/etc\/(passwd|shadow)\b|\.ssh\/id_(rsa|dsa|ed25519)\b|\bhomedir\b/;
|
|
29439
|
+
var PKG_JSON_BENIGN_INSTALL = /* @__PURE__ */ new Set([
|
|
29440
|
+
"node-gyp rebuild",
|
|
29441
|
+
"prebuild-install",
|
|
29442
|
+
"prebuild-install || node-gyp rebuild",
|
|
29443
|
+
"husky install",
|
|
29444
|
+
"patch-package",
|
|
29445
|
+
"npm run build"
|
|
29446
|
+
]);
|
|
29447
|
+
var PKG_JSON_INSTALL_SHELL_RE = /\b(curl|wget|nc|ncat|node\s+-e|node\s+-r|sh\s+-c|bash\s+-c|eval|base64\s+-d)\b/;
|
|
29448
|
+
var PY_NETWORK_RECEIVER_METHODS = [
|
|
29449
|
+
{ receiver: "requests", method: "post" },
|
|
29450
|
+
{ receiver: "requests", method: "put" },
|
|
29451
|
+
{ receiver: "urllib.request", method: "urlopen" },
|
|
29452
|
+
{ receiver: "socket", method: "create_connection" },
|
|
29453
|
+
{ receiver: "socket", method: "connect" },
|
|
29454
|
+
{ receiver: "subprocess", method: "run" },
|
|
29455
|
+
{ receiver: "subprocess", method: "Popen" },
|
|
29456
|
+
{ receiver: "os", method: "system" }
|
|
29457
|
+
];
|
|
29458
|
+
var PY_ENV_SIGNAL_RE = /\bos\.environ\b|\bpwd\.getpw\b|\bid_(rsa|dsa|ed25519)\b|\bhome\b|\b\/etc\/(passwd|shadow)\b|\bPath\.home\b|\bglob\.glob\b/;
|
|
29459
|
+
var GO_INIT_DANGEROUS = [
|
|
29460
|
+
{ receiver: "exec", method: "Command" },
|
|
29461
|
+
{ receiver: "http", method: "Post" },
|
|
29462
|
+
{ receiver: "http", method: "Get" },
|
|
29463
|
+
{ receiver: "net", method: "LookupTXT" },
|
|
29464
|
+
{ receiver: "os", method: "Setenv" }
|
|
29465
|
+
];
|
|
29466
|
+
var RUST_DANGEROUS_METHODS = /* @__PURE__ */ new Set([
|
|
29467
|
+
"Command::new",
|
|
29468
|
+
"process::Command::new",
|
|
29469
|
+
"std::process::Command::new",
|
|
29470
|
+
"new"
|
|
29471
|
+
// Command::new appears as method='new', receiver='Command' / etc.
|
|
29472
|
+
]);
|
|
29473
|
+
var RUST_DANGEROUS_RECEIVERS = /* @__PURE__ */ new Set([
|
|
29474
|
+
"Command",
|
|
29475
|
+
"process::Command",
|
|
29476
|
+
"std::process::Command",
|
|
29477
|
+
"reqwest",
|
|
29478
|
+
"reqwest::blocking"
|
|
29479
|
+
]);
|
|
29480
|
+
var ModuleSideEffectPass = class {
|
|
29481
|
+
name = "module-side-effect";
|
|
29482
|
+
category = "security";
|
|
29483
|
+
run(ctx) {
|
|
29484
|
+
const { graph, language, code } = ctx;
|
|
29485
|
+
const file = graph.ir.meta.file;
|
|
29486
|
+
const findings = [];
|
|
29487
|
+
const emit = (line, pattern, api) => {
|
|
29488
|
+
if (findings.some((f) => f.line === line && f.pattern === pattern)) return;
|
|
29489
|
+
findings.push({ line, language, pattern, api });
|
|
29490
|
+
ctx.addFinding({
|
|
29491
|
+
id: `${this.name}-${file}-${line}-${pattern.replace(/\W+/g, "-")}`,
|
|
29492
|
+
pass: this.name,
|
|
29493
|
+
category: this.category,
|
|
29494
|
+
rule_id: this.name,
|
|
29495
|
+
cwe: "CWE-829",
|
|
29496
|
+
severity: "high",
|
|
29497
|
+
level: "error",
|
|
29498
|
+
message: `Module-level / install-time side effect (${pattern}) in \`${api}\`. Code that runs at import / build / install time is invisible to runtime defenses and is the standard delivery vector for supply-chain droppers (shai-hulud-style harvesters, malicious typosquats, build.rs exfil). If this side effect is intentional, move it into an explicit function invoked at runtime; if it is install-time configuration, restrict it to documented APIs (e.g. \`cargo:\` directives, \`node-gyp rebuild\`).`,
|
|
29499
|
+
file,
|
|
29500
|
+
line,
|
|
29501
|
+
fix: this.fixFor(language, pattern),
|
|
29502
|
+
evidence: { language, api, pattern }
|
|
29503
|
+
});
|
|
29504
|
+
};
|
|
29505
|
+
const isRustBuildScript = language === "rust" && /\bbuild\.rs$/.test(file);
|
|
29506
|
+
for (const call of graph.ir.calls) {
|
|
29507
|
+
if (language === "rust" && !isRustBuildScript) continue;
|
|
29508
|
+
const det = this.detectCall(call, language);
|
|
29509
|
+
if (!det) continue;
|
|
29510
|
+
emit(call.location.line, det.pattern, det.api);
|
|
29511
|
+
}
|
|
29512
|
+
if (language === "javascript" || language === "typescript") {
|
|
29513
|
+
if (/\bpackage\.json$/.test(file)) {
|
|
29514
|
+
for (const extra of this.scanPackageJson(code)) {
|
|
29515
|
+
emit(extra.line, extra.pattern, extra.api);
|
|
29516
|
+
}
|
|
29517
|
+
}
|
|
29518
|
+
}
|
|
29519
|
+
return { findings };
|
|
29520
|
+
}
|
|
29521
|
+
detectCall(call, language) {
|
|
29522
|
+
const method = call.method_name;
|
|
29523
|
+
const receiver = call.receiver ?? "";
|
|
29524
|
+
if (language === "javascript" || language === "typescript") {
|
|
29525
|
+
if (call.in_method != null) return null;
|
|
29526
|
+
if (JS_EXEC_RECEIVERS.has(receiver) && JS_EXEC_METHODS.has(method)) {
|
|
29527
|
+
return {
|
|
29528
|
+
pattern: "module-level child_process call",
|
|
29529
|
+
api: `${receiver}.${method}`
|
|
29530
|
+
};
|
|
29531
|
+
}
|
|
29532
|
+
const recvMethod = `${receiver}:${method}`;
|
|
29533
|
+
if (JS_NETWORK_RECEIVER_METHOD.has(recvMethod)) {
|
|
29534
|
+
return {
|
|
29535
|
+
pattern: "module-level network request",
|
|
29536
|
+
api: `${receiver}.${method}`
|
|
29537
|
+
};
|
|
29538
|
+
}
|
|
29539
|
+
if (JS_NETWORK_MAYBE.has(method) && receiver === "") {
|
|
29540
|
+
for (const arg of call.arguments) {
|
|
29541
|
+
if (JS_ENV_SIGNAL_RE.test(arg.expression ?? "")) {
|
|
29542
|
+
return {
|
|
29543
|
+
pattern: "module-level fetch of process.env",
|
|
29544
|
+
api: method
|
|
29545
|
+
};
|
|
29546
|
+
}
|
|
29547
|
+
}
|
|
29548
|
+
}
|
|
29549
|
+
return null;
|
|
29550
|
+
}
|
|
29551
|
+
if (language === "python") {
|
|
29552
|
+
if (call.in_method != null) return null;
|
|
29553
|
+
for (const tuple of PY_NETWORK_RECEIVER_METHODS) {
|
|
29554
|
+
if (receiver === tuple.receiver && method === tuple.method) {
|
|
29555
|
+
for (const arg of call.arguments) {
|
|
29556
|
+
if (PY_ENV_SIGNAL_RE.test(arg.expression ?? "")) {
|
|
29557
|
+
return {
|
|
29558
|
+
pattern: "import-time network call with env signal",
|
|
29559
|
+
api: `${receiver}.${method}`
|
|
29560
|
+
};
|
|
29561
|
+
}
|
|
29562
|
+
}
|
|
29563
|
+
}
|
|
29564
|
+
}
|
|
29565
|
+
return null;
|
|
29566
|
+
}
|
|
29567
|
+
if (language === "go") {
|
|
29568
|
+
if (call.in_method !== "init") return null;
|
|
29569
|
+
for (const tuple of GO_INIT_DANGEROUS) {
|
|
29570
|
+
if (receiver === tuple.receiver && method === tuple.method) {
|
|
29571
|
+
return {
|
|
29572
|
+
pattern: "init() install-time side effect",
|
|
29573
|
+
api: `${receiver}.${method}`
|
|
29574
|
+
};
|
|
29575
|
+
}
|
|
29576
|
+
}
|
|
29577
|
+
return null;
|
|
29578
|
+
}
|
|
29579
|
+
if (language === "rust") {
|
|
29580
|
+
const recv = receiver.trim();
|
|
29581
|
+
if (RUST_DANGEROUS_RECEIVERS.has(recv) || recv.startsWith("Command::")) {
|
|
29582
|
+
if (method === "new" || RUST_DANGEROUS_METHODS.has(method) || method === "get" || method === "post") {
|
|
29583
|
+
return {
|
|
29584
|
+
pattern: "build.rs side effect",
|
|
29585
|
+
api: `${recv || method}.${method}`
|
|
29586
|
+
};
|
|
29587
|
+
}
|
|
29588
|
+
}
|
|
29589
|
+
return null;
|
|
29590
|
+
}
|
|
29591
|
+
return null;
|
|
29592
|
+
}
|
|
29593
|
+
/**
|
|
29594
|
+
* Scan a package.json file for dangerous install-lifecycle scripts.
|
|
29595
|
+
* Best-effort regex extraction — package.json is not a JS source per se but
|
|
29596
|
+
* the analyzer routes it through the JS pipeline.
|
|
29597
|
+
*/
|
|
29598
|
+
scanPackageJson(code) {
|
|
29599
|
+
const out2 = [];
|
|
29600
|
+
const lines = code.split("\n");
|
|
29601
|
+
const installRe = /"(pre|post)?install"\s*:\s*"([^"]+)"/i;
|
|
29602
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
29603
|
+
const m = lines[i2].match(installRe);
|
|
29604
|
+
if (!m) continue;
|
|
29605
|
+
const value = m[2].trim();
|
|
29606
|
+
if (PKG_JSON_BENIGN_INSTALL.has(value)) continue;
|
|
29607
|
+
if (!PKG_JSON_INSTALL_SHELL_RE.test(value)) continue;
|
|
29608
|
+
out2.push({
|
|
29609
|
+
line: i2 + 1,
|
|
29610
|
+
pattern: "npm lifecycle hook executes shell",
|
|
29611
|
+
api: `scripts.${m[1] ?? ""}install`
|
|
29612
|
+
});
|
|
29613
|
+
}
|
|
29614
|
+
return out2;
|
|
29615
|
+
}
|
|
29616
|
+
fixFor(language, pattern) {
|
|
29617
|
+
if (pattern.includes("child_process")) {
|
|
29618
|
+
return "Remove the module-level child_process call. If an install-time step is genuinely required, move it into an explicit function and document why it must run at install time.";
|
|
29619
|
+
}
|
|
29620
|
+
if (pattern.includes("module-level network")) {
|
|
29621
|
+
return "Network requests should not run at module load. Move the call inside an exported function called explicitly by the caller.";
|
|
29622
|
+
}
|
|
29623
|
+
if (pattern.includes("module-level fetch of process.env")) {
|
|
29624
|
+
return "Exfiltrating `process.env` at module load is the canonical supply-chain dropper shape. Remove this code or, if intentional, gate it behind an explicit opt-in.";
|
|
29625
|
+
}
|
|
29626
|
+
if (pattern.includes("npm lifecycle hook")) {
|
|
29627
|
+
return "Replace the install-script shell payload with a build tool (e.g. `node-gyp rebuild`, `prebuild-install`). Lifecycle scripts that invoke curl/wget/node -e/sh -c are how supply-chain droppers are delivered.";
|
|
29628
|
+
}
|
|
29629
|
+
if (pattern.includes("import-time network call")) {
|
|
29630
|
+
return "Move the network call inside an explicit function. Sending `os.environ` or filesystem secrets at module import is the canonical credential-harvester shape.";
|
|
29631
|
+
}
|
|
29632
|
+
if (pattern.includes("init()")) {
|
|
29633
|
+
return "Move the side effect out of `init()`. Go `init` functions run automatically on package import; network/exec calls there are invisible to the caller and are how supply-chain droppers operate.";
|
|
29634
|
+
}
|
|
29635
|
+
if (pattern.includes("build.rs")) {
|
|
29636
|
+
return "`build.rs` should only emit `cargo:` directives. Spawning subprocesses or making network requests at build time is a documented supply-chain attack vector (see RUSTSEC).";
|
|
29637
|
+
}
|
|
29638
|
+
void language;
|
|
29639
|
+
return "Remove the module-level side effect or move it inside an explicit, runtime-invoked function.";
|
|
29640
|
+
}
|
|
29641
|
+
};
|
|
29642
|
+
|
|
29643
|
+
// src/analysis/passes/cache-no-vary-pass.ts
|
|
29644
|
+
function isSharedCacheable(value) {
|
|
29645
|
+
const v = value.toLowerCase();
|
|
29646
|
+
if (/\b(private|no-store|no-cache)\b/.test(v)) return false;
|
|
29647
|
+
const pub = /\bpublic\b/.test(v);
|
|
29648
|
+
const maxMatch = /\b(?:s-maxage|max-age)\s*=\s*(\d+)/.exec(v);
|
|
29649
|
+
const positiveMax = maxMatch ? Number(maxMatch[1]) > 0 : false;
|
|
29650
|
+
return pub || positiveMax;
|
|
29651
|
+
}
|
|
29652
|
+
function isVaryCovering(value) {
|
|
29653
|
+
const v = value.toLowerCase();
|
|
29654
|
+
return /\b(cookie|authorization|\*)\b/.test(v);
|
|
29655
|
+
}
|
|
29656
|
+
var JS_AUTH_SIGNAL_RE = /\b(?:req|request)\s*\.\s*(?:cookies|session|user(?:Id|Name)?)\b|\b(?:req|request)\s*\.\s*headers\s*\.\s*(?:cookie|authorization)\b|\bres(?:ponse)?\s*\.\s*cookie\s*\(/i;
|
|
29657
|
+
var PY_AUTH_SIGNAL_RE = /\brequest\s*\.\s*cookies\b|\brequest\s*\.\s*headers\s*\.\s*get\s*\(\s*['"]Authorization['"]|\brequest\s*\.\s*authorization\b|\bsession\s*\[|\b(?:g\.user|current_user)\b|\bset_cookie\s*\(/i;
|
|
29658
|
+
var GO_AUTH_SIGNAL_RE = /\br\s*\.\s*Cookie\s*\(|\br\s*\.\s*Header\s*(?:\(\)|\.)\s*\.?\s*Get\s*\(\s*"(?:Cookie|Authorization)"|\br\s*\.\s*BasicAuth\s*\(|\bhttp\s*\.\s*SetCookie\s*\(|\bc\s*\.\s*(?:GetHeader|Cookie|SetCookie)\s*\(/;
|
|
29659
|
+
var JAVA_AUTH_SIGNAL_RE = /@CookieValue\b|@RequestHeader\s*\(\s*"(?:Cookie|Authorization)"|\brequest\s*\.\s*getCookies\s*\(|\brequest\s*\.\s*getHeader\s*\(\s*"(?:Cookie|Authorization)"|\bresponse\s*\.\s*addCookie\s*\(|\bSecurityContextHolder\b|\bPrincipal\s+\w+|\bAuthentication\s+\w+/;
|
|
29660
|
+
var PY_CACHE_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Cache-Control['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
|
|
29661
|
+
var PY_VARY_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Vary['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
|
|
29662
|
+
var PY_VARY_DECORATOR_RE = /^\s*@\s*(?:vary_on_cookie|vary_on_headers)\b/;
|
|
29663
|
+
var PY_CACHE_CONTROL_DECORATOR_RE = /^\s*@\s*cache_control\s*\(([^)]*)\)/;
|
|
29664
|
+
var JS_HEADER_METHODS = /* @__PURE__ */ new Set(["setHeader", "set", "header"]);
|
|
29665
|
+
var GO_HEADER_METHODS = /* @__PURE__ */ new Set(["Set", "Add"]);
|
|
29666
|
+
var JAVA_HEADER_METHODS = /* @__PURE__ */ new Set(["setHeader", "addHeader"]);
|
|
29667
|
+
var JS_RES_RECEIVERS = /* @__PURE__ */ new Set(["res", "response", "ctx"]);
|
|
29668
|
+
function classifyCall(call, language) {
|
|
29669
|
+
const method = call.method_name;
|
|
29670
|
+
const receiver = (call.receiver ?? "").trim();
|
|
29671
|
+
const arg0 = call.arguments[0]?.literal ?? null;
|
|
29672
|
+
const arg1 = call.arguments[1]?.literal ?? null;
|
|
29673
|
+
if (language === "javascript" || language === "typescript") {
|
|
29674
|
+
if (JS_RES_RECEIVERS.has(receiver) && JS_HEADER_METHODS.has(method)) {
|
|
29675
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29676
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29677
|
+
return { kind: "cache-public", value: arg1 };
|
|
29678
|
+
}
|
|
29679
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29680
|
+
return { kind: "vary" };
|
|
29681
|
+
}
|
|
29682
|
+
}
|
|
29683
|
+
if (JS_RES_RECEIVERS.has(receiver) && method === "vary") {
|
|
29684
|
+
const v = arg0 ?? "";
|
|
29685
|
+
if (isVaryCovering(v) || v === "") return { kind: "vary" };
|
|
29686
|
+
}
|
|
29687
|
+
if (JS_RES_RECEIVERS.has(receiver) && method === "cookie") {
|
|
29688
|
+
return { kind: "auth" };
|
|
29689
|
+
}
|
|
29690
|
+
return null;
|
|
29691
|
+
}
|
|
29692
|
+
if (language === "python") {
|
|
29693
|
+
if (receiver === "request.cookies" || receiver === "request.session") {
|
|
29694
|
+
return { kind: "auth" };
|
|
29695
|
+
}
|
|
29696
|
+
if (receiver === "request.headers" && method === "get") {
|
|
29697
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29698
|
+
if (v === "authorization" || v === "cookie") return { kind: "auth" };
|
|
29699
|
+
}
|
|
29700
|
+
if ((receiver === "response" || receiver === "resp") && method === "set_cookie") {
|
|
29701
|
+
return { kind: "auth" };
|
|
29702
|
+
}
|
|
29703
|
+
if (method === "patch_vary_headers") return { kind: "vary" };
|
|
29704
|
+
if (method === "patch_cache_control") {
|
|
29705
|
+
const argTxt = call.arguments.map((a) => a.expression ?? "").join(",");
|
|
29706
|
+
if (/\bpublic\s*=\s*True\b/.test(argTxt)) {
|
|
29707
|
+
return { kind: "cache-public", value: argTxt };
|
|
29708
|
+
}
|
|
29709
|
+
}
|
|
29710
|
+
return null;
|
|
29711
|
+
}
|
|
29712
|
+
if (language === "go") {
|
|
29713
|
+
if ((receiver === "w.Header()" || receiver === "rw.Header()") && GO_HEADER_METHODS.has(method)) {
|
|
29714
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29715
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29716
|
+
return { kind: "cache-public", value: arg1 };
|
|
29717
|
+
}
|
|
29718
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29719
|
+
return { kind: "vary" };
|
|
29720
|
+
}
|
|
29721
|
+
}
|
|
29722
|
+
if (receiver === "c" && method === "Header") {
|
|
29723
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29724
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29725
|
+
return { kind: "cache-public", value: arg1 };
|
|
29726
|
+
}
|
|
29727
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29728
|
+
return { kind: "vary" };
|
|
29729
|
+
}
|
|
29730
|
+
}
|
|
29731
|
+
if (receiver === "r" && (method === "Cookie" || method === "BasicAuth")) {
|
|
29732
|
+
return { kind: "auth" };
|
|
29733
|
+
}
|
|
29734
|
+
if ((receiver === "r.Header" || receiver === "r.Header()") && method === "Get") {
|
|
29735
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29736
|
+
if (v === "cookie" || v === "authorization") return { kind: "auth" };
|
|
29737
|
+
}
|
|
29738
|
+
if (receiver === "http" && method === "SetCookie") return { kind: "auth" };
|
|
29739
|
+
if (receiver === "c" && (method === "Cookie" || method === "GetHeader" || method === "SetCookie")) {
|
|
29740
|
+
return { kind: "auth" };
|
|
29741
|
+
}
|
|
29742
|
+
return null;
|
|
29743
|
+
}
|
|
29744
|
+
if (language === "java") {
|
|
29745
|
+
if ((receiver === "response" || receiver === "resp") && JAVA_HEADER_METHODS.has(method)) {
|
|
29746
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29747
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29748
|
+
return { kind: "cache-public", value: arg1 };
|
|
29749
|
+
}
|
|
29750
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29751
|
+
return { kind: "vary" };
|
|
29752
|
+
}
|
|
29753
|
+
}
|
|
29754
|
+
if ((receiver === "headers" || receiver === "httpHeaders") && method === "setCacheControl") {
|
|
29755
|
+
return { kind: "cache-public", value: "HttpHeaders.setCacheControl(...)" };
|
|
29756
|
+
}
|
|
29757
|
+
if ((receiver === "headers" || receiver === "httpHeaders") && method === "setVary") {
|
|
29758
|
+
return { kind: "vary" };
|
|
29759
|
+
}
|
|
29760
|
+
if (receiver === "request" && method === "getCookies") {
|
|
29761
|
+
return { kind: "auth" };
|
|
29762
|
+
}
|
|
29763
|
+
if (receiver === "request" && method === "getHeader") {
|
|
29764
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29765
|
+
if (v === "cookie" || v === "authorization") return { kind: "auth" };
|
|
29766
|
+
}
|
|
29767
|
+
if ((receiver === "response" || receiver === "resp") && method === "addCookie") {
|
|
29768
|
+
return { kind: "auth" };
|
|
29769
|
+
}
|
|
29770
|
+
return null;
|
|
29771
|
+
}
|
|
29772
|
+
return null;
|
|
29773
|
+
}
|
|
29774
|
+
function authSignalRegex(language) {
|
|
29775
|
+
switch (language) {
|
|
29776
|
+
case "javascript":
|
|
29777
|
+
case "typescript":
|
|
29778
|
+
return JS_AUTH_SIGNAL_RE;
|
|
29779
|
+
case "python":
|
|
29780
|
+
return PY_AUTH_SIGNAL_RE;
|
|
29781
|
+
case "go":
|
|
29782
|
+
return GO_AUTH_SIGNAL_RE;
|
|
29783
|
+
case "java":
|
|
29784
|
+
return JAVA_AUTH_SIGNAL_RE;
|
|
29785
|
+
default:
|
|
29786
|
+
return null;
|
|
29787
|
+
}
|
|
29788
|
+
}
|
|
29789
|
+
function scanWindow(code, language, startLine, endLine) {
|
|
29790
|
+
const lines = code.split("\n");
|
|
29791
|
+
const lo = Math.max(0, startLine - 1);
|
|
29792
|
+
const hi = Math.min(lines.length, endLine);
|
|
29793
|
+
const out2 = { vary: false, auth: false };
|
|
29794
|
+
const authRe = authSignalRegex(language);
|
|
29795
|
+
for (let i2 = lo; i2 < hi; i2++) {
|
|
29796
|
+
const ln = lines[i2];
|
|
29797
|
+
if (authRe && authRe.test(ln)) out2.auth = true;
|
|
29798
|
+
if (language === "python") {
|
|
29799
|
+
if (!out2.cachePublic) {
|
|
29800
|
+
const mc = PY_CACHE_HEADER_ASSIGN_RE.exec(ln);
|
|
29801
|
+
if (mc && isSharedCacheable(mc[2])) {
|
|
29802
|
+
out2.cachePublic = { line: i2 + 1, value: mc[2] };
|
|
29803
|
+
}
|
|
29804
|
+
}
|
|
29805
|
+
if (!out2.cachePublic) {
|
|
29806
|
+
const md = PY_CACHE_CONTROL_DECORATOR_RE.exec(ln);
|
|
29807
|
+
if (md) {
|
|
29808
|
+
const argTxt = md[1];
|
|
29809
|
+
if (/\bpublic\s*=\s*True\b/.test(argTxt) && (/\bmax_age\s*=\s*[1-9]\d*\b/.test(argTxt) || !/max_age/.test(argTxt))) {
|
|
29810
|
+
out2.cachePublic = { line: i2 + 1, value: argTxt };
|
|
29811
|
+
}
|
|
29812
|
+
}
|
|
29813
|
+
}
|
|
29814
|
+
if (!out2.vary) {
|
|
29815
|
+
const mv = PY_VARY_HEADER_ASSIGN_RE.exec(ln);
|
|
29816
|
+
if (mv && isVaryCovering(mv[2])) out2.vary = true;
|
|
29817
|
+
if (PY_VARY_DECORATOR_RE.test(ln)) out2.vary = true;
|
|
29818
|
+
}
|
|
29819
|
+
}
|
|
29820
|
+
}
|
|
29821
|
+
return out2;
|
|
29822
|
+
}
|
|
29823
|
+
var CacheNoVaryPass = class {
|
|
29824
|
+
name = "cache-no-vary";
|
|
29825
|
+
category = "security";
|
|
29826
|
+
run(ctx) {
|
|
29827
|
+
const { graph, language, code } = ctx;
|
|
29828
|
+
const file = graph.ir.meta.file;
|
|
29829
|
+
const findings = [];
|
|
29830
|
+
const isSupported = language === "javascript" || language === "typescript" || language === "python" || language === "go" || language === "java";
|
|
29831
|
+
if (!isSupported) return { findings };
|
|
29832
|
+
if (/(?:\.test|\.spec)\.[jt]sx?$/i.test(file) || /__tests__\/|\/tests?\//i.test(file)) {
|
|
29833
|
+
return { findings };
|
|
29834
|
+
}
|
|
29835
|
+
const callsByHandler = /* @__PURE__ */ new Map();
|
|
29836
|
+
for (const call of graph.ir.calls) {
|
|
29837
|
+
const key = call.in_method ?? "<top>";
|
|
29838
|
+
let arr = callsByHandler.get(key);
|
|
29839
|
+
if (!arr) {
|
|
29840
|
+
arr = [];
|
|
29841
|
+
callsByHandler.set(key, arr);
|
|
29842
|
+
}
|
|
29843
|
+
arr.push(call);
|
|
29844
|
+
}
|
|
29845
|
+
const emit = (line, handler, cacheValue) => {
|
|
29846
|
+
if (findings.some((f) => f.line === line && f.handler === handler)) return;
|
|
29847
|
+
findings.push({ line, language, handler, cacheValue });
|
|
29848
|
+
ctx.addFinding({
|
|
29849
|
+
id: `${this.name}-${file}-${line}`,
|
|
29850
|
+
pass: this.name,
|
|
29851
|
+
category: this.category,
|
|
29852
|
+
rule_id: this.name,
|
|
29853
|
+
cwe: "CWE-524",
|
|
29854
|
+
severity: "medium",
|
|
29855
|
+
level: "warning",
|
|
29856
|
+
message: `Response sets a shared-cacheable Cache-Control ('${cacheValue}') in a handler that reads authenticated or user-scoped state, but does not set 'Vary: Cookie' or 'Vary: Authorization'. A shared cache (CDN, reverse proxy, ISP cache) keys the response by URL only and may serve one user's body to another. (CWE-524)`,
|
|
29857
|
+
file,
|
|
29858
|
+
line,
|
|
29859
|
+
fix: `Either add 'Vary: Cookie' (or 'Vary: Authorization') so caches key on the user identity, or change the directive to 'private' / 'no-store' so the response is never shared-cached.`,
|
|
29860
|
+
evidence: {
|
|
29861
|
+
language,
|
|
29862
|
+
handler: handler ?? "<top>",
|
|
29863
|
+
cacheValue
|
|
29864
|
+
}
|
|
29865
|
+
});
|
|
29866
|
+
};
|
|
29867
|
+
for (const [handlerKey, calls] of callsByHandler) {
|
|
29868
|
+
const handler = handlerKey === "<top>" ? null : handlerKey;
|
|
29869
|
+
const cachePublicHits = [];
|
|
29870
|
+
let varyFromCalls = false;
|
|
29871
|
+
let authFromCalls = false;
|
|
29872
|
+
for (const call of calls) {
|
|
29873
|
+
const cls = classifyCall(call, language);
|
|
29874
|
+
if (!cls) continue;
|
|
29875
|
+
if (cls.kind === "cache-public") {
|
|
29876
|
+
cachePublicHits.push({
|
|
29877
|
+
line: call.location.line,
|
|
29878
|
+
value: cls.value ?? ""
|
|
29879
|
+
});
|
|
29880
|
+
} else if (cls.kind === "vary") {
|
|
29881
|
+
varyFromCalls = true;
|
|
29882
|
+
} else if (cls.kind === "auth") {
|
|
29883
|
+
authFromCalls = true;
|
|
29884
|
+
}
|
|
29885
|
+
}
|
|
29886
|
+
let minLine = Infinity;
|
|
29887
|
+
let maxLine = -Infinity;
|
|
29888
|
+
for (const c of calls) {
|
|
29889
|
+
if (c.location?.line) {
|
|
29890
|
+
minLine = Math.min(minLine, c.location.line);
|
|
29891
|
+
maxLine = Math.max(maxLine, c.location.line);
|
|
29892
|
+
}
|
|
29893
|
+
}
|
|
29894
|
+
if (minLine === Infinity) continue;
|
|
29895
|
+
const winStart = Math.max(1, minLine - 5);
|
|
29896
|
+
const winEnd = maxLine + 5;
|
|
29897
|
+
const winScan = scanWindow(code, language, winStart, winEnd);
|
|
29898
|
+
if (winScan.cachePublic) cachePublicHits.push(winScan.cachePublic);
|
|
29899
|
+
const vary = varyFromCalls || winScan.vary;
|
|
29900
|
+
const auth = authFromCalls || winScan.auth;
|
|
29901
|
+
if (cachePublicHits.length === 0) continue;
|
|
29902
|
+
if (vary) continue;
|
|
29903
|
+
if (!auth) continue;
|
|
29904
|
+
emit(cachePublicHits[0].line, handler, cachePublicHits[0].value);
|
|
29905
|
+
}
|
|
29906
|
+
return { findings };
|
|
29907
|
+
}
|
|
29908
|
+
};
|
|
29909
|
+
|
|
29416
29910
|
// src/analysis/passes/jwt-verify-disabled-pass.ts
|
|
29417
29911
|
var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
|
|
29418
29912
|
var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
|
|
@@ -30769,6 +31263,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
30769
31263
|
if (!disabledPasses.has("weak-crypto")) pipeline.add(new WeakCryptoPass());
|
|
30770
31264
|
if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
|
|
30771
31265
|
if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
|
|
31266
|
+
if (!disabledPasses.has("module-side-effect")) pipeline.add(new ModuleSideEffectPass());
|
|
31267
|
+
if (!disabledPasses.has("cache-no-vary")) pipeline.add(new CacheNoVaryPass());
|
|
30772
31268
|
if (!disabledPasses.has("jwt-verify-disabled")) pipeline.add(new JwtVerifyDisabledPass());
|
|
30773
31269
|
if (!disabledPasses.has("csrf-protection-disabled")) pipeline.add(new CsrfProtectionDisabledPass());
|
|
30774
31270
|
if (!disabledPasses.has("xml-entity-expansion")) pipeline.add(new XmlEntityExpansionPass());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.70.0",
|
|
4
4
|
"description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|