circle-ir 3.124.0 → 3.126.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.
|
@@ -23654,6 +23654,9 @@ var LanguageSourcesPass = class {
|
|
|
23654
23654
|
if (language === "python") {
|
|
23655
23655
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
23656
23656
|
additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
|
|
23657
|
+
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
23658
|
+
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
23659
|
+
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
23657
23660
|
const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
|
|
23658
23661
|
for (const finding of pyMisconfigFindings) {
|
|
23659
23662
|
ctx.addFinding(finding);
|
|
@@ -23672,6 +23675,7 @@ var LanguageSourcesPass = class {
|
|
|
23672
23675
|
additionalSanitizers.push(...findJsCryptoHashSanitizers(code));
|
|
23673
23676
|
additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
|
|
23674
23677
|
additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
|
|
23678
|
+
additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
|
|
23675
23679
|
}
|
|
23676
23680
|
if (language === "java") {
|
|
23677
23681
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -24920,6 +24924,227 @@ function findPythonRangeCheckGuardSanitizers(code) {
|
|
|
24920
24924
|
}
|
|
24921
24925
|
return sanitizers;
|
|
24922
24926
|
}
|
|
24927
|
+
function findPythonRegexAllowlistWrapperSanitizers(code) {
|
|
24928
|
+
const sanitizers = [];
|
|
24929
|
+
const lines = code.split("\n");
|
|
24930
|
+
const defOpen = /^(\s*)def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*:\s*$/;
|
|
24931
|
+
const tightRegex = /r?["'](?:\^)?(?:\\w[+*]|\\d[+*]|\[[A-Za-z0-9_\\\-\.\s]+\][+*]|[A-Za-z0-9_\-\.]+)(?:\$)?["']/;
|
|
24932
|
+
const terminator = /\b(return\s+(?:None|"|'|\(|\[|\{|False|jsonify|make_response|redirect|abort)|raise\s+\w|abort\s*\(|sys\.exit\s*\()/;
|
|
24933
|
+
const wrappers = [];
|
|
24934
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
24935
|
+
const m = defOpen.exec(lines[i2]);
|
|
24936
|
+
if (!m) continue;
|
|
24937
|
+
const defIndent = m[1].length;
|
|
24938
|
+
const wrapperName = m[2];
|
|
24939
|
+
const argName = m[3];
|
|
24940
|
+
let foundGuard = false;
|
|
24941
|
+
let returnsArg = false;
|
|
24942
|
+
let blockEnd = -1;
|
|
24943
|
+
const maxScan = Math.min(lines.length, i2 + 40);
|
|
24944
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
24945
|
+
const line = lines[j];
|
|
24946
|
+
if (line.trim() === "") continue;
|
|
24947
|
+
const indent = line.length - line.trimStart().length;
|
|
24948
|
+
if (indent <= defIndent) {
|
|
24949
|
+
blockEnd = j - 1;
|
|
24950
|
+
break;
|
|
24951
|
+
}
|
|
24952
|
+
if (!foundGuard) {
|
|
24953
|
+
const guard = new RegExp(
|
|
24954
|
+
`if\\s+not\\s+re\\.(?:fullmatch|match)\\s*\\(\\s*(${tightRegex.source})\\s*,\\s*${argName}\\s*\\)\\s*:`
|
|
24955
|
+
);
|
|
24956
|
+
if (guard.test(line)) {
|
|
24957
|
+
for (let k = j + 1; k < maxScan; k++) {
|
|
24958
|
+
const klin = lines[k];
|
|
24959
|
+
if (klin.trim() === "") continue;
|
|
24960
|
+
const kindent = klin.length - klin.trimStart().length;
|
|
24961
|
+
if (kindent <= indent) break;
|
|
24962
|
+
if (terminator.test(klin)) foundGuard = true;
|
|
24963
|
+
break;
|
|
24964
|
+
}
|
|
24965
|
+
}
|
|
24966
|
+
}
|
|
24967
|
+
if (new RegExp(`^\\s*return\\s+${argName}\\s*$`).test(line)) {
|
|
24968
|
+
returnsArg = true;
|
|
24969
|
+
}
|
|
24970
|
+
}
|
|
24971
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 39);
|
|
24972
|
+
if (!foundGuard || !returnsArg) continue;
|
|
24973
|
+
wrappers.push({ name: wrapperName, defLine: i2, defIndent });
|
|
24974
|
+
}
|
|
24975
|
+
if (wrappers.length === 0) return sanitizers;
|
|
24976
|
+
const wrapperKinds = [
|
|
24977
|
+
"ldap_injection",
|
|
24978
|
+
"xpath_injection",
|
|
24979
|
+
"sql_injection",
|
|
24980
|
+
"command_injection",
|
|
24981
|
+
"path_traversal",
|
|
24982
|
+
"xss",
|
|
24983
|
+
"external_taint_escape"
|
|
24984
|
+
];
|
|
24985
|
+
for (const w of wrappers) {
|
|
24986
|
+
const assignedCallRe = new RegExp(
|
|
24987
|
+
`^(\\s*)([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${w.name}\\s*\\(`
|
|
24988
|
+
);
|
|
24989
|
+
const bareCallRe = new RegExp(`\\b${w.name}\\s*\\(`);
|
|
24990
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
24991
|
+
if (i2 === w.defLine) continue;
|
|
24992
|
+
const line = lines[i2];
|
|
24993
|
+
if (!bareCallRe.test(line)) continue;
|
|
24994
|
+
if (i2 > w.defLine) {
|
|
24995
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
24996
|
+
if (lineIndent > w.defIndent) {
|
|
24997
|
+
let stillInside = true;
|
|
24998
|
+
for (let k = w.defLine + 1; k < i2; k++) {
|
|
24999
|
+
const kline = lines[k];
|
|
25000
|
+
if (kline.trim() === "") continue;
|
|
25001
|
+
const kindent = kline.length - kline.trimStart().length;
|
|
25002
|
+
if (kindent <= w.defIndent) {
|
|
25003
|
+
stillInside = false;
|
|
25004
|
+
break;
|
|
25005
|
+
}
|
|
25006
|
+
}
|
|
25007
|
+
if (stillInside) continue;
|
|
25008
|
+
}
|
|
25009
|
+
}
|
|
25010
|
+
sanitizers.push({
|
|
25011
|
+
type: "python_regex_allowlist_wrapper",
|
|
25012
|
+
method: w.name,
|
|
25013
|
+
line: i2 + 1,
|
|
25014
|
+
sanitizes: [...wrapperKinds]
|
|
25015
|
+
});
|
|
25016
|
+
const am = assignedCallRe.exec(line);
|
|
25017
|
+
if (!am) continue;
|
|
25018
|
+
const callIndent = am[1].length;
|
|
25019
|
+
const validated = am[2];
|
|
25020
|
+
const validatedRe = new RegExp(`\\b${validated}\\b`);
|
|
25021
|
+
for (let j = i2 + 1; j < lines.length; j++) {
|
|
25022
|
+
const jline = lines[j];
|
|
25023
|
+
if (jline.trim() === "") continue;
|
|
25024
|
+
const jindent = jline.length - jline.trimStart().length;
|
|
25025
|
+
if (jindent < callIndent) break;
|
|
25026
|
+
if (validatedRe.test(jline)) {
|
|
25027
|
+
sanitizers.push({
|
|
25028
|
+
type: "python_regex_allowlist_wrapper",
|
|
25029
|
+
method: w.name,
|
|
25030
|
+
line: j + 1,
|
|
25031
|
+
sanitizes: [...wrapperKinds]
|
|
25032
|
+
});
|
|
25033
|
+
}
|
|
25034
|
+
}
|
|
25035
|
+
}
|
|
25036
|
+
}
|
|
25037
|
+
return sanitizers;
|
|
25038
|
+
}
|
|
25039
|
+
function findPythonSetMembershipXssGuardSanitizers(code) {
|
|
25040
|
+
const sanitizers = [];
|
|
25041
|
+
const lines = code.split("\n");
|
|
25042
|
+
const guardOpen = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
|
|
25043
|
+
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
25044
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
25045
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25046
|
+
const m = guardOpen.exec(lines[i2]);
|
|
25047
|
+
if (!m) continue;
|
|
25048
|
+
const guardIndent = m[1].length;
|
|
25049
|
+
const guardedVar = m[2];
|
|
25050
|
+
const allowName = m[3];
|
|
25051
|
+
if (!allowlistName.test(allowName)) continue;
|
|
25052
|
+
let bodyHasTerminator = false;
|
|
25053
|
+
let blockEnd = -1;
|
|
25054
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
25055
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
25056
|
+
const line = lines[j];
|
|
25057
|
+
if (line.trim() === "") continue;
|
|
25058
|
+
const indent = line.length - line.trimStart().length;
|
|
25059
|
+
if (indent <= guardIndent) {
|
|
25060
|
+
blockEnd = j - 1;
|
|
25061
|
+
break;
|
|
25062
|
+
}
|
|
25063
|
+
if (terminator.test(line)) bodyHasTerminator = true;
|
|
25064
|
+
}
|
|
25065
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
|
|
25066
|
+
if (!bodyHasTerminator) continue;
|
|
25067
|
+
const varRe = new RegExp(`\\b${guardedVar}\\b`);
|
|
25068
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
25069
|
+
const lineText = lines[l - 1];
|
|
25070
|
+
if (!varRe.test(lineText)) continue;
|
|
25071
|
+
sanitizers.push({
|
|
25072
|
+
type: "python_set_membership_xss_guard",
|
|
25073
|
+
method: "if",
|
|
25074
|
+
line: l,
|
|
25075
|
+
sanitizes: ["xss", "external_taint_escape"]
|
|
25076
|
+
});
|
|
25077
|
+
}
|
|
25078
|
+
}
|
|
25079
|
+
return sanitizers;
|
|
25080
|
+
}
|
|
25081
|
+
function findPythonDefusedXmlSanitizers(code) {
|
|
25082
|
+
const sanitizers = [];
|
|
25083
|
+
const lines = code.split("\n");
|
|
25084
|
+
const moduleAliases = /* @__PURE__ */ new Set();
|
|
25085
|
+
const fromNames = /* @__PURE__ */ new Set();
|
|
25086
|
+
const importAs = /^\s*import\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+as\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/;
|
|
25087
|
+
const importBare = /^\s*import\s+defusedxml(?:\.([A-Za-z_][A-Za-z0-9_.]*))?\s*$/;
|
|
25088
|
+
const fromImport = /^\s*from\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+import\s+(.+)$/;
|
|
25089
|
+
for (const raw of lines) {
|
|
25090
|
+
const line = raw.replace(/#.*$/, "");
|
|
25091
|
+
let m;
|
|
25092
|
+
if (m = importAs.exec(line)) {
|
|
25093
|
+
moduleAliases.add(m[1]);
|
|
25094
|
+
continue;
|
|
25095
|
+
}
|
|
25096
|
+
if (m = importBare.exec(line)) {
|
|
25097
|
+
if (m[1]) {
|
|
25098
|
+
moduleAliases.add(m[1].split(".").pop());
|
|
25099
|
+
}
|
|
25100
|
+
moduleAliases.add("defusedxml");
|
|
25101
|
+
continue;
|
|
25102
|
+
}
|
|
25103
|
+
if (m = fromImport.exec(line)) {
|
|
25104
|
+
const items = m[1].split(",");
|
|
25105
|
+
for (const item of items) {
|
|
25106
|
+
const trimmed = item.trim().replace(/[()\\]/g, "");
|
|
25107
|
+
if (!trimmed) continue;
|
|
25108
|
+
const asMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
25109
|
+
if (asMatch) {
|
|
25110
|
+
fromNames.add(asMatch[2]);
|
|
25111
|
+
} else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
|
|
25112
|
+
fromNames.add(trimmed);
|
|
25113
|
+
}
|
|
25114
|
+
}
|
|
25115
|
+
}
|
|
25116
|
+
}
|
|
25117
|
+
if (moduleAliases.size === 0 && fromNames.size === 0) return sanitizers;
|
|
25118
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25119
|
+
const line = lines[i2];
|
|
25120
|
+
if (/^\s*(?:import|from)\s+/.test(line)) continue;
|
|
25121
|
+
let matched = false;
|
|
25122
|
+
for (const alias of moduleAliases) {
|
|
25123
|
+
const re = new RegExp(`\\b${alias}\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*\\s*\\(`);
|
|
25124
|
+
if (re.test(line)) {
|
|
25125
|
+
matched = true;
|
|
25126
|
+
break;
|
|
25127
|
+
}
|
|
25128
|
+
}
|
|
25129
|
+
if (!matched) {
|
|
25130
|
+
for (const name2 of fromNames) {
|
|
25131
|
+
const re = new RegExp(`\\b${name2}\\s*\\(`);
|
|
25132
|
+
if (re.test(line)) {
|
|
25133
|
+
matched = true;
|
|
25134
|
+
break;
|
|
25135
|
+
}
|
|
25136
|
+
}
|
|
25137
|
+
}
|
|
25138
|
+
if (!matched) continue;
|
|
25139
|
+
sanitizers.push({
|
|
25140
|
+
type: "python_defusedxml_import",
|
|
25141
|
+
method: "defusedxml",
|
|
25142
|
+
line: i2 + 1,
|
|
25143
|
+
sanitizes: ["xxe", "external_taint_escape"]
|
|
25144
|
+
});
|
|
25145
|
+
}
|
|
25146
|
+
return sanitizers;
|
|
25147
|
+
}
|
|
24923
25148
|
function findRustSetAllowlistGuardSanitizers(code) {
|
|
24924
25149
|
const sanitizers = [];
|
|
24925
25150
|
const lines = code.split("\n");
|
|
@@ -25117,6 +25342,89 @@ function findJsWrapperFunctionSanitizers(code) {
|
|
|
25117
25342
|
}
|
|
25118
25343
|
return sanitizers;
|
|
25119
25344
|
}
|
|
25345
|
+
function findJsSsrfAllowlistGuardSanitizers(code) {
|
|
25346
|
+
const sanitizers = [];
|
|
25347
|
+
const lines = code.split("\n");
|
|
25348
|
+
const urlAliasToHostAlias = /* @__PURE__ */ new Map();
|
|
25349
|
+
const urlAliasDecl = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*new\s+URL\s*\(\s*([A-Za-z_][\w.[\]'"`]*)\s*\)/;
|
|
25350
|
+
const hostAliasDecl = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\.(?:hostname|host)\b/;
|
|
25351
|
+
const hostFromUrl = /* @__PURE__ */ new Map();
|
|
25352
|
+
for (const line of lines) {
|
|
25353
|
+
const ua = urlAliasDecl.exec(line);
|
|
25354
|
+
if (ua) urlAliasToHostAlias.set(ua[1], ua[2]);
|
|
25355
|
+
const ha = hostAliasDecl.exec(line);
|
|
25356
|
+
if (ha) hostFromUrl.set(ha[1], ha[2]);
|
|
25357
|
+
}
|
|
25358
|
+
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
25359
|
+
const guardHas = /if\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*(?:has|includes)\s*\(\s*([A-Za-z_]\w*)(?:\s*\.\s*(?:hostname|host))?\s*\)\s*\)/;
|
|
25360
|
+
const guardIndexOf = /if\s*\(\s*([A-Za-z_]\w*)\s*\.\s*indexOf\s*\(\s*([A-Za-z_]\w*)(?:\s*\.\s*(?:hostname|host))?\s*\)\s*<\s*0\s*\)/;
|
|
25361
|
+
const terminator = /\b(return|throw|res\s*\.\s*status\s*\([^)]*\)\s*\.\s*(?:send|end|json)\s*\()/;
|
|
25362
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25363
|
+
const line = lines[i2];
|
|
25364
|
+
let m = guardHas.exec(line);
|
|
25365
|
+
let allow = null;
|
|
25366
|
+
let guardedVar = null;
|
|
25367
|
+
if (m) {
|
|
25368
|
+
allow = m[1];
|
|
25369
|
+
guardedVar = m[2];
|
|
25370
|
+
} else {
|
|
25371
|
+
m = guardIndexOf.exec(line);
|
|
25372
|
+
if (m) {
|
|
25373
|
+
allow = m[1];
|
|
25374
|
+
guardedVar = m[2];
|
|
25375
|
+
}
|
|
25376
|
+
}
|
|
25377
|
+
if (!allow || !guardedVar) continue;
|
|
25378
|
+
if (!allowlistName.test(allow)) continue;
|
|
25379
|
+
let bodyHasTerminator = false;
|
|
25380
|
+
let blockEnd = -1;
|
|
25381
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
25382
|
+
let braceDepth = 0;
|
|
25383
|
+
let started = false;
|
|
25384
|
+
for (let j = i2; j < maxScan; j++) {
|
|
25385
|
+
const ln = lines[j];
|
|
25386
|
+
for (const ch of ln) {
|
|
25387
|
+
if (ch === "{") {
|
|
25388
|
+
braceDepth++;
|
|
25389
|
+
started = true;
|
|
25390
|
+
} else if (ch === "}") {
|
|
25391
|
+
braceDepth--;
|
|
25392
|
+
if (started && braceDepth === 0) {
|
|
25393
|
+
blockEnd = j;
|
|
25394
|
+
break;
|
|
25395
|
+
}
|
|
25396
|
+
}
|
|
25397
|
+
}
|
|
25398
|
+
if (started && j > i2 && terminator.test(ln)) bodyHasTerminator = true;
|
|
25399
|
+
if (blockEnd !== -1) break;
|
|
25400
|
+
}
|
|
25401
|
+
if (!started) continue;
|
|
25402
|
+
if (blockEnd === -1) continue;
|
|
25403
|
+
if (!bodyHasTerminator) continue;
|
|
25404
|
+
const safeNames = /* @__PURE__ */ new Set([guardedVar]);
|
|
25405
|
+
if (hostFromUrl.has(guardedVar)) {
|
|
25406
|
+
safeNames.add(hostFromUrl.get(guardedVar));
|
|
25407
|
+
}
|
|
25408
|
+
for (const [hostName, urlName] of hostFromUrl) {
|
|
25409
|
+
if (urlName === guardedVar) safeNames.add(hostName);
|
|
25410
|
+
}
|
|
25411
|
+
void urlAliasToHostAlias;
|
|
25412
|
+
const refRes = [...safeNames].map(
|
|
25413
|
+
(n) => new RegExp(`\\b${n}\\b`)
|
|
25414
|
+
);
|
|
25415
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
25416
|
+
const lineText = lines[l - 1];
|
|
25417
|
+
if (!refRes.some((re) => re.test(lineText))) continue;
|
|
25418
|
+
sanitizers.push({
|
|
25419
|
+
type: "js_ssrf_allowlist_guard",
|
|
25420
|
+
method: "if",
|
|
25421
|
+
line: l,
|
|
25422
|
+
sanitizes: ["ssrf", "external_taint_escape"]
|
|
25423
|
+
});
|
|
25424
|
+
}
|
|
25425
|
+
}
|
|
25426
|
+
return sanitizers;
|
|
25427
|
+
}
|
|
25120
25428
|
function collectEnvSecretVars(lines, language) {
|
|
25121
25429
|
const out2 = /* @__PURE__ */ new Map();
|
|
25122
25430
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.126.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",
|