circle-ir 3.108.0 → 3.111.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/nodejs.json +18 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +38 -15
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +496 -0
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +26 -2
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +145 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +472 -21
- package/dist/core/circle-ir-core.cjs +120 -16
- package/dist/core/circle-ir-core.js +120 -16
- package/package.json +1 -1
|
@@ -11169,10 +11169,20 @@ var DEFAULT_SINKS = [
|
|
|
11169
11169
|
{ method: "bash", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11170
11170
|
{ method: "shell", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11171
11171
|
{ method: "sh", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11172
|
-
|
|
11172
|
+
// cognium-dev #187 Sprint 54: arg_positions [0, 1] so the JS shell-mode shape
|
|
11173
|
+
// `spawn('sh', ['-c', tainted])` / `execFile('/bin/sh', ['-c', tainted])`
|
|
11174
|
+
// surfaces taint at the argv-array position (arg[1]).
|
|
11175
|
+
{ method: "spawn", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11176
|
+
{ method: "spawnSync", languages: ["javascript", "typescript"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11177
|
+
{ method: "execFile", languages: ["javascript", "typescript"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11178
|
+
{ method: "execFileSync", languages: ["javascript", "typescript"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11173
11179
|
{ method: "fork", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11174
11180
|
{ method: "popen", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11175
11181
|
{ method: "system", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11182
|
+
// execa (npm) — parses tainted shell-style strings into program+argv;
|
|
11183
|
+
// arg[0] is shell-injectable. cognium-dev #187 Sprint 54.
|
|
11184
|
+
{ method: "command", class: "execa", languages: ["javascript", "typescript"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11185
|
+
{ method: "commandSync", class: "execa", languages: ["javascript", "typescript"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11176
11186
|
// Apache Commons Exec
|
|
11177
11187
|
// Note: bare class 'Executor' removed (see comment above) — DefaultExecutor matched explicitly.
|
|
11178
11188
|
{ method: "setCommandline", class: "DefaultExecutor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -12183,7 +12193,9 @@ var DEFAULT_SINKS = [
|
|
|
12183
12193
|
{ method: "aggregate", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0] },
|
|
12184
12194
|
// pymongo dynamic attribute-access pattern: `db.users.find({...})` — receiver
|
|
12185
12195
|
// class isn't statically known. Method-only entries restricted to Python.
|
|
12186
|
-
// cognium-dev#104 Sprint 22
|
|
12196
|
+
// cognium-dev#104 Sprint 22, #194 Sprint 54 added `find`/`aggregate`.
|
|
12197
|
+
{ method: "find", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
12198
|
+
{ method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
12187
12199
|
{ method: "find_one", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
12188
12200
|
{ method: "update_one", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0, 1], languages: ["python"] },
|
|
12189
12201
|
{ method: "update_many", type: "nosql_injection", cwe: "CWE-943", severity: "critical", arg_positions: [0, 1], languages: ["python"] },
|
|
@@ -12195,21 +12207,32 @@ var DEFAULT_SINKS = [
|
|
|
12195
12207
|
{ method: "from_string", class: "Template", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
12196
12208
|
{ method: "Template", class: "jinja2", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
12197
12209
|
{ method: "Template", class: "mako", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
12198
|
-
// Python Log Injection
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12210
|
+
// Python Log Injection — cognium-dev #193: positions [0, 1, 2, 3, 4]
|
|
12211
|
+
// cover both the format string (arg 0) and Python `logging`'s positional
|
|
12212
|
+
// *args (1..N), which get rendered into the log line via `%` substitution
|
|
12213
|
+
// (e.g. `log.warning("user=%s", user)` taints via arg 1). Five-arg cap
|
|
12214
|
+
// matches the established explicit-enumeration pattern used elsewhere in
|
|
12215
|
+
// this file.
|
|
12216
|
+
{ method: "info", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12217
|
+
{ method: "warning", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12218
|
+
{ method: "warn", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12219
|
+
{ method: "error", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12220
|
+
{ method: "debug", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12221
|
+
{ method: "critical", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12222
|
+
{ method: "fatal", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12223
|
+
{ method: "exception", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12224
|
+
{ method: "log", class: "logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3, 4] },
|
|
12204
12225
|
// Python `logging` module top-level functions (e.g. logging.info(...))
|
|
12205
12226
|
// — cognium-dev#104 Sprint 22: OOP fixtures use `import logging; logging.info(self.msg)`.
|
|
12206
|
-
{ method: "info", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0] },
|
|
12207
|
-
{ method: "warning", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0] },
|
|
12208
|
-
{ method: "
|
|
12209
|
-
{ method: "
|
|
12210
|
-
{ method: "
|
|
12211
|
-
{ method: "
|
|
12212
|
-
{ method: "
|
|
12227
|
+
{ method: "info", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12228
|
+
{ method: "warning", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12229
|
+
{ method: "warn", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12230
|
+
{ method: "error", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12231
|
+
{ method: "debug", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12232
|
+
{ method: "critical", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12233
|
+
{ method: "fatal", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12234
|
+
{ method: "log", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3, 4] },
|
|
12235
|
+
{ method: "exception", class: "logging", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3, 4] },
|
|
12213
12236
|
// =========================================================================
|
|
12214
12237
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
12215
12238
|
// =========================================================================
|
|
@@ -12839,10 +12862,50 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
12839
12862
|
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
12840
12863
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
12841
12864
|
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
12842
|
-
const
|
|
12865
|
+
const sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
|
|
12866
|
+
const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines);
|
|
12843
12867
|
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
12844
12868
|
return { sources, sinks, sanitizers };
|
|
12845
12869
|
}
|
|
12870
|
+
function expandPromisifyAliases(patterns, sourceLines, language) {
|
|
12871
|
+
if (!sourceLines || sourceLines.length === 0) return patterns;
|
|
12872
|
+
if (language !== "javascript" && language !== "typescript") return patterns;
|
|
12873
|
+
const PROMISIFY_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:util\s*\.\s*)?promisify\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
|
|
12874
|
+
const innerToPattern = /* @__PURE__ */ new Map();
|
|
12875
|
+
for (const p of patterns) {
|
|
12876
|
+
if (p.languages && !p.languages.includes(language)) continue;
|
|
12877
|
+
if (p.class !== void 0) continue;
|
|
12878
|
+
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
12879
|
+
}
|
|
12880
|
+
for (const p of patterns) {
|
|
12881
|
+
if (p.languages && !p.languages.includes(language)) continue;
|
|
12882
|
+
if (innerToPattern.has(p.method)) continue;
|
|
12883
|
+
innerToPattern.set(p.method, p);
|
|
12884
|
+
}
|
|
12885
|
+
const aliasPatterns = [];
|
|
12886
|
+
const seenAliases = /* @__PURE__ */ new Set();
|
|
12887
|
+
for (const line of sourceLines) {
|
|
12888
|
+
const m = PROMISIFY_RE.exec(line);
|
|
12889
|
+
if (!m) continue;
|
|
12890
|
+
const aliasName = m[1];
|
|
12891
|
+
const innerName = m[2];
|
|
12892
|
+
if (!aliasName || !innerName) continue;
|
|
12893
|
+
if (seenAliases.has(aliasName)) continue;
|
|
12894
|
+
const template = innerToPattern.get(innerName);
|
|
12895
|
+
if (!template) continue;
|
|
12896
|
+
seenAliases.add(aliasName);
|
|
12897
|
+
aliasPatterns.push({
|
|
12898
|
+
method: aliasName,
|
|
12899
|
+
type: template.type,
|
|
12900
|
+
cwe: template.cwe,
|
|
12901
|
+
severity: template.severity,
|
|
12902
|
+
arg_positions: template.arg_positions,
|
|
12903
|
+
languages: [language],
|
|
12904
|
+
note: `util.promisify alias of ${innerName} \u2014 cognium-dev #187`
|
|
12905
|
+
});
|
|
12906
|
+
}
|
|
12907
|
+
return aliasPatterns.length === 0 ? patterns : patterns.concat(aliasPatterns);
|
|
12908
|
+
}
|
|
12846
12909
|
function attachSourceLineCode(sources, sinks, code) {
|
|
12847
12910
|
const lines = code.split("\n");
|
|
12848
12911
|
for (const s of sources) {
|
|
@@ -13208,6 +13271,41 @@ function isSafeGoExecCommandCall(call, pattern, language) {
|
|
|
13208
13271
|
if (SHELL_PROGRAMS.has(program)) return false;
|
|
13209
13272
|
return true;
|
|
13210
13273
|
}
|
|
13274
|
+
function isSafeJSChildProcessCall(call, pattern, language) {
|
|
13275
|
+
if (language !== "javascript" && language !== "typescript") return false;
|
|
13276
|
+
if (pattern.type !== "command_injection") return false;
|
|
13277
|
+
const m = call.method_name;
|
|
13278
|
+
if (m !== "spawn" && m !== "spawnSync" && m !== "execFile" && m !== "execFileSync") return false;
|
|
13279
|
+
const programArg = call.arguments.find((a) => a.position === 0);
|
|
13280
|
+
if (!programArg) return false;
|
|
13281
|
+
let program;
|
|
13282
|
+
if (programArg.literal !== null && programArg.literal !== void 0) {
|
|
13283
|
+
program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
|
|
13284
|
+
} else {
|
|
13285
|
+
const expr = (programArg.expression ?? "").trim();
|
|
13286
|
+
if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
|
|
13287
|
+
return false;
|
|
13288
|
+
}
|
|
13289
|
+
const stripped = expr.slice(1, -1);
|
|
13290
|
+
program = stripped.split("/").pop() ?? stripped;
|
|
13291
|
+
}
|
|
13292
|
+
const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
|
|
13293
|
+
"sh",
|
|
13294
|
+
"bash",
|
|
13295
|
+
"zsh",
|
|
13296
|
+
"dash",
|
|
13297
|
+
"ash",
|
|
13298
|
+
"ksh",
|
|
13299
|
+
"cmd",
|
|
13300
|
+
"cmd.exe",
|
|
13301
|
+
"powershell",
|
|
13302
|
+
"pwsh",
|
|
13303
|
+
"powershell.exe",
|
|
13304
|
+
"pwsh.exe"
|
|
13305
|
+
]);
|
|
13306
|
+
if (SHELL_PROGRAMS.has(program)) return false;
|
|
13307
|
+
return true;
|
|
13308
|
+
}
|
|
13211
13309
|
function isSafeRustCommandCall(call, pattern, language) {
|
|
13212
13310
|
if (language !== "rust") return false;
|
|
13213
13311
|
if (pattern.type !== "command_injection") return false;
|
|
@@ -13422,6 +13520,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13422
13520
|
if (isSafeRustCommandCall(call, pattern, language)) {
|
|
13423
13521
|
continue;
|
|
13424
13522
|
}
|
|
13523
|
+
if (isSafeJSChildProcessCall(call, pattern, language)) {
|
|
13524
|
+
continue;
|
|
13525
|
+
}
|
|
13425
13526
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
13426
13527
|
continue;
|
|
13427
13528
|
}
|
|
@@ -13859,6 +13960,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
13859
13960
|
return true;
|
|
13860
13961
|
}
|
|
13861
13962
|
}
|
|
13963
|
+
if (className === "Logger" && /\.getLogger\(.*\)$/.test(receiver)) {
|
|
13964
|
+
return true;
|
|
13965
|
+
}
|
|
13862
13966
|
}
|
|
13863
13967
|
if (receiver.includes("::") && receiver.endsWith(")")) {
|
|
13864
13968
|
const scopedMatch = receiver.match(/^(\w+)::(\w+)\(.*\)$/);
|
|
@@ -24415,6 +24519,25 @@ var JS_XSS_SANITIZERS = [
|
|
|
24415
24519
|
/\bbypassSecurityTrust/
|
|
24416
24520
|
// Angular
|
|
24417
24521
|
];
|
|
24522
|
+
var JS_LOG_INJECTION_SANITIZERS = [
|
|
24523
|
+
/\bstripCrlf\s*\(/,
|
|
24524
|
+
/\bstripCRLF\s*\(/,
|
|
24525
|
+
/\bremoveNewlines\s*\(/,
|
|
24526
|
+
/\bsanitizeLogValue\s*\(/,
|
|
24527
|
+
// Inline CRLF-stripping regex literal: .replace(/[\r\n]/g, '')
|
|
24528
|
+
/\.replace\(\s*\/\[\s*\\r\s*\\n\s*\]\/[gimsu]*\s*,\s*['"`]['"`]\s*\)/,
|
|
24529
|
+
// Inline CRLF sequence: .replace(/\r\n/g, '')
|
|
24530
|
+
/\.replace\(\s*\/\\r\\n\/[gimsu]*\s*,\s*['"`]['"`]\s*\)/
|
|
24531
|
+
];
|
|
24532
|
+
var PY_BUILTIN_LDAP_SANITIZERS = ["escape_filter_chars", "filter_format"];
|
|
24533
|
+
var PY_LDAP_METACHARS = ["(", ")", "=", "*", "\\"];
|
|
24534
|
+
var PY_DEF_RE = /^\s*def\s+([A-Za-z_]\w*)\s*\(\s*([A-Za-z_]\w*)\s*\)\s*:\s*$/;
|
|
24535
|
+
var PY_LDAP_STRIP_RETURN_RE = /^\s*return\s+re\.sub\(\s*r?["']\[([^"'\]]+)\]["']\s*,\s*r?["']["']\s*,\s*([A-Za-z_]\w*)\s*\)\s*$/;
|
|
24536
|
+
var PY_XML_PARSER_HARDENED_RE = /\bXMLParser\s*\([^)]*\bresolve_entities\s*=\s*False\b[^)]*\)/;
|
|
24537
|
+
var PY_SQL_EXEC_METHODS = /* @__PURE__ */ new Set(["execute", "executemany"]);
|
|
24538
|
+
var PY_INLINE_FULLMATCH_RE = /\bre\s*\.\s*fullmatch\s*\(\s*r?"((?:[^"\\]|\\.)*)"/g;
|
|
24539
|
+
var PY_INLINE_MATCH_ANCHORED_RE = /\bre\s*\.\s*match\s*\(\s*r?"\^((?:[^"\\]|\\.)*)\$"/g;
|
|
24540
|
+
var PY_BIND_PLACEHOLDER_RE = /\?|%s|:[A-Za-z_]\w*/;
|
|
24418
24541
|
var DATA_PARSER_TYPES = /* @__PURE__ */ new Set([
|
|
24419
24542
|
"Parser",
|
|
24420
24543
|
// commonmark, airline, picocli, jcommander
|
|
@@ -25125,9 +25248,329 @@ var SinkFilterPass = class {
|
|
|
25125
25248
|
return true;
|
|
25126
25249
|
});
|
|
25127
25250
|
}
|
|
25251
|
+
if (["javascript", "typescript"].includes(language)) {
|
|
25252
|
+
const sourceLines = ctx.code.split("\n");
|
|
25253
|
+
filtered = filtered.filter((sink) => {
|
|
25254
|
+
if (sink.type !== "log_injection") return true;
|
|
25255
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
25256
|
+
if (JS_LOG_INJECTION_SANITIZERS.some((p) => p.test(sinkLineText))) return false;
|
|
25257
|
+
const varNames = extractSourceIdentifiers(sinkLineText);
|
|
25258
|
+
for (const v of varNames) {
|
|
25259
|
+
if (isAssignedFromSanitizerPattern(sourceLines, sink.line, v, JS_LOG_INJECTION_SANITIZERS)) {
|
|
25260
|
+
return false;
|
|
25261
|
+
}
|
|
25262
|
+
}
|
|
25263
|
+
return true;
|
|
25264
|
+
});
|
|
25265
|
+
}
|
|
25266
|
+
if (language === "python") {
|
|
25267
|
+
const sourceLines = ctx.code.split("\n");
|
|
25268
|
+
const wrappers = findPythonLdapStripWrappers(sourceLines);
|
|
25269
|
+
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
25270
|
+
if (allLdapSanitizers.length > 0) {
|
|
25271
|
+
const ldapPatterns = allLdapSanitizers.map(
|
|
25272
|
+
(name2) => new RegExp(`\\b${escapeRegex(name2)}\\s*\\(`)
|
|
25273
|
+
);
|
|
25274
|
+
filtered = filtered.filter((sink) => {
|
|
25275
|
+
if (sink.type !== "ldap_injection") return true;
|
|
25276
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
25277
|
+
if (ldapPatterns.some((p) => p.test(sinkLineText))) return false;
|
|
25278
|
+
const varNames = extractSourceIdentifiers(sinkLineText);
|
|
25279
|
+
for (const v of varNames) {
|
|
25280
|
+
if (isAssignedFromSanitizerPattern(sourceLines, sink.line, v, ldapPatterns)) {
|
|
25281
|
+
return false;
|
|
25282
|
+
}
|
|
25283
|
+
}
|
|
25284
|
+
return true;
|
|
25285
|
+
});
|
|
25286
|
+
}
|
|
25287
|
+
}
|
|
25288
|
+
if (language === "python") {
|
|
25289
|
+
const sourceLines = ctx.code.split("\n");
|
|
25290
|
+
filtered = filtered.filter((sink) => {
|
|
25291
|
+
if (sink.type !== "xxe") return true;
|
|
25292
|
+
const lookback = 30;
|
|
25293
|
+
const lo = Math.max(0, sink.line - 1 - lookback);
|
|
25294
|
+
for (let i2 = sink.line - 2; i2 >= lo; i2--) {
|
|
25295
|
+
const ln = sourceLines[i2] ?? "";
|
|
25296
|
+
if (PY_XML_PARSER_HARDENED_RE.test(ln)) return false;
|
|
25297
|
+
if (/^\s*def\s+\w+\s*\(/.test(ln)) break;
|
|
25298
|
+
}
|
|
25299
|
+
return true;
|
|
25300
|
+
});
|
|
25301
|
+
}
|
|
25302
|
+
if (language === "python") {
|
|
25303
|
+
const sourceLines = ctx.code.split("\n");
|
|
25304
|
+
filtered = filtered.filter((sink) => {
|
|
25305
|
+
if (sink.type !== "sql_injection") return true;
|
|
25306
|
+
const method = sink.method ?? "";
|
|
25307
|
+
if (!PY_SQL_EXEC_METHODS.has(method)) return true;
|
|
25308
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
25309
|
+
const fstrMatch = sinkLineText.match(/\bf"((?:[^"\\]|\\.)*)"|\bf'((?:[^'\\]|\\.)*)'/);
|
|
25310
|
+
if (!fstrMatch) return true;
|
|
25311
|
+
const fstrBody = fstrMatch[1] ?? fstrMatch[2] ?? "";
|
|
25312
|
+
const { literals, interps } = splitPyFstringTokens(fstrBody);
|
|
25313
|
+
if (interps.length === 0) return true;
|
|
25314
|
+
if (!literals.some((lit) => PY_BIND_PLACEHOLDER_RE.test(lit))) return true;
|
|
25315
|
+
let sawHelperWithGuard = false;
|
|
25316
|
+
for (const interp of interps) {
|
|
25317
|
+
const t = interp.trim();
|
|
25318
|
+
if (t === "") return true;
|
|
25319
|
+
if (/^-?\d+$/.test(t) || /^"(?:[^"\\]|\\.)*"$/.test(t) || /^'(?:[^'\\]|\\.)*'$/.test(t)) {
|
|
25320
|
+
continue;
|
|
25321
|
+
}
|
|
25322
|
+
let helperName = null;
|
|
25323
|
+
const directCall = t.match(/^([A-Za-z_]\w*)\s*\(/);
|
|
25324
|
+
if (directCall) {
|
|
25325
|
+
helperName = directCall[1] ?? null;
|
|
25326
|
+
} else if (/^[A-Za-z_]\w*$/.test(t)) {
|
|
25327
|
+
helperName = findPythonAssignedHelperCall(t, sink.line, sourceLines);
|
|
25328
|
+
if (helperName === null) return true;
|
|
25329
|
+
} else {
|
|
25330
|
+
return true;
|
|
25331
|
+
}
|
|
25332
|
+
const body2 = findPythonFunctionBody(helperName, sourceLines);
|
|
25333
|
+
if (body2 && pythonBodyHasInlineRegexAllowlistRaise(body2)) {
|
|
25334
|
+
sawHelperWithGuard = true;
|
|
25335
|
+
}
|
|
25336
|
+
}
|
|
25337
|
+
if (!sawHelperWithGuard) return true;
|
|
25338
|
+
return false;
|
|
25339
|
+
});
|
|
25340
|
+
}
|
|
25128
25341
|
return { sources, sinks: filtered, sanitizers };
|
|
25129
25342
|
}
|
|
25130
25343
|
};
|
|
25344
|
+
function escapeRegex(s) {
|
|
25345
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25346
|
+
}
|
|
25347
|
+
var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
25348
|
+
// JS/TS
|
|
25349
|
+
"if",
|
|
25350
|
+
"else",
|
|
25351
|
+
"return",
|
|
25352
|
+
"const",
|
|
25353
|
+
"let",
|
|
25354
|
+
"var",
|
|
25355
|
+
"function",
|
|
25356
|
+
"class",
|
|
25357
|
+
"new",
|
|
25358
|
+
"this",
|
|
25359
|
+
"for",
|
|
25360
|
+
"while",
|
|
25361
|
+
"do",
|
|
25362
|
+
"break",
|
|
25363
|
+
"continue",
|
|
25364
|
+
"switch",
|
|
25365
|
+
"case",
|
|
25366
|
+
"default",
|
|
25367
|
+
"try",
|
|
25368
|
+
"catch",
|
|
25369
|
+
"finally",
|
|
25370
|
+
"throw",
|
|
25371
|
+
"typeof",
|
|
25372
|
+
"instanceof",
|
|
25373
|
+
"in",
|
|
25374
|
+
"of",
|
|
25375
|
+
"await",
|
|
25376
|
+
"async",
|
|
25377
|
+
"import",
|
|
25378
|
+
"export",
|
|
25379
|
+
"from",
|
|
25380
|
+
"as",
|
|
25381
|
+
"void",
|
|
25382
|
+
"null",
|
|
25383
|
+
"undefined",
|
|
25384
|
+
"true",
|
|
25385
|
+
"false",
|
|
25386
|
+
"super",
|
|
25387
|
+
"extends",
|
|
25388
|
+
"static",
|
|
25389
|
+
// Python
|
|
25390
|
+
"def",
|
|
25391
|
+
"lambda",
|
|
25392
|
+
"yield",
|
|
25393
|
+
"pass",
|
|
25394
|
+
"with",
|
|
25395
|
+
"global",
|
|
25396
|
+
"nonlocal",
|
|
25397
|
+
"and",
|
|
25398
|
+
"or",
|
|
25399
|
+
"not",
|
|
25400
|
+
"is",
|
|
25401
|
+
"None",
|
|
25402
|
+
"True",
|
|
25403
|
+
"False",
|
|
25404
|
+
"raise",
|
|
25405
|
+
"except",
|
|
25406
|
+
"elif",
|
|
25407
|
+
"self",
|
|
25408
|
+
"cls",
|
|
25409
|
+
"print",
|
|
25410
|
+
// Common stdlib roots that show up everywhere and would balloon scans.
|
|
25411
|
+
"console",
|
|
25412
|
+
"req",
|
|
25413
|
+
"res",
|
|
25414
|
+
"request",
|
|
25415
|
+
"response"
|
|
25416
|
+
]);
|
|
25417
|
+
function extractSourceIdentifiers(text) {
|
|
25418
|
+
const out2 = [];
|
|
25419
|
+
const re = /\b([A-Za-z_]\w+)\b/g;
|
|
25420
|
+
let m;
|
|
25421
|
+
while ((m = re.exec(text)) !== null) {
|
|
25422
|
+
const name2 = m[1];
|
|
25423
|
+
if (!RESERVED_KEYWORDS.has(name2)) out2.push(name2);
|
|
25424
|
+
}
|
|
25425
|
+
return out2;
|
|
25426
|
+
}
|
|
25427
|
+
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
25428
|
+
const escapedVar = escapeRegex(varName);
|
|
25429
|
+
const assignRe = new RegExp(
|
|
25430
|
+
`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`
|
|
25431
|
+
);
|
|
25432
|
+
const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
|
|
25433
|
+
const lo = Math.max(0, sinkLine - 1 - lookback);
|
|
25434
|
+
for (let i2 = sinkLine - 2; i2 >= lo; i2--) {
|
|
25435
|
+
const ln = sourceLines[i2] ?? "";
|
|
25436
|
+
const m = ln.match(assignRe);
|
|
25437
|
+
if (m) {
|
|
25438
|
+
const rhs = (m[1] ?? "").trim();
|
|
25439
|
+
return sanitizerPatterns.some((p) => p.test(rhs));
|
|
25440
|
+
}
|
|
25441
|
+
if (fnBoundaryRe.test(ln)) break;
|
|
25442
|
+
}
|
|
25443
|
+
return false;
|
|
25444
|
+
}
|
|
25445
|
+
function findPythonLdapStripWrappers(sourceLines) {
|
|
25446
|
+
const out2 = [];
|
|
25447
|
+
for (let i2 = 0; i2 < sourceLines.length; i2++) {
|
|
25448
|
+
const defMatch = sourceLines[i2].match(PY_DEF_RE);
|
|
25449
|
+
if (!defMatch) continue;
|
|
25450
|
+
const fnName = defMatch[1];
|
|
25451
|
+
const param = defMatch[2];
|
|
25452
|
+
for (let j = i2 + 1; j < Math.min(i2 + 4, sourceLines.length); j++) {
|
|
25453
|
+
const body2 = sourceLines[j];
|
|
25454
|
+
if (/^\s*$/.test(body2) || /^\s*#/.test(body2)) continue;
|
|
25455
|
+
const ret = body2.match(PY_LDAP_STRIP_RETURN_RE);
|
|
25456
|
+
if (!ret) break;
|
|
25457
|
+
const charClass = ret[1];
|
|
25458
|
+
const argName = ret[2];
|
|
25459
|
+
if (argName !== param) break;
|
|
25460
|
+
const metaCount = PY_LDAP_METACHARS.filter((c) => charClass.includes(c)).length;
|
|
25461
|
+
if (metaCount >= 3) out2.push(fnName);
|
|
25462
|
+
break;
|
|
25463
|
+
}
|
|
25464
|
+
}
|
|
25465
|
+
return out2;
|
|
25466
|
+
}
|
|
25467
|
+
function splitPyFstringTokens(body2) {
|
|
25468
|
+
const literals = [];
|
|
25469
|
+
const interps = [];
|
|
25470
|
+
let i2 = 0;
|
|
25471
|
+
let cur = "";
|
|
25472
|
+
while (i2 < body2.length) {
|
|
25473
|
+
const ch = body2[i2] ?? "";
|
|
25474
|
+
if (ch === "{" && body2[i2 + 1] === "{") {
|
|
25475
|
+
cur += "{";
|
|
25476
|
+
i2 += 2;
|
|
25477
|
+
continue;
|
|
25478
|
+
}
|
|
25479
|
+
if (ch === "}" && body2[i2 + 1] === "}") {
|
|
25480
|
+
cur += "}";
|
|
25481
|
+
i2 += 2;
|
|
25482
|
+
continue;
|
|
25483
|
+
}
|
|
25484
|
+
if (ch === "{") {
|
|
25485
|
+
literals.push(cur);
|
|
25486
|
+
cur = "";
|
|
25487
|
+
i2++;
|
|
25488
|
+
let depth = 1;
|
|
25489
|
+
let expr = "";
|
|
25490
|
+
while (i2 < body2.length && depth > 0) {
|
|
25491
|
+
const ch2 = body2[i2] ?? "";
|
|
25492
|
+
if (ch2 === "{") {
|
|
25493
|
+
depth++;
|
|
25494
|
+
expr += ch2;
|
|
25495
|
+
i2++;
|
|
25496
|
+
continue;
|
|
25497
|
+
}
|
|
25498
|
+
if (ch2 === "}") {
|
|
25499
|
+
depth--;
|
|
25500
|
+
if (depth === 0) {
|
|
25501
|
+
i2++;
|
|
25502
|
+
break;
|
|
25503
|
+
}
|
|
25504
|
+
expr += ch2;
|
|
25505
|
+
i2++;
|
|
25506
|
+
continue;
|
|
25507
|
+
}
|
|
25508
|
+
expr += ch2;
|
|
25509
|
+
i2++;
|
|
25510
|
+
}
|
|
25511
|
+
const colonIdx = expr.indexOf(":");
|
|
25512
|
+
const interp = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
|
|
25513
|
+
interps.push(interp);
|
|
25514
|
+
continue;
|
|
25515
|
+
}
|
|
25516
|
+
cur += ch;
|
|
25517
|
+
i2++;
|
|
25518
|
+
}
|
|
25519
|
+
literals.push(cur);
|
|
25520
|
+
return { literals, interps };
|
|
25521
|
+
}
|
|
25522
|
+
function findPythonAssignedHelperCall(varName, sinkLine, sourceLines) {
|
|
25523
|
+
if (!/^[A-Za-z_]\w*$/.test(varName)) return null;
|
|
25524
|
+
const lo = Math.max(0, sinkLine - 31);
|
|
25525
|
+
const assignRe = new RegExp(
|
|
25526
|
+
`^\\s*${varName}\\s*=\\s*([A-Za-z_]\\w*)\\s*\\(`
|
|
25527
|
+
);
|
|
25528
|
+
for (let i2 = sinkLine - 2; i2 >= lo; i2--) {
|
|
25529
|
+
const ln = sourceLines[i2] ?? "";
|
|
25530
|
+
const m = ln.match(assignRe);
|
|
25531
|
+
if (m) return m[1] ?? null;
|
|
25532
|
+
}
|
|
25533
|
+
return null;
|
|
25534
|
+
}
|
|
25535
|
+
function findPythonFunctionBody(funcName, sourceLines) {
|
|
25536
|
+
if (!/^[A-Za-z_]\w*$/.test(funcName)) return null;
|
|
25537
|
+
const sigRe = new RegExp(`^(\\s*)def\\s+${funcName}\\s*\\(`);
|
|
25538
|
+
for (let i2 = 0; i2 < sourceLines.length; i2++) {
|
|
25539
|
+
const ln = sourceLines[i2] ?? "";
|
|
25540
|
+
const m = ln.match(sigRe);
|
|
25541
|
+
if (!m) continue;
|
|
25542
|
+
const baseIndent = (m[1] ?? "").length;
|
|
25543
|
+
const body2 = [];
|
|
25544
|
+
for (let j = i2 + 1; j < sourceLines.length; j++) {
|
|
25545
|
+
const ln2 = sourceLines[j] ?? "";
|
|
25546
|
+
if (ln2.trim() === "") {
|
|
25547
|
+
body2.push(ln2);
|
|
25548
|
+
continue;
|
|
25549
|
+
}
|
|
25550
|
+
const indentMatch = ln2.match(/^(\s*)/);
|
|
25551
|
+
const indent = (indentMatch?.[1] ?? "").length;
|
|
25552
|
+
if (indent <= baseIndent) break;
|
|
25553
|
+
body2.push(ln2);
|
|
25554
|
+
}
|
|
25555
|
+
return body2;
|
|
25556
|
+
}
|
|
25557
|
+
return null;
|
|
25558
|
+
}
|
|
25559
|
+
function pythonBodyHasInlineRegexAllowlistRaise(bodyLines) {
|
|
25560
|
+
const stripped = bodyLines.map((ln) => ln.replace(/#.*$/, ""));
|
|
25561
|
+
const text = stripped.join("\n");
|
|
25562
|
+
if (!/\braise\s+/.test(text)) return false;
|
|
25563
|
+
PY_INLINE_FULLMATCH_RE.lastIndex = 0;
|
|
25564
|
+
let m;
|
|
25565
|
+
while ((m = PY_INLINE_FULLMATCH_RE.exec(text)) !== null) {
|
|
25566
|
+
if (isImplicitlyAnchoredAllowlistRegex(m[1] ?? "")) return true;
|
|
25567
|
+
}
|
|
25568
|
+
PY_INLINE_MATCH_ANCHORED_RE.lastIndex = 0;
|
|
25569
|
+
while ((m = PY_INLINE_MATCH_ANCHORED_RE.exec(text)) !== null) {
|
|
25570
|
+
if (isImplicitlyAnchoredAllowlistRegex(m[1] ?? "")) return true;
|
|
25571
|
+
}
|
|
25572
|
+
return false;
|
|
25573
|
+
}
|
|
25131
25574
|
function evalArithmetic(input) {
|
|
25132
25575
|
let pos = 0;
|
|
25133
25576
|
const len = input.length;
|
|
@@ -25497,7 +25940,10 @@ var TaintPropagationPass = class {
|
|
|
25497
25940
|
if (f.sink_type !== "nosql_injection") return true;
|
|
25498
25941
|
const sink = sinkByLine.get(f.sink_line);
|
|
25499
25942
|
if (!sink || !sink.code || !sink.method) return true;
|
|
25500
|
-
|
|
25943
|
+
const sourceVar = f.path && f.path.length > 0 ? f.path[0].variable : null;
|
|
25944
|
+
const isHttpSource = typeof f.source_type === "string" && f.source_type.startsWith("http_");
|
|
25945
|
+
const sourceVarForFilter = isHttpSource ? sourceVar : null;
|
|
25946
|
+
return !isMongoValueBoundFilter(sink.code, sink.method, sourceVarForFilter);
|
|
25501
25947
|
});
|
|
25502
25948
|
}
|
|
25503
25949
|
}
|
|
@@ -25532,7 +25978,7 @@ var TaintPropagationPass = class {
|
|
|
25532
25978
|
return { flows: finalFlows };
|
|
25533
25979
|
}
|
|
25534
25980
|
};
|
|
25535
|
-
function isMongoValueBoundFilter(sinkCode, sinkMethod) {
|
|
25981
|
+
function isMongoValueBoundFilter(sinkCode, sinkMethod, sourceVar) {
|
|
25536
25982
|
if (!sinkCode || !sinkMethod) return false;
|
|
25537
25983
|
const callIdx = sinkCode.indexOf(`${sinkMethod}(`);
|
|
25538
25984
|
if (callIdx < 0) return false;
|
|
@@ -25584,6 +26030,11 @@ function isMongoValueBoundFilter(sinkCode, sinkMethod) {
|
|
|
25584
26030
|
if (/\.\.\./.test(stripped)) return false;
|
|
25585
26031
|
if (/(^|[,{\s])\$[A-Za-z_]\w*\s*:/.test(stripped)) return false;
|
|
25586
26032
|
if (/(['"])\$[A-Za-z_]\w*\1\s*:/.test(body2)) return false;
|
|
26033
|
+
if (sourceVar && /^[A-Za-z_]\w*$/.test(sourceVar)) {
|
|
26034
|
+
const escaped = sourceVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26035
|
+
const tokenRe = new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u");
|
|
26036
|
+
if (tokenRe.test(stripped)) return false;
|
|
26037
|
+
}
|
|
25587
26038
|
return true;
|
|
25588
26039
|
}
|
|
25589
26040
|
function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
|
|
@@ -27171,7 +27622,7 @@ var NullDerefPass = class {
|
|
|
27171
27622
|
};
|
|
27172
27623
|
|
|
27173
27624
|
// src/analysis/passes/resource-leak-pass.ts
|
|
27174
|
-
function
|
|
27625
|
+
function escapeRegex2(s) {
|
|
27175
27626
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27176
27627
|
}
|
|
27177
27628
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -27334,7 +27785,7 @@ var ResourceLeakPass = class {
|
|
|
27334
27785
|
* ownership to the caller (caller is responsible for close).
|
|
27335
27786
|
*/
|
|
27336
27787
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
27337
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
27788
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex2(variable)}\\b`);
|
|
27338
27789
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
27339
27790
|
if (varRe.test(lines[l - 1] ?? "")) return true;
|
|
27340
27791
|
}
|
|
@@ -27347,7 +27798,7 @@ var ResourceLeakPass = class {
|
|
|
27347
27798
|
*/
|
|
27348
27799
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
27349
27800
|
const thisAssignRe = new RegExp(
|
|
27350
|
-
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
27801
|
+
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`
|
|
27351
27802
|
);
|
|
27352
27803
|
for (let l = openLine; l <= toLine && l <= lines.length; l++) {
|
|
27353
27804
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|