cognium-dev 3.69.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/cli.js +290 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -29814,6 +29814,293 @@ class ModuleSideEffectPass {
|
|
|
29814
29814
|
}
|
|
29815
29815
|
}
|
|
29816
29816
|
|
|
29817
|
+
// ../circle-ir/dist/analysis/passes/cache-no-vary-pass.js
|
|
29818
|
+
function isSharedCacheable(value) {
|
|
29819
|
+
const v = value.toLowerCase();
|
|
29820
|
+
if (/\b(private|no-store|no-cache)\b/.test(v))
|
|
29821
|
+
return false;
|
|
29822
|
+
const pub = /\bpublic\b/.test(v);
|
|
29823
|
+
const maxMatch = /\b(?:s-maxage|max-age)\s*=\s*(\d+)/.exec(v);
|
|
29824
|
+
const positiveMax = maxMatch ? Number(maxMatch[1]) > 0 : false;
|
|
29825
|
+
return pub || positiveMax;
|
|
29826
|
+
}
|
|
29827
|
+
function isVaryCovering(value) {
|
|
29828
|
+
const v = value.toLowerCase();
|
|
29829
|
+
return /\b(cookie|authorization|\*)\b/.test(v);
|
|
29830
|
+
}
|
|
29831
|
+
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;
|
|
29832
|
+
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;
|
|
29833
|
+
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*\(/;
|
|
29834
|
+
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+/;
|
|
29835
|
+
var PY_CACHE_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Cache-Control['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
|
|
29836
|
+
var PY_VARY_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Vary['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
|
|
29837
|
+
var PY_VARY_DECORATOR_RE = /^\s*@\s*(?:vary_on_cookie|vary_on_headers)\b/;
|
|
29838
|
+
var PY_CACHE_CONTROL_DECORATOR_RE = /^\s*@\s*cache_control\s*\(([^)]*)\)/;
|
|
29839
|
+
var JS_HEADER_METHODS = new Set(["setHeader", "set", "header"]);
|
|
29840
|
+
var GO_HEADER_METHODS = new Set(["Set", "Add"]);
|
|
29841
|
+
var JAVA_HEADER_METHODS = new Set(["setHeader", "addHeader"]);
|
|
29842
|
+
var JS_RES_RECEIVERS = new Set(["res", "response", "ctx"]);
|
|
29843
|
+
function classifyCall(call, language) {
|
|
29844
|
+
const method = call.method_name;
|
|
29845
|
+
const receiver = (call.receiver ?? "").trim();
|
|
29846
|
+
const arg0 = call.arguments[0]?.literal ?? null;
|
|
29847
|
+
const arg1 = call.arguments[1]?.literal ?? null;
|
|
29848
|
+
if (language === "javascript" || language === "typescript") {
|
|
29849
|
+
if (JS_RES_RECEIVERS.has(receiver) && JS_HEADER_METHODS.has(method)) {
|
|
29850
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29851
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29852
|
+
return { kind: "cache-public", value: arg1 };
|
|
29853
|
+
}
|
|
29854
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29855
|
+
return { kind: "vary" };
|
|
29856
|
+
}
|
|
29857
|
+
}
|
|
29858
|
+
if (JS_RES_RECEIVERS.has(receiver) && method === "vary") {
|
|
29859
|
+
const v = arg0 ?? "";
|
|
29860
|
+
if (isVaryCovering(v) || v === "")
|
|
29861
|
+
return { kind: "vary" };
|
|
29862
|
+
}
|
|
29863
|
+
if (JS_RES_RECEIVERS.has(receiver) && method === "cookie") {
|
|
29864
|
+
return { kind: "auth" };
|
|
29865
|
+
}
|
|
29866
|
+
return null;
|
|
29867
|
+
}
|
|
29868
|
+
if (language === "python") {
|
|
29869
|
+
if (receiver === "request.cookies" || receiver === "request.session") {
|
|
29870
|
+
return { kind: "auth" };
|
|
29871
|
+
}
|
|
29872
|
+
if (receiver === "request.headers" && method === "get") {
|
|
29873
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29874
|
+
if (v === "authorization" || v === "cookie")
|
|
29875
|
+
return { kind: "auth" };
|
|
29876
|
+
}
|
|
29877
|
+
if ((receiver === "response" || receiver === "resp") && method === "set_cookie") {
|
|
29878
|
+
return { kind: "auth" };
|
|
29879
|
+
}
|
|
29880
|
+
if (method === "patch_vary_headers")
|
|
29881
|
+
return { kind: "vary" };
|
|
29882
|
+
if (method === "patch_cache_control") {
|
|
29883
|
+
const argTxt = call.arguments.map((a) => a.expression ?? "").join(",");
|
|
29884
|
+
if (/\bpublic\s*=\s*True\b/.test(argTxt)) {
|
|
29885
|
+
return { kind: "cache-public", value: argTxt };
|
|
29886
|
+
}
|
|
29887
|
+
}
|
|
29888
|
+
return null;
|
|
29889
|
+
}
|
|
29890
|
+
if (language === "go") {
|
|
29891
|
+
if ((receiver === "w.Header()" || receiver === "rw.Header()") && GO_HEADER_METHODS.has(method)) {
|
|
29892
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29893
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29894
|
+
return { kind: "cache-public", value: arg1 };
|
|
29895
|
+
}
|
|
29896
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29897
|
+
return { kind: "vary" };
|
|
29898
|
+
}
|
|
29899
|
+
}
|
|
29900
|
+
if (receiver === "c" && method === "Header") {
|
|
29901
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29902
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29903
|
+
return { kind: "cache-public", value: arg1 };
|
|
29904
|
+
}
|
|
29905
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29906
|
+
return { kind: "vary" };
|
|
29907
|
+
}
|
|
29908
|
+
}
|
|
29909
|
+
if (receiver === "r" && (method === "Cookie" || method === "BasicAuth")) {
|
|
29910
|
+
return { kind: "auth" };
|
|
29911
|
+
}
|
|
29912
|
+
if ((receiver === "r.Header" || receiver === "r.Header()") && method === "Get") {
|
|
29913
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29914
|
+
if (v === "cookie" || v === "authorization")
|
|
29915
|
+
return { kind: "auth" };
|
|
29916
|
+
}
|
|
29917
|
+
if (receiver === "http" && method === "SetCookie")
|
|
29918
|
+
return { kind: "auth" };
|
|
29919
|
+
if (receiver === "c" && (method === "Cookie" || method === "GetHeader" || method === "SetCookie")) {
|
|
29920
|
+
return { kind: "auth" };
|
|
29921
|
+
}
|
|
29922
|
+
return null;
|
|
29923
|
+
}
|
|
29924
|
+
if (language === "java") {
|
|
29925
|
+
if ((receiver === "response" || receiver === "resp") && JAVA_HEADER_METHODS.has(method)) {
|
|
29926
|
+
const header = (arg0 ?? "").toLowerCase();
|
|
29927
|
+
if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
|
|
29928
|
+
return { kind: "cache-public", value: arg1 };
|
|
29929
|
+
}
|
|
29930
|
+
if (header === "vary" && arg1 && isVaryCovering(arg1)) {
|
|
29931
|
+
return { kind: "vary" };
|
|
29932
|
+
}
|
|
29933
|
+
}
|
|
29934
|
+
if ((receiver === "headers" || receiver === "httpHeaders") && method === "setCacheControl") {
|
|
29935
|
+
return { kind: "cache-public", value: "HttpHeaders.setCacheControl(...)" };
|
|
29936
|
+
}
|
|
29937
|
+
if ((receiver === "headers" || receiver === "httpHeaders") && method === "setVary") {
|
|
29938
|
+
return { kind: "vary" };
|
|
29939
|
+
}
|
|
29940
|
+
if (receiver === "request" && method === "getCookies") {
|
|
29941
|
+
return { kind: "auth" };
|
|
29942
|
+
}
|
|
29943
|
+
if (receiver === "request" && method === "getHeader") {
|
|
29944
|
+
const v = (arg0 ?? "").toLowerCase();
|
|
29945
|
+
if (v === "cookie" || v === "authorization")
|
|
29946
|
+
return { kind: "auth" };
|
|
29947
|
+
}
|
|
29948
|
+
if ((receiver === "response" || receiver === "resp") && method === "addCookie") {
|
|
29949
|
+
return { kind: "auth" };
|
|
29950
|
+
}
|
|
29951
|
+
return null;
|
|
29952
|
+
}
|
|
29953
|
+
return null;
|
|
29954
|
+
}
|
|
29955
|
+
function authSignalRegex(language) {
|
|
29956
|
+
switch (language) {
|
|
29957
|
+
case "javascript":
|
|
29958
|
+
case "typescript":
|
|
29959
|
+
return JS_AUTH_SIGNAL_RE;
|
|
29960
|
+
case "python":
|
|
29961
|
+
return PY_AUTH_SIGNAL_RE;
|
|
29962
|
+
case "go":
|
|
29963
|
+
return GO_AUTH_SIGNAL_RE;
|
|
29964
|
+
case "java":
|
|
29965
|
+
return JAVA_AUTH_SIGNAL_RE;
|
|
29966
|
+
default:
|
|
29967
|
+
return null;
|
|
29968
|
+
}
|
|
29969
|
+
}
|
|
29970
|
+
function scanWindow(code, language, startLine, endLine) {
|
|
29971
|
+
const lines = code.split(`
|
|
29972
|
+
`);
|
|
29973
|
+
const lo = Math.max(0, startLine - 1);
|
|
29974
|
+
const hi = Math.min(lines.length, endLine);
|
|
29975
|
+
const out2 = { vary: false, auth: false };
|
|
29976
|
+
const authRe = authSignalRegex(language);
|
|
29977
|
+
for (let i2 = lo;i2 < hi; i2++) {
|
|
29978
|
+
const ln = lines[i2];
|
|
29979
|
+
if (authRe && authRe.test(ln))
|
|
29980
|
+
out2.auth = true;
|
|
29981
|
+
if (language === "python") {
|
|
29982
|
+
if (!out2.cachePublic) {
|
|
29983
|
+
const mc = PY_CACHE_HEADER_ASSIGN_RE.exec(ln);
|
|
29984
|
+
if (mc && isSharedCacheable(mc[2])) {
|
|
29985
|
+
out2.cachePublic = { line: i2 + 1, value: mc[2] };
|
|
29986
|
+
}
|
|
29987
|
+
}
|
|
29988
|
+
if (!out2.cachePublic) {
|
|
29989
|
+
const md = PY_CACHE_CONTROL_DECORATOR_RE.exec(ln);
|
|
29990
|
+
if (md) {
|
|
29991
|
+
const argTxt = md[1];
|
|
29992
|
+
if (/\bpublic\s*=\s*True\b/.test(argTxt) && (/\bmax_age\s*=\s*[1-9]\d*\b/.test(argTxt) || !/max_age/.test(argTxt))) {
|
|
29993
|
+
out2.cachePublic = { line: i2 + 1, value: argTxt };
|
|
29994
|
+
}
|
|
29995
|
+
}
|
|
29996
|
+
}
|
|
29997
|
+
if (!out2.vary) {
|
|
29998
|
+
const mv = PY_VARY_HEADER_ASSIGN_RE.exec(ln);
|
|
29999
|
+
if (mv && isVaryCovering(mv[2]))
|
|
30000
|
+
out2.vary = true;
|
|
30001
|
+
if (PY_VARY_DECORATOR_RE.test(ln))
|
|
30002
|
+
out2.vary = true;
|
|
30003
|
+
}
|
|
30004
|
+
}
|
|
30005
|
+
}
|
|
30006
|
+
return out2;
|
|
30007
|
+
}
|
|
30008
|
+
|
|
30009
|
+
class CacheNoVaryPass {
|
|
30010
|
+
name = "cache-no-vary";
|
|
30011
|
+
category = "security";
|
|
30012
|
+
run(ctx) {
|
|
30013
|
+
const { graph, language, code } = ctx;
|
|
30014
|
+
const file = graph.ir.meta.file;
|
|
30015
|
+
const findings = [];
|
|
30016
|
+
const isSupported = language === "javascript" || language === "typescript" || language === "python" || language === "go" || language === "java";
|
|
30017
|
+
if (!isSupported)
|
|
30018
|
+
return { findings };
|
|
30019
|
+
if (/(?:\.test|\.spec)\.[jt]sx?$/i.test(file) || /__tests__\/|\/tests?\//i.test(file)) {
|
|
30020
|
+
return { findings };
|
|
30021
|
+
}
|
|
30022
|
+
const callsByHandler = new Map;
|
|
30023
|
+
for (const call of graph.ir.calls) {
|
|
30024
|
+
const key = call.in_method ?? "<top>";
|
|
30025
|
+
let arr = callsByHandler.get(key);
|
|
30026
|
+
if (!arr) {
|
|
30027
|
+
arr = [];
|
|
30028
|
+
callsByHandler.set(key, arr);
|
|
30029
|
+
}
|
|
30030
|
+
arr.push(call);
|
|
30031
|
+
}
|
|
30032
|
+
const emit = (line, handler, cacheValue) => {
|
|
30033
|
+
if (findings.some((f) => f.line === line && f.handler === handler))
|
|
30034
|
+
return;
|
|
30035
|
+
findings.push({ line, language, handler, cacheValue });
|
|
30036
|
+
ctx.addFinding({
|
|
30037
|
+
id: `${this.name}-${file}-${line}`,
|
|
30038
|
+
pass: this.name,
|
|
30039
|
+
category: this.category,
|
|
30040
|
+
rule_id: this.name,
|
|
30041
|
+
cwe: "CWE-524",
|
|
30042
|
+
severity: "medium",
|
|
30043
|
+
level: "warning",
|
|
30044
|
+
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)`,
|
|
30045
|
+
file,
|
|
30046
|
+
line,
|
|
30047
|
+
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.`,
|
|
30048
|
+
evidence: {
|
|
30049
|
+
language,
|
|
30050
|
+
handler: handler ?? "<top>",
|
|
30051
|
+
cacheValue
|
|
30052
|
+
}
|
|
30053
|
+
});
|
|
30054
|
+
};
|
|
30055
|
+
for (const [handlerKey, calls] of callsByHandler) {
|
|
30056
|
+
const handler = handlerKey === "<top>" ? null : handlerKey;
|
|
30057
|
+
const cachePublicHits = [];
|
|
30058
|
+
let varyFromCalls = false;
|
|
30059
|
+
let authFromCalls = false;
|
|
30060
|
+
for (const call of calls) {
|
|
30061
|
+
const cls = classifyCall(call, language);
|
|
30062
|
+
if (!cls)
|
|
30063
|
+
continue;
|
|
30064
|
+
if (cls.kind === "cache-public") {
|
|
30065
|
+
cachePublicHits.push({
|
|
30066
|
+
line: call.location.line,
|
|
30067
|
+
value: cls.value ?? ""
|
|
30068
|
+
});
|
|
30069
|
+
} else if (cls.kind === "vary") {
|
|
30070
|
+
varyFromCalls = true;
|
|
30071
|
+
} else if (cls.kind === "auth") {
|
|
30072
|
+
authFromCalls = true;
|
|
30073
|
+
}
|
|
30074
|
+
}
|
|
30075
|
+
let minLine = Infinity;
|
|
30076
|
+
let maxLine = -Infinity;
|
|
30077
|
+
for (const c of calls) {
|
|
30078
|
+
if (c.location?.line) {
|
|
30079
|
+
minLine = Math.min(minLine, c.location.line);
|
|
30080
|
+
maxLine = Math.max(maxLine, c.location.line);
|
|
30081
|
+
}
|
|
30082
|
+
}
|
|
30083
|
+
if (minLine === Infinity)
|
|
30084
|
+
continue;
|
|
30085
|
+
const winStart = Math.max(1, minLine - 5);
|
|
30086
|
+
const winEnd = maxLine + 5;
|
|
30087
|
+
const winScan = scanWindow(code, language, winStart, winEnd);
|
|
30088
|
+
if (winScan.cachePublic)
|
|
30089
|
+
cachePublicHits.push(winScan.cachePublic);
|
|
30090
|
+
const vary = varyFromCalls || winScan.vary;
|
|
30091
|
+
const auth = authFromCalls || winScan.auth;
|
|
30092
|
+
if (cachePublicHits.length === 0)
|
|
30093
|
+
continue;
|
|
30094
|
+
if (vary)
|
|
30095
|
+
continue;
|
|
30096
|
+
if (!auth)
|
|
30097
|
+
continue;
|
|
30098
|
+
emit(cachePublicHits[0].line, handler, cachePublicHits[0].value);
|
|
30099
|
+
}
|
|
30100
|
+
return { findings };
|
|
30101
|
+
}
|
|
30102
|
+
}
|
|
30103
|
+
|
|
29817
30104
|
// ../circle-ir/dist/analysis/passes/jwt-verify-disabled-pass.js
|
|
29818
30105
|
var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
|
|
29819
30106
|
var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
|
|
@@ -31408,6 +31695,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
31408
31695
|
pipeline.add(new TlsVerifyDisabledPass);
|
|
31409
31696
|
if (!disabledPasses.has("module-side-effect"))
|
|
31410
31697
|
pipeline.add(new ModuleSideEffectPass);
|
|
31698
|
+
if (!disabledPasses.has("cache-no-vary"))
|
|
31699
|
+
pipeline.add(new CacheNoVaryPass);
|
|
31411
31700
|
if (!disabledPasses.has("jwt-verify-disabled"))
|
|
31412
31701
|
pipeline.add(new JwtVerifyDisabledPass);
|
|
31413
31702
|
if (!disabledPasses.has("csrf-protection-disabled"))
|
|
@@ -31609,7 +31898,7 @@ var colors = {
|
|
|
31609
31898
|
};
|
|
31610
31899
|
|
|
31611
31900
|
// src/version.ts
|
|
31612
|
-
var version = "3.
|
|
31901
|
+
var version = "3.70.0";
|
|
31613
31902
|
|
|
31614
31903
|
// src/formatters.ts
|
|
31615
31904
|
var SINK_SEVERITY = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.70.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"circle-ir": "^3.
|
|
68
|
+
"circle-ir": "^3.70.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^25.5.0",
|