circle-ir 3.194.0 → 3.197.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/README.md +39 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +43 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +624 -2
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +68 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/rules.d.ts.map +1 -1
- package/dist/analysis/rules.js +42 -0
- package/dist/analysis/rules.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +24 -8
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +582 -604
- package/dist/core/circle-ir-core.cjs +59 -13
- package/dist/core/circle-ir-core.js +59 -13
- package/dist/languages/plugins/bash.d.ts.map +1 -1
- package/dist/languages/plugins/bash.js +12 -16
- package/dist/languages/plugins/bash.js.map +1 -1
- package/dist/languages/plugins/go.d.ts +4 -0
- package/dist/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +16 -32
- package/dist/languages/plugins/go.js.map +1 -1
- package/dist/languages/plugins/java.d.ts +9 -1
- package/dist/languages/plugins/java.d.ts.map +1 -1
- package/dist/languages/plugins/java.js +16 -127
- package/dist/languages/plugins/java.js.map +1 -1
- package/dist/languages/plugins/javascript.d.ts +7 -0
- package/dist/languages/plugins/javascript.d.ts.map +1 -1
- package/dist/languages/plugins/javascript.js +10 -117
- package/dist/languages/plugins/javascript.js.map +1 -1
- package/dist/languages/plugins/python.d.ts +7 -0
- package/dist/languages/plugins/python.d.ts.map +1 -1
- package/dist/languages/plugins/python.js +24 -177
- package/dist/languages/plugins/python.js.map +1 -1
- package/dist/languages/plugins/rust.d.ts +9 -0
- package/dist/languages/plugins/rust.d.ts.map +1 -1
- package/dist/languages/plugins/rust.js +22 -110
- package/dist/languages/plugins/rust.js.map +1 -1
- package/dist/languages/types.d.ts +3 -3
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/types/config.d.ts +15 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +13 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +37 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -2
|
@@ -11360,7 +11360,11 @@ var DEFAULT_SOURCES = [
|
|
|
11360
11360
|
{ method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11361
11361
|
{ method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11362
11362
|
{ method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11363
|
-
|
|
11363
|
+
// `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
|
|
11364
|
+
// query-string extractor), so it is http_body — retyped from http_param
|
|
11365
|
+
// when the RustPlugin's http_body duplicate was removed, so the two
|
|
11366
|
+
// surfaces no longer disagree. Matches Gin's `Context.PostForm`.
|
|
11367
|
+
{ method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11364
11368
|
// Rust std library
|
|
11365
11369
|
{ method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
11366
11370
|
{ method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
@@ -12638,7 +12642,11 @@ var DEFAULT_SINKS = [
|
|
|
12638
12642
|
// Also match without receiver (destructured imports: const { exec } = require('child_process'))
|
|
12639
12643
|
// `exec` is intentionally classless: catches Node.js child_process.exec AND
|
|
12640
12644
|
// Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
|
|
12641
|
-
|
|
12645
|
+
// Excluded from Python: there, bare `exec(code)` is the code-execution
|
|
12646
|
+
// builtin, registered below as code_injection / CWE-94. Before the exclusion
|
|
12647
|
+
// a Python `exec(user_code)` matched both patterns and emitted two findings
|
|
12648
|
+
// for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
|
|
12649
|
+
{ method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
|
|
12642
12650
|
// `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
|
|
12643
12651
|
{ method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12644
12652
|
{ method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
@@ -12666,7 +12674,11 @@ var DEFAULT_SINKS = [
|
|
|
12666
12674
|
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
12667
12675
|
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
12668
12676
|
// Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
|
|
12669
|
-
|
|
12677
|
+
// Severity `critical` (not `high`) preserves what the JS plugin's duplicate
|
|
12678
|
+
// builtin emitted before the #4 follow-up consolidation removed it: the
|
|
12679
|
+
// plugin copy carried `critical`, which won the `findSinks` dedup slot via
|
|
12680
|
+
// the +0.1 critical-severity confidence boost.
|
|
12681
|
+
{ method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12670
12682
|
// sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
|
|
12671
12683
|
// `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
|
|
12672
12684
|
// target `Connection.all`, so class-scoped patterns matching `Connection`
|
|
@@ -13225,10 +13237,30 @@ var DEFAULT_SINKS = [
|
|
|
13225
13237
|
{ method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13226
13238
|
{ method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13227
13239
|
{ method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13240
|
+
// `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
|
|
13241
|
+
// Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
|
|
13242
|
+
// string emits two findings. Unlike the logger case above, neither is
|
|
13243
|
+
// redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
|
|
13244
|
+
// finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
|
|
13245
|
+
// is the standard Go reflected-XSS shape). Collapsing it requires deciding
|
|
13246
|
+
// by the *type of arg[0]*, which sink patterns cannot express today: the
|
|
13247
|
+
// options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
|
|
13248
|
+
// emits xss only for a resolved ResponseWriter, or dropping one CWE and
|
|
13249
|
+
// accepting either lost XSS recall or a mis-classified stderr write.
|
|
13250
|
+
// Left as-is pending the Go corpus run — see tasks.md.
|
|
13228
13251
|
{ method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
13229
13252
|
// cognium-dev #264 — Go stdlib `log` package format-string entry points.
|
|
13230
13253
|
// log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
|
|
13231
13254
|
// format string reaches the same fmt.Sprintf machinery internally.
|
|
13255
|
+
//
|
|
13256
|
+
// These deliberately coexist with the Go plugin's log_injection (CWE-117)
|
|
13257
|
+
// entries for the same methods, so such a call emits both. The Logger
|
|
13258
|
+
// decision note in the Java log_injection section above does NOT apply
|
|
13259
|
+
// here: it reasons about SLF4J-style APIs where the format string is a
|
|
13260
|
+
// literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
|
|
13261
|
+
// a real format string that an attacker can control. CWE-117 covers the
|
|
13262
|
+
// forged log line, CWE-134 the format string itself. Locked by
|
|
13263
|
+
// tests/analysis/passes/format-string-additions.test.ts.
|
|
13232
13264
|
{ method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13233
13265
|
{ method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13234
13266
|
{ method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
@@ -13242,6 +13274,14 @@ var DEFAULT_SINKS = [
|
|
|
13242
13274
|
{ method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
13243
13275
|
// Express: res.location(url) and res.redirect(url) — Location header.
|
|
13244
13276
|
{ method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
13277
|
+
// `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
|
|
13278
|
+
// coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
|
|
13279
|
+
// redirect target emits two flows. That is intentional, not duplication —
|
|
13280
|
+
// #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
|
|
13281
|
+
// survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
|
|
13282
|
+
// crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
|
|
13283
|
+
// by issue-189-sprint82-open-redirect-cluster.test.ts and
|
|
13284
|
+
// crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
|
|
13245
13285
|
{ method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
13246
13286
|
// Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
|
|
13247
13287
|
// (Header is a map; the actual `value` is arg 1 of the call). We flag the
|
|
@@ -14162,18 +14202,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
|
|
|
14162
14202
|
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
14163
14203
|
return { sources, sinks, sanitizers };
|
|
14164
14204
|
}
|
|
14205
|
+
function sinkPatternAppliesTo(pattern, language) {
|
|
14206
|
+
if (language === void 0) return true;
|
|
14207
|
+
if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
|
|
14208
|
+
return false;
|
|
14209
|
+
}
|
|
14210
|
+
if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
|
|
14211
|
+
return false;
|
|
14212
|
+
}
|
|
14213
|
+
return true;
|
|
14214
|
+
}
|
|
14165
14215
|
function expandPromisifyAliases(patterns, sourceLines, language) {
|
|
14166
14216
|
if (!sourceLines || sourceLines.length === 0) return patterns;
|
|
14167
14217
|
if (language !== "javascript" && language !== "typescript") return patterns;
|
|
14168
14218
|
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*\)/;
|
|
14169
14219
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
14170
14220
|
for (const p of patterns) {
|
|
14171
|
-
if (p
|
|
14221
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14172
14222
|
if (p.class !== void 0) continue;
|
|
14173
14223
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
14174
14224
|
}
|
|
14175
14225
|
for (const p of patterns) {
|
|
14176
|
-
if (p
|
|
14226
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14177
14227
|
if (innerToPattern.has(p.method)) continue;
|
|
14178
14228
|
innerToPattern.set(p.method, p);
|
|
14179
14229
|
}
|
|
@@ -14207,7 +14257,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
|
|
|
14207
14257
|
const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
|
|
14208
14258
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
14209
14259
|
for (const p of patterns) {
|
|
14210
|
-
if (p
|
|
14260
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14211
14261
|
if (p.class !== void 0) continue;
|
|
14212
14262
|
if (p.method !== "eval" && p.method !== "Function") continue;
|
|
14213
14263
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
@@ -14915,9 +14965,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14915
14965
|
return false;
|
|
14916
14966
|
}
|
|
14917
14967
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14918
|
-
const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
|
|
14919
|
-
(p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
|
|
14920
|
-
);
|
|
14968
|
+
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14921
14969
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
14922
14970
|
for (const call of calls) {
|
|
14923
14971
|
for (const pattern of patternsForLanguage) {
|
|
@@ -15262,10 +15310,8 @@ var SINK_FQN_EXCLUSIONS = {
|
|
|
15262
15310
|
]
|
|
15263
15311
|
};
|
|
15264
15312
|
function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
15265
|
-
if (pattern
|
|
15266
|
-
|
|
15267
|
-
return false;
|
|
15268
|
-
}
|
|
15313
|
+
if (!sinkPatternAppliesTo(pattern, language)) {
|
|
15314
|
+
return false;
|
|
15269
15315
|
}
|
|
15270
15316
|
const callMethodName = call.method_name;
|
|
15271
15317
|
const patternMethod = pattern.method;
|
|
@@ -22146,107 +22192,30 @@ var JavaPlugin = class extends BaseLanguagePlugin {
|
|
|
22146
22192
|
severity: "high",
|
|
22147
22193
|
confidence: 0.9,
|
|
22148
22194
|
returnTainted: true
|
|
22149
|
-
},
|
|
22150
|
-
// Servlet API
|
|
22151
|
-
{
|
|
22152
|
-
method: "getParameter",
|
|
22153
|
-
class: "HttpServletRequest",
|
|
22154
|
-
type: "http_param",
|
|
22155
|
-
severity: "high",
|
|
22156
|
-
confidence: 0.95,
|
|
22157
|
-
returnTainted: true
|
|
22158
|
-
},
|
|
22159
|
-
{
|
|
22160
|
-
method: "getParameterValues",
|
|
22161
|
-
class: "HttpServletRequest",
|
|
22162
|
-
type: "http_param",
|
|
22163
|
-
severity: "high",
|
|
22164
|
-
confidence: 0.95,
|
|
22165
|
-
returnTainted: true
|
|
22166
|
-
},
|
|
22167
|
-
{
|
|
22168
|
-
method: "getHeader",
|
|
22169
|
-
class: "HttpServletRequest",
|
|
22170
|
-
type: "http_header",
|
|
22171
|
-
severity: "high",
|
|
22172
|
-
confidence: 0.9,
|
|
22173
|
-
returnTainted: true
|
|
22174
|
-
},
|
|
22175
|
-
{
|
|
22176
|
-
method: "getCookies",
|
|
22177
|
-
class: "HttpServletRequest",
|
|
22178
|
-
type: "http_cookie",
|
|
22179
|
-
severity: "high",
|
|
22180
|
-
confidence: 0.9,
|
|
22181
|
-
returnTainted: true
|
|
22182
|
-
},
|
|
22183
|
-
{
|
|
22184
|
-
method: "getInputStream",
|
|
22185
|
-
class: "HttpServletRequest",
|
|
22186
|
-
type: "http_body",
|
|
22187
|
-
severity: "high",
|
|
22188
|
-
confidence: 0.85,
|
|
22189
|
-
returnTainted: true
|
|
22190
|
-
},
|
|
22191
|
-
{
|
|
22192
|
-
method: "getReader",
|
|
22193
|
-
class: "HttpServletRequest",
|
|
22194
|
-
type: "http_body",
|
|
22195
|
-
severity: "high",
|
|
22196
|
-
confidence: 0.85,
|
|
22197
|
-
returnTainted: true
|
|
22198
22195
|
}
|
|
22196
|
+
// NOTE: the Servlet API call sources (`HttpServletRequest.getParameter`,
|
|
22197
|
+
// `getParameterValues`, `getHeader`, `getCookies`, `getInputStream`,
|
|
22198
|
+
// `getReader`) are registered identically in DEFAULT_SOURCES. Because
|
|
22199
|
+
// `findSources` pushes one TaintSource per matching pattern with no
|
|
22200
|
+
// dedup, the duplicates emitted a second, byte-identical source at every
|
|
22201
|
+
// call site. Removed in the #4 follow-up consolidation — DEFAULT_SOURCES
|
|
22202
|
+
// is the canonical registry (ADR-004).
|
|
22199
22203
|
];
|
|
22200
22204
|
}
|
|
22201
22205
|
/**
|
|
22202
22206
|
* Java-specific taint sink patterns.
|
|
22203
|
-
*
|
|
22207
|
+
*
|
|
22208
|
+
* Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
|
|
22209
|
+
* `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
|
|
22210
|
+
* (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
|
|
22211
|
+
* plugin builtins *after* the config patterns, and `findSinks` dedupes by
|
|
22212
|
+
* `location:line:cwe` keeping the higher-confidence match, so the config
|
|
22213
|
+
* copy always won the slot. Removed in the #4 follow-up consolidation
|
|
22214
|
+
* (`Statement.execute*`, `Runtime.exec`, `ProcessBuilder.start`,
|
|
22215
|
+
* `PrintWriter.write/println`, `DirContext.search`, `XPath.evaluate`).
|
|
22204
22216
|
*/
|
|
22205
22217
|
getBuiltinSinks() {
|
|
22206
22218
|
return [
|
|
22207
|
-
// SQL Injection
|
|
22208
|
-
{
|
|
22209
|
-
method: "executeQuery",
|
|
22210
|
-
class: "Statement",
|
|
22211
|
-
type: "sql_injection",
|
|
22212
|
-
cwe: "CWE-89",
|
|
22213
|
-
severity: "critical",
|
|
22214
|
-
argPositions: [0]
|
|
22215
|
-
},
|
|
22216
|
-
{
|
|
22217
|
-
method: "executeUpdate",
|
|
22218
|
-
class: "Statement",
|
|
22219
|
-
type: "sql_injection",
|
|
22220
|
-
cwe: "CWE-89",
|
|
22221
|
-
severity: "critical",
|
|
22222
|
-
argPositions: [0]
|
|
22223
|
-
},
|
|
22224
|
-
{
|
|
22225
|
-
method: "execute",
|
|
22226
|
-
class: "Statement",
|
|
22227
|
-
type: "sql_injection",
|
|
22228
|
-
cwe: "CWE-89",
|
|
22229
|
-
severity: "critical",
|
|
22230
|
-
argPositions: [0]
|
|
22231
|
-
},
|
|
22232
|
-
// Command Injection
|
|
22233
|
-
{
|
|
22234
|
-
method: "exec",
|
|
22235
|
-
class: "Runtime",
|
|
22236
|
-
type: "command_injection",
|
|
22237
|
-
cwe: "CWE-78",
|
|
22238
|
-
severity: "critical",
|
|
22239
|
-
argPositions: [0]
|
|
22240
|
-
},
|
|
22241
|
-
{
|
|
22242
|
-
method: "start",
|
|
22243
|
-
class: "ProcessBuilder",
|
|
22244
|
-
type: "command_injection",
|
|
22245
|
-
cwe: "CWE-78",
|
|
22246
|
-
severity: "critical",
|
|
22247
|
-
argPositions: []
|
|
22248
|
-
// Constructor args are dangerous
|
|
22249
|
-
},
|
|
22250
22219
|
// Path Traversal
|
|
22251
22220
|
{
|
|
22252
22221
|
method: "FileInputStream",
|
|
@@ -22262,41 +22231,6 @@ var JavaPlugin = class extends BaseLanguagePlugin {
|
|
|
22262
22231
|
severity: "high",
|
|
22263
22232
|
argPositions: [0]
|
|
22264
22233
|
},
|
|
22265
|
-
// XSS
|
|
22266
|
-
{
|
|
22267
|
-
method: "write",
|
|
22268
|
-
class: "PrintWriter",
|
|
22269
|
-
type: "xss",
|
|
22270
|
-
cwe: "CWE-79",
|
|
22271
|
-
severity: "high",
|
|
22272
|
-
argPositions: [0]
|
|
22273
|
-
},
|
|
22274
|
-
{
|
|
22275
|
-
method: "println",
|
|
22276
|
-
class: "PrintWriter",
|
|
22277
|
-
type: "xss",
|
|
22278
|
-
cwe: "CWE-79",
|
|
22279
|
-
severity: "high",
|
|
22280
|
-
argPositions: [0]
|
|
22281
|
-
},
|
|
22282
|
-
// LDAP Injection
|
|
22283
|
-
{
|
|
22284
|
-
method: "search",
|
|
22285
|
-
class: "DirContext",
|
|
22286
|
-
type: "ldap_injection",
|
|
22287
|
-
cwe: "CWE-90",
|
|
22288
|
-
severity: "high",
|
|
22289
|
-
argPositions: [0, 1]
|
|
22290
|
-
},
|
|
22291
|
-
// XPath Injection
|
|
22292
|
-
{
|
|
22293
|
-
method: "evaluate",
|
|
22294
|
-
class: "XPath",
|
|
22295
|
-
type: "xpath_injection",
|
|
22296
|
-
cwe: "CWE-643",
|
|
22297
|
-
severity: "high",
|
|
22298
|
-
argPositions: [0]
|
|
22299
|
-
},
|
|
22300
22234
|
// Deserialization
|
|
22301
22235
|
{
|
|
22302
22236
|
method: "readObject",
|
|
@@ -22855,42 +22789,17 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22855
22789
|
}
|
|
22856
22790
|
/**
|
|
22857
22791
|
* JavaScript/TypeScript taint sink patterns.
|
|
22792
|
+
*
|
|
22793
|
+
* Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
|
|
22794
|
+
* `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
|
|
22795
|
+
* (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
|
|
22796
|
+
* plugin builtins *after* the config patterns, and `findSinks` dedupes by
|
|
22797
|
+
* `location:line:cwe` keeping the higher-confidence match, so the config
|
|
22798
|
+
* copy always won the slot. Removed in the #4 follow-up consolidation.
|
|
22858
22799
|
*/
|
|
22859
22800
|
getBuiltinSinks() {
|
|
22860
22801
|
return [
|
|
22861
|
-
// Command Injection
|
|
22862
|
-
{
|
|
22863
|
-
method: "exec",
|
|
22864
|
-
class: "child_process",
|
|
22865
|
-
type: "command_injection",
|
|
22866
|
-
cwe: "CWE-78",
|
|
22867
|
-
severity: "critical",
|
|
22868
|
-
argPositions: [0]
|
|
22869
|
-
},
|
|
22870
|
-
{
|
|
22871
|
-
method: "execSync",
|
|
22872
|
-
class: "child_process",
|
|
22873
|
-
type: "command_injection",
|
|
22874
|
-
cwe: "CWE-78",
|
|
22875
|
-
severity: "critical",
|
|
22876
|
-
argPositions: [0]
|
|
22877
|
-
},
|
|
22878
|
-
{
|
|
22879
|
-
method: "spawn",
|
|
22880
|
-
class: "child_process",
|
|
22881
|
-
type: "command_injection",
|
|
22882
|
-
cwe: "CWE-78",
|
|
22883
|
-
severity: "critical",
|
|
22884
|
-
argPositions: [0, 1]
|
|
22885
|
-
},
|
|
22886
22802
|
// Code Injection
|
|
22887
|
-
{
|
|
22888
|
-
method: "eval",
|
|
22889
|
-
type: "code_injection",
|
|
22890
|
-
cwe: "CWE-94",
|
|
22891
|
-
severity: "critical",
|
|
22892
|
-
argPositions: [0]
|
|
22893
|
-
},
|
|
22894
22803
|
{
|
|
22895
22804
|
method: "Function",
|
|
22896
22805
|
type: "code_injection",
|
|
@@ -22914,46 +22823,7 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22914
22823
|
argPositions: [0]
|
|
22915
22824
|
// When first arg is string
|
|
22916
22825
|
},
|
|
22917
|
-
// Path Traversal
|
|
22918
|
-
{
|
|
22919
|
-
method: "readFileSync",
|
|
22920
|
-
class: "fs",
|
|
22921
|
-
type: "path_traversal",
|
|
22922
|
-
cwe: "CWE-22",
|
|
22923
|
-
severity: "high",
|
|
22924
|
-
argPositions: [0]
|
|
22925
|
-
},
|
|
22926
|
-
{
|
|
22927
|
-
method: "writeFileSync",
|
|
22928
|
-
class: "fs",
|
|
22929
|
-
type: "path_traversal",
|
|
22930
|
-
cwe: "CWE-22",
|
|
22931
|
-
severity: "high",
|
|
22932
|
-
argPositions: [0]
|
|
22933
|
-
},
|
|
22934
|
-
{
|
|
22935
|
-
method: "createReadStream",
|
|
22936
|
-
class: "fs",
|
|
22937
|
-
type: "path_traversal",
|
|
22938
|
-
cwe: "CWE-22",
|
|
22939
|
-
severity: "high",
|
|
22940
|
-
argPositions: [0]
|
|
22941
|
-
},
|
|
22942
22826
|
// XSS (DOM)
|
|
22943
|
-
{
|
|
22944
|
-
method: "innerHTML",
|
|
22945
|
-
type: "xss",
|
|
22946
|
-
cwe: "CWE-79",
|
|
22947
|
-
severity: "high",
|
|
22948
|
-
argPositions: [0]
|
|
22949
|
-
},
|
|
22950
|
-
{
|
|
22951
|
-
method: "outerHTML",
|
|
22952
|
-
type: "xss",
|
|
22953
|
-
cwe: "CWE-79",
|
|
22954
|
-
severity: "high",
|
|
22955
|
-
argPositions: [0]
|
|
22956
|
-
},
|
|
22957
22827
|
{
|
|
22958
22828
|
method: "document.write",
|
|
22959
22829
|
type: "xss",
|
|
@@ -23005,13 +22875,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23005
22875
|
severity: "critical",
|
|
23006
22876
|
argPositions: [0]
|
|
23007
22877
|
},
|
|
23008
|
-
{
|
|
23009
|
-
method: "raw",
|
|
23010
|
-
type: "sql_injection",
|
|
23011
|
-
cwe: "CWE-89",
|
|
23012
|
-
severity: "critical",
|
|
23013
|
-
argPositions: [0]
|
|
23014
|
-
},
|
|
23015
22878
|
// Prisma ORM - unsafe raw query methods ($executeRaw/$queryRaw with template literals are safe/parameterized)
|
|
23016
22879
|
{
|
|
23017
22880
|
method: "$executeRawUnsafe",
|
|
@@ -23027,30 +22890,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23027
22890
|
severity: "critical",
|
|
23028
22891
|
argPositions: [0]
|
|
23029
22892
|
},
|
|
23030
|
-
// SSRF
|
|
23031
|
-
{
|
|
23032
|
-
method: "fetch",
|
|
23033
|
-
type: "ssrf",
|
|
23034
|
-
cwe: "CWE-918",
|
|
23035
|
-
severity: "high",
|
|
23036
|
-
argPositions: [0]
|
|
23037
|
-
},
|
|
23038
|
-
{
|
|
23039
|
-
method: "get",
|
|
23040
|
-
class: "axios",
|
|
23041
|
-
type: "ssrf",
|
|
23042
|
-
cwe: "CWE-918",
|
|
23043
|
-
severity: "high",
|
|
23044
|
-
argPositions: [0]
|
|
23045
|
-
},
|
|
23046
|
-
{
|
|
23047
|
-
method: "request",
|
|
23048
|
-
class: "http",
|
|
23049
|
-
type: "ssrf",
|
|
23050
|
-
cwe: "CWE-918",
|
|
23051
|
-
severity: "high",
|
|
23052
|
-
argPositions: [0]
|
|
23053
|
-
},
|
|
23054
22893
|
// NoSQL Injection
|
|
23055
22894
|
{
|
|
23056
22895
|
method: "find",
|
|
@@ -23059,13 +22898,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23059
22898
|
severity: "high",
|
|
23060
22899
|
argPositions: [0]
|
|
23061
22900
|
},
|
|
23062
|
-
{
|
|
23063
|
-
method: "findOne",
|
|
23064
|
-
type: "nosql_injection",
|
|
23065
|
-
cwe: "CWE-943",
|
|
23066
|
-
severity: "high",
|
|
23067
|
-
argPositions: [0]
|
|
23068
|
-
},
|
|
23069
22901
|
// Prototype Pollution (JS-specific)
|
|
23070
22902
|
{
|
|
23071
22903
|
method: "merge",
|
|
@@ -23159,14 +22991,9 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23159
22991
|
// =========================================================
|
|
23160
22992
|
// Next.js Sinks
|
|
23161
22993
|
// =========================================================
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
type: "open_redirect",
|
|
23166
|
-
cwe: "CWE-601",
|
|
23167
|
-
severity: "high",
|
|
23168
|
-
argPositions: [0]
|
|
23169
|
-
},
|
|
22994
|
+
// NOTE: classless `redirect` (CWE-601) lives in DEFAULT_SINKS, scoped to
|
|
22995
|
+
// javascript+typescript — the duplicate builtin here never won the dedup
|
|
22996
|
+
// slot (equal confidence, config pattern iterates first).
|
|
23170
22997
|
{
|
|
23171
22998
|
// Router push with user-controlled URL
|
|
23172
22999
|
method: "push",
|
|
@@ -23448,13 +23275,15 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23448
23275
|
returnTainted: true
|
|
23449
23276
|
},
|
|
23450
23277
|
// Standard library sources
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23278
|
+
//
|
|
23279
|
+
// `input()` and `os.getenv()` are registered in DEFAULT_SOURCES as
|
|
23280
|
+
// `io_input` / `env_input`. The duplicates that used to sit here typed
|
|
23281
|
+
// them `user_input` / `env_var` — strings outside the published
|
|
23282
|
+
// `SourceType` union, which reach consumers only because the merge in
|
|
23283
|
+
// TaintMatcherPass casts `TaintSourcePattern.type` (a bare `string`) to
|
|
23284
|
+
// `SourceType`. Both entries fired, so one call emitted two sources with
|
|
23285
|
+
// two different type strings. Removed in favour of the canonical
|
|
23286
|
+
// registry (ADR-004).
|
|
23458
23287
|
{
|
|
23459
23288
|
method: "argv",
|
|
23460
23289
|
class: "sys",
|
|
@@ -23470,153 +23299,37 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23470
23299
|
severity: "medium",
|
|
23471
23300
|
confidence: 0.85,
|
|
23472
23301
|
returnTainted: true
|
|
23473
|
-
},
|
|
23474
|
-
{
|
|
23475
|
-
method: "getenv",
|
|
23476
|
-
class: "os",
|
|
23477
|
-
type: "env_var",
|
|
23478
|
-
severity: "medium",
|
|
23479
|
-
confidence: 0.85,
|
|
23480
|
-
returnTainted: true
|
|
23481
|
-
},
|
|
23482
|
-
// File reading
|
|
23483
|
-
{
|
|
23484
|
-
method: "read",
|
|
23485
|
-
type: "file_input",
|
|
23486
|
-
severity: "medium",
|
|
23487
|
-
confidence: 0.8,
|
|
23488
|
-
returnTainted: true
|
|
23489
|
-
},
|
|
23490
|
-
{
|
|
23491
|
-
method: "readline",
|
|
23492
|
-
type: "file_input",
|
|
23493
|
-
severity: "medium",
|
|
23494
|
-
confidence: 0.8,
|
|
23495
|
-
returnTainted: true
|
|
23496
|
-
},
|
|
23497
|
-
{
|
|
23498
|
-
method: "readlines",
|
|
23499
|
-
type: "file_input",
|
|
23500
|
-
severity: "medium",
|
|
23501
|
-
confidence: 0.8,
|
|
23502
|
-
returnTainted: true
|
|
23503
23302
|
}
|
|
23303
|
+
// NOTE: the file-reading sources (`read`, `readline`, `readlines` as
|
|
23304
|
+
// file_input) are registered identically in DEFAULT_SOURCES. Since
|
|
23305
|
+
// `findSources` pushes one TaintSource per matching pattern with no
|
|
23306
|
+
// dedup, these duplicates emitted a second, byte-identical source at
|
|
23307
|
+
// every call site. Removed in the #4 follow-up consolidation —
|
|
23308
|
+
// DEFAULT_SOURCES is the canonical registry (ADR-004).
|
|
23504
23309
|
];
|
|
23505
23310
|
}
|
|
23506
23311
|
/**
|
|
23507
23312
|
* Python taint sink patterns.
|
|
23313
|
+
*
|
|
23314
|
+
* Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
|
|
23315
|
+
* `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
|
|
23316
|
+
* (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
|
|
23317
|
+
* plugin builtins *after* the config patterns, and `findSinks` dedupes by
|
|
23318
|
+
* `location:line:cwe` keeping the higher-confidence match, so the config
|
|
23319
|
+
* copy always won the slot. Removed in the #4 follow-up consolidation.
|
|
23508
23320
|
*/
|
|
23509
23321
|
getBuiltinSinks() {
|
|
23510
23322
|
return [
|
|
23511
|
-
//
|
|
23323
|
+
// Path Traversal
|
|
23512
23324
|
{
|
|
23513
|
-
method: "
|
|
23514
|
-
class: "
|
|
23515
|
-
type: "
|
|
23516
|
-
cwe: "CWE-
|
|
23517
|
-
severity: "
|
|
23518
|
-
argPositions: [0]
|
|
23519
|
-
},
|
|
23520
|
-
{
|
|
23521
|
-
method: "popen",
|
|
23522
|
-
class: "os",
|
|
23523
|
-
type: "command_injection",
|
|
23524
|
-
cwe: "CWE-78",
|
|
23525
|
-
severity: "critical",
|
|
23526
|
-
argPositions: [0]
|
|
23527
|
-
},
|
|
23528
|
-
{
|
|
23529
|
-
method: "run",
|
|
23530
|
-
class: "subprocess",
|
|
23531
|
-
type: "command_injection",
|
|
23532
|
-
cwe: "CWE-78",
|
|
23533
|
-
severity: "critical",
|
|
23534
|
-
argPositions: [0]
|
|
23535
|
-
},
|
|
23536
|
-
{
|
|
23537
|
-
method: "call",
|
|
23538
|
-
class: "subprocess",
|
|
23539
|
-
type: "command_injection",
|
|
23540
|
-
cwe: "CWE-78",
|
|
23541
|
-
severity: "critical",
|
|
23542
|
-
argPositions: [0]
|
|
23543
|
-
},
|
|
23544
|
-
{
|
|
23545
|
-
method: "Popen",
|
|
23546
|
-
class: "subprocess",
|
|
23547
|
-
type: "command_injection",
|
|
23548
|
-
cwe: "CWE-78",
|
|
23549
|
-
severity: "critical",
|
|
23550
|
-
argPositions: [0]
|
|
23551
|
-
},
|
|
23552
|
-
// Code Injection
|
|
23553
|
-
{
|
|
23554
|
-
method: "eval",
|
|
23555
|
-
type: "code_injection",
|
|
23556
|
-
cwe: "CWE-94",
|
|
23557
|
-
severity: "critical",
|
|
23558
|
-
argPositions: [0]
|
|
23559
|
-
},
|
|
23560
|
-
{
|
|
23561
|
-
method: "exec",
|
|
23562
|
-
type: "code_injection",
|
|
23563
|
-
cwe: "CWE-94",
|
|
23564
|
-
severity: "critical",
|
|
23565
|
-
argPositions: [0]
|
|
23566
|
-
},
|
|
23567
|
-
{
|
|
23568
|
-
method: "compile",
|
|
23569
|
-
type: "code_injection",
|
|
23570
|
-
cwe: "CWE-94",
|
|
23571
|
-
severity: "high",
|
|
23572
|
-
argPositions: [0]
|
|
23573
|
-
},
|
|
23574
|
-
// SQL Injection
|
|
23575
|
-
{
|
|
23576
|
-
method: "execute",
|
|
23577
|
-
type: "sql_injection",
|
|
23578
|
-
cwe: "CWE-89",
|
|
23579
|
-
severity: "critical",
|
|
23580
|
-
argPositions: [0]
|
|
23581
|
-
},
|
|
23582
|
-
{
|
|
23583
|
-
method: "executemany",
|
|
23584
|
-
type: "sql_injection",
|
|
23585
|
-
cwe: "CWE-89",
|
|
23586
|
-
severity: "critical",
|
|
23587
|
-
argPositions: [0]
|
|
23588
|
-
},
|
|
23589
|
-
{
|
|
23590
|
-
method: "raw",
|
|
23591
|
-
type: "sql_injection",
|
|
23592
|
-
cwe: "CWE-89",
|
|
23593
|
-
severity: "critical",
|
|
23594
|
-
argPositions: [0]
|
|
23595
|
-
},
|
|
23596
|
-
// Path Traversal
|
|
23597
|
-
{
|
|
23598
|
-
method: "open",
|
|
23599
|
-
type: "path_traversal",
|
|
23600
|
-
cwe: "CWE-22",
|
|
23601
|
-
severity: "high",
|
|
23602
|
-
argPositions: [0]
|
|
23603
|
-
},
|
|
23604
|
-
{
|
|
23605
|
-
method: "read",
|
|
23606
|
-
class: "pathlib",
|
|
23607
|
-
type: "path_traversal",
|
|
23608
|
-
cwe: "CWE-22",
|
|
23609
|
-
severity: "high",
|
|
23610
|
-
argPositions: [0]
|
|
23611
|
-
},
|
|
23612
|
-
// XSS (template injection)
|
|
23613
|
-
{
|
|
23614
|
-
method: "Markup",
|
|
23615
|
-
type: "xss",
|
|
23616
|
-
cwe: "CWE-79",
|
|
23617
|
-
severity: "high",
|
|
23325
|
+
method: "read",
|
|
23326
|
+
class: "pathlib",
|
|
23327
|
+
type: "path_traversal",
|
|
23328
|
+
cwe: "CWE-22",
|
|
23329
|
+
severity: "high",
|
|
23618
23330
|
argPositions: [0]
|
|
23619
23331
|
},
|
|
23332
|
+
// XSS (template injection)
|
|
23620
23333
|
{
|
|
23621
23334
|
method: "safe",
|
|
23622
23335
|
type: "xss",
|
|
@@ -23625,22 +23338,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23625
23338
|
argPositions: [0]
|
|
23626
23339
|
},
|
|
23627
23340
|
// SSRF
|
|
23628
|
-
{
|
|
23629
|
-
method: "get",
|
|
23630
|
-
class: "requests",
|
|
23631
|
-
type: "ssrf",
|
|
23632
|
-
cwe: "CWE-918",
|
|
23633
|
-
severity: "high",
|
|
23634
|
-
argPositions: [0]
|
|
23635
|
-
},
|
|
23636
|
-
{
|
|
23637
|
-
method: "post",
|
|
23638
|
-
class: "requests",
|
|
23639
|
-
type: "ssrf",
|
|
23640
|
-
cwe: "CWE-918",
|
|
23641
|
-
severity: "high",
|
|
23642
|
-
argPositions: [0]
|
|
23643
|
-
},
|
|
23644
23341
|
{
|
|
23645
23342
|
method: "urlopen",
|
|
23646
23343
|
class: "urllib",
|
|
@@ -23668,34 +23365,11 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23668
23365
|
argPositions: [1]
|
|
23669
23366
|
},
|
|
23670
23367
|
// Deserialization
|
|
23671
|
-
{
|
|
23672
|
-
method: "loads",
|
|
23673
|
-
class: "pickle",
|
|
23674
|
-
type: "deserialization",
|
|
23675
|
-
cwe: "CWE-502",
|
|
23676
|
-
severity: "critical",
|
|
23677
|
-
argPositions: [0]
|
|
23678
|
-
},
|
|
23679
|
-
{
|
|
23680
|
-
method: "load",
|
|
23681
|
-
class: "pickle",
|
|
23682
|
-
type: "deserialization",
|
|
23683
|
-
cwe: "CWE-502",
|
|
23684
|
-
severity: "critical",
|
|
23685
|
-
argPositions: [0]
|
|
23686
|
-
},
|
|
23687
23368
|
// NOTE: yaml.safe_load / yaml.SafeLoader are intentionally NOT sinks.
|
|
23688
23369
|
// safe_load constructs only standard scalar/list/dict types and cannot
|
|
23689
23370
|
// instantiate arbitrary Python objects, so it is not a CWE-502 sink
|
|
23690
|
-
// (unlike yaml.load
|
|
23691
|
-
|
|
23692
|
-
method: "load",
|
|
23693
|
-
class: "yaml",
|
|
23694
|
-
type: "deserialization",
|
|
23695
|
-
cwe: "CWE-502",
|
|
23696
|
-
severity: "critical",
|
|
23697
|
-
argPositions: [0]
|
|
23698
|
-
},
|
|
23371
|
+
// (unlike yaml.load — registered in DEFAULT_SINKS — and
|
|
23372
|
+
// yaml.unsafe_load / yaml.full_load below).
|
|
23699
23373
|
{
|
|
23700
23374
|
method: "unsafe_load",
|
|
23701
23375
|
class: "yaml",
|
|
@@ -23854,34 +23528,17 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23854
23528
|
getBuiltinSources() {
|
|
23855
23529
|
return [
|
|
23856
23530
|
// Actix-web request extractors
|
|
23857
|
-
|
|
23858
|
-
|
|
23859
|
-
|
|
23860
|
-
|
|
23861
|
-
|
|
23862
|
-
|
|
23863
|
-
|
|
23864
|
-
|
|
23865
|
-
|
|
23866
|
-
|
|
23867
|
-
|
|
23868
|
-
confidence: 0.95,
|
|
23869
|
-
returnTainted: true
|
|
23870
|
-
},
|
|
23871
|
-
{
|
|
23872
|
-
method: "Path",
|
|
23873
|
-
type: "http_path",
|
|
23874
|
-
severity: "high",
|
|
23875
|
-
confidence: 0.95,
|
|
23876
|
-
returnTainted: true
|
|
23877
|
-
},
|
|
23878
|
-
{
|
|
23879
|
-
method: "Form",
|
|
23880
|
-
type: "http_body",
|
|
23881
|
-
severity: "high",
|
|
23882
|
-
confidence: 0.95,
|
|
23883
|
-
returnTainted: true
|
|
23884
|
-
},
|
|
23531
|
+
// NOTE: `Query` (http_param), `Json` (http_body) and `Path` (http_path)
|
|
23532
|
+
// are registered identically in DEFAULT_SOURCES; the duplicates here
|
|
23533
|
+
// emitted a second, byte-identical source at every call site
|
|
23534
|
+
// (`findSources` has no dedup). Removed in the #4 follow-up
|
|
23535
|
+
// consolidation — DEFAULT_SOURCES is the canonical registry (ADR-004).
|
|
23536
|
+
// `Form` stays because the two surfaces disagree on its type
|
|
23537
|
+
// `Form` is registered in DEFAULT_SOURCES, retyped there to `http_body`
|
|
23538
|
+
// to match this entry (actix/axum `Form<T>` decodes the urlencoded
|
|
23539
|
+
// request body, not the query string — `Query<T>` is the query one).
|
|
23540
|
+
// The duplicate that used to sit here made every extractor emit two
|
|
23541
|
+
// sources with different types.
|
|
23885
23542
|
// Standard library sources
|
|
23886
23543
|
{
|
|
23887
23544
|
method: "args",
|
|
@@ -23931,20 +23588,22 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23931
23588
|
severity: "medium",
|
|
23932
23589
|
confidence: 0.8,
|
|
23933
23590
|
returnTainted: true
|
|
23934
|
-
},
|
|
23935
|
-
// Network input
|
|
23936
|
-
{
|
|
23937
|
-
method: "read",
|
|
23938
|
-
class: "TcpStream",
|
|
23939
|
-
type: "network_input",
|
|
23940
|
-
severity: "high",
|
|
23941
|
-
confidence: 0.85,
|
|
23942
|
-
returnTainted: true
|
|
23943
23591
|
}
|
|
23592
|
+
// NOTE: `TcpStream.read` (network_input) is registered identically in
|
|
23593
|
+
// DEFAULT_SOURCES — duplicate removed in the #4 follow-up consolidation.
|
|
23944
23594
|
];
|
|
23945
23595
|
}
|
|
23946
23596
|
/**
|
|
23947
23597
|
* Rust taint sink patterns.
|
|
23598
|
+
*
|
|
23599
|
+
* Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
|
|
23600
|
+
* `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
|
|
23601
|
+
* (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
|
|
23602
|
+
* plugin builtins *after* the config patterns, and `findSinks` dedupes by
|
|
23603
|
+
* `location:line:cwe` keeping the higher-confidence match, so the config
|
|
23604
|
+
* copy always won the slot. Removed in the #4 follow-up consolidation
|
|
23605
|
+
* (`Command.arg/args`, classless `execute`, `File.open/create`,
|
|
23606
|
+
* `serde_json.from_str/from_slice`, `reqwest.get/post`).
|
|
23948
23607
|
*/
|
|
23949
23608
|
getBuiltinSinks() {
|
|
23950
23609
|
return [
|
|
@@ -23957,30 +23616,7 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23957
23616
|
severity: "critical",
|
|
23958
23617
|
argPositions: [0]
|
|
23959
23618
|
},
|
|
23960
|
-
{
|
|
23961
|
-
method: "arg",
|
|
23962
|
-
class: "Command",
|
|
23963
|
-
type: "command_injection",
|
|
23964
|
-
cwe: "CWE-78",
|
|
23965
|
-
severity: "critical",
|
|
23966
|
-
argPositions: [0]
|
|
23967
|
-
},
|
|
23968
|
-
{
|
|
23969
|
-
method: "args",
|
|
23970
|
-
class: "Command",
|
|
23971
|
-
type: "command_injection",
|
|
23972
|
-
cwe: "CWE-78",
|
|
23973
|
-
severity: "critical",
|
|
23974
|
-
argPositions: [0]
|
|
23975
|
-
},
|
|
23976
23619
|
// SQL Injection
|
|
23977
|
-
{
|
|
23978
|
-
method: "execute",
|
|
23979
|
-
type: "sql_injection",
|
|
23980
|
-
cwe: "CWE-89",
|
|
23981
|
-
severity: "critical",
|
|
23982
|
-
argPositions: [0]
|
|
23983
|
-
},
|
|
23984
23620
|
{
|
|
23985
23621
|
method: "query",
|
|
23986
23622
|
type: "sql_injection",
|
|
@@ -23996,22 +23632,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23996
23632
|
argPositions: [0]
|
|
23997
23633
|
},
|
|
23998
23634
|
// Path Traversal
|
|
23999
|
-
{
|
|
24000
|
-
method: "open",
|
|
24001
|
-
class: "File",
|
|
24002
|
-
type: "path_traversal",
|
|
24003
|
-
cwe: "CWE-22",
|
|
24004
|
-
severity: "high",
|
|
24005
|
-
argPositions: [0]
|
|
24006
|
-
},
|
|
24007
|
-
{
|
|
24008
|
-
method: "create",
|
|
24009
|
-
class: "File",
|
|
24010
|
-
type: "path_traversal",
|
|
24011
|
-
cwe: "CWE-22",
|
|
24012
|
-
severity: "high",
|
|
24013
|
-
argPositions: [0]
|
|
24014
|
-
},
|
|
24015
23635
|
{
|
|
24016
23636
|
method: "read_to_string",
|
|
24017
23637
|
class: "std::fs",
|
|
@@ -24051,40 +23671,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
24051
23671
|
severity: "critical",
|
|
24052
23672
|
argPositions: [0]
|
|
24053
23673
|
},
|
|
24054
|
-
// Deserialization (with serde)
|
|
24055
|
-
{
|
|
24056
|
-
method: "from_str",
|
|
24057
|
-
class: "serde_json",
|
|
24058
|
-
type: "deserialization",
|
|
24059
|
-
cwe: "CWE-502",
|
|
24060
|
-
severity: "medium",
|
|
24061
|
-
argPositions: [0]
|
|
24062
|
-
},
|
|
24063
|
-
{
|
|
24064
|
-
method: "from_slice",
|
|
24065
|
-
class: "serde_json",
|
|
24066
|
-
type: "deserialization",
|
|
24067
|
-
cwe: "CWE-502",
|
|
24068
|
-
severity: "medium",
|
|
24069
|
-
argPositions: [0]
|
|
24070
|
-
},
|
|
24071
|
-
// SSRF
|
|
24072
|
-
{
|
|
24073
|
-
method: "get",
|
|
24074
|
-
class: "reqwest",
|
|
24075
|
-
type: "ssrf",
|
|
24076
|
-
cwe: "CWE-918",
|
|
24077
|
-
severity: "high",
|
|
24078
|
-
argPositions: [0]
|
|
24079
|
-
},
|
|
24080
|
-
{
|
|
24081
|
-
method: "post",
|
|
24082
|
-
class: "reqwest",
|
|
24083
|
-
type: "ssrf",
|
|
24084
|
-
cwe: "CWE-918",
|
|
24085
|
-
severity: "high",
|
|
24086
|
-
argPositions: [0]
|
|
24087
|
-
},
|
|
24088
23674
|
// Regex DoS
|
|
24089
23675
|
{
|
|
24090
23676
|
method: "new",
|
|
@@ -24199,14 +23785,13 @@ var BashPlugin = class extends BaseLanguagePlugin {
|
|
|
24199
23785
|
*/
|
|
24200
23786
|
getBuiltinSources() {
|
|
24201
23787
|
return [
|
|
24202
|
-
// read
|
|
24203
|
-
|
|
24204
|
-
|
|
24205
|
-
|
|
24206
|
-
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
},
|
|
23788
|
+
// NOTE: no `read` entry here. The `read` builtin is covered precisely by
|
|
23789
|
+
// `findBashTaintSources` (language-sources-pass, #213 slice 3), which
|
|
23790
|
+
// emits `io_input` *with the bound variable name* for `read x`,
|
|
23791
|
+
// `read -p "…" x`, `read -a arr`, bare `read` → `$REPLY`, etc. This
|
|
23792
|
+
// pattern-based duplicate added a second, variable-less source on the
|
|
23793
|
+
// same line, and typed it differently from the classless `read`
|
|
23794
|
+
// (file_input) entry in DEFAULT_SOURCES that it collided with.
|
|
24210
23795
|
// curl/wget output as taint sources for supply-chain attack patterns
|
|
24211
23796
|
// (e.g., curl ... | sh, $(curl ...) piped to eval/bash).
|
|
24212
23797
|
{
|
|
@@ -24232,14 +23817,11 @@ var BashPlugin = class extends BaseLanguagePlugin {
|
|
|
24232
23817
|
*/
|
|
24233
23818
|
getBuiltinSinks() {
|
|
24234
23819
|
return [
|
|
24235
|
-
//
|
|
24236
|
-
|
|
24237
|
-
|
|
24238
|
-
|
|
24239
|
-
|
|
24240
|
-
severity: "critical",
|
|
24241
|
-
argPositions: [0]
|
|
24242
|
-
},
|
|
23820
|
+
// NOTE: classless `eval` (CWE-94, critical) is registered unscoped in
|
|
23821
|
+
// DEFAULT_SINKS and already covers Bash; the duplicate builtin that used
|
|
23822
|
+
// to sit here never won the findSinks dedup slot (equal confidence, the
|
|
23823
|
+
// config pattern iterates first). Removed in the #4 follow-up
|
|
23824
|
+
// consolidation — DEFAULT_SINKS is the canonical registry (ADR-004).
|
|
24243
23825
|
// Command injection: spawning a sub-shell with -c flag
|
|
24244
23826
|
{
|
|
24245
23827
|
method: "bash",
|
|
@@ -24681,30 +24263,15 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24681
24263
|
returnTainted: true
|
|
24682
24264
|
},
|
|
24683
24265
|
// Gin framework
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
24687
|
-
|
|
24688
|
-
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
method: "Param",
|
|
24694
|
-
class: "Context",
|
|
24695
|
-
type: "http_path",
|
|
24696
|
-
severity: "high",
|
|
24697
|
-
confidence: 0.95,
|
|
24698
|
-
returnTainted: true
|
|
24699
|
-
},
|
|
24700
|
-
{
|
|
24701
|
-
method: "PostForm",
|
|
24702
|
-
class: "Context",
|
|
24703
|
-
type: "http_param",
|
|
24704
|
-
severity: "high",
|
|
24705
|
-
confidence: 0.95,
|
|
24706
|
-
returnTainted: true
|
|
24707
|
-
},
|
|
24266
|
+
// NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
|
|
24267
|
+
// registered identically in DEFAULT_SOURCES; the duplicates here emitted
|
|
24268
|
+
// a second, byte-identical source at every call site (`findSources` has
|
|
24269
|
+
// no dedup). Removed in the #4 follow-up consolidation — DEFAULT_SOURCES
|
|
24270
|
+
// is the canonical registry (ADR-004). `PostForm` stays because the two
|
|
24271
|
+
// `Context.PostForm` is registered in DEFAULT_SOURCES as `http_body`,
|
|
24272
|
+
// which is the accurate type — Gin reads it from the POST body, not the
|
|
24273
|
+
// query string. The duplicate that used to sit here typed it
|
|
24274
|
+
// `http_param`, so one call emitted two sources with different types.
|
|
24708
24275
|
{
|
|
24709
24276
|
method: "GetRawData",
|
|
24710
24277
|
class: "Context",
|
|
@@ -24750,6 +24317,10 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24750
24317
|
}
|
|
24751
24318
|
/**
|
|
24752
24319
|
* Go taint sink patterns.
|
|
24320
|
+
*
|
|
24321
|
+
* Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
|
|
24322
|
+
* `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
|
|
24323
|
+
* (ADR-004). See the #4 follow-up consolidation note on the other plugins.
|
|
24753
24324
|
*/
|
|
24754
24325
|
getBuiltinSinks() {
|
|
24755
24326
|
return [
|
|
@@ -24926,14 +24497,9 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24926
24497
|
severity: "medium",
|
|
24927
24498
|
argPositions: [0]
|
|
24928
24499
|
},
|
|
24929
|
-
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
type: "deserialization",
|
|
24933
|
-
cwe: "CWE-502",
|
|
24934
|
-
severity: "medium",
|
|
24935
|
-
argPositions: [0]
|
|
24936
|
-
},
|
|
24500
|
+
// NOTE: `Decoder.Decode` (CWE-502) lives in DEFAULT_SINKS at severity
|
|
24501
|
+
// `high`, which outranked this duplicate's `medium` in the findSinks
|
|
24502
|
+
// dedup — removed in the #4 follow-up consolidation.
|
|
24937
24503
|
// Log Injection — `log.Printf("event=%s", userInput)` allows
|
|
24938
24504
|
// CRLF/log forging when the format string interpolates user data.
|
|
24939
24505
|
// CWE-117. (cognium-dev #107)
|
|
@@ -26283,6 +25849,10 @@ var LanguageSourcesPass = class {
|
|
|
26283
25849
|
additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
|
|
26284
25850
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
26285
25851
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
25852
|
+
additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
|
|
25853
|
+
additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
|
|
25854
|
+
additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
|
|
25855
|
+
additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
|
|
26286
25856
|
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
26287
25857
|
additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
|
|
26288
25858
|
const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
|
|
@@ -26897,13 +26467,138 @@ function findPythonAssignmentSources(sourceCode, language) {
|
|
|
26897
26467
|
}
|
|
26898
26468
|
return sources;
|
|
26899
26469
|
}
|
|
26470
|
+
function evaluatePythonConstExpression(expr, consts) {
|
|
26471
|
+
const COMPARISON = /(<=|>=|==|!=|<|>)/;
|
|
26472
|
+
const parts2 = expr.split(COMPARISON);
|
|
26473
|
+
if (parts2.length === 3) {
|
|
26474
|
+
const left = evaluatePythonConstExpression(parts2[0], consts);
|
|
26475
|
+
const right = evaluatePythonConstExpression(parts2[2], consts);
|
|
26476
|
+
if (typeof left !== "number" || typeof right !== "number") return void 0;
|
|
26477
|
+
switch (parts2[1]) {
|
|
26478
|
+
case "<":
|
|
26479
|
+
return left < right;
|
|
26480
|
+
case ">":
|
|
26481
|
+
return left > right;
|
|
26482
|
+
case "<=":
|
|
26483
|
+
return left <= right;
|
|
26484
|
+
case ">=":
|
|
26485
|
+
return left >= right;
|
|
26486
|
+
case "==":
|
|
26487
|
+
return left === right;
|
|
26488
|
+
case "!=":
|
|
26489
|
+
return left !== right;
|
|
26490
|
+
default:
|
|
26491
|
+
return void 0;
|
|
26492
|
+
}
|
|
26493
|
+
}
|
|
26494
|
+
if (parts2.length !== 1) return void 0;
|
|
26495
|
+
const matched = expr.match(/\d+|[A-Za-z_][A-Za-z0-9_]*|\/\/|[-+*/%()]|\S/g);
|
|
26496
|
+
if (!matched) return void 0;
|
|
26497
|
+
const tokens = matched;
|
|
26498
|
+
let pos = 0;
|
|
26499
|
+
const peek = () => tokens[pos];
|
|
26500
|
+
const parseFactor = () => {
|
|
26501
|
+
const tok = tokens[pos++];
|
|
26502
|
+
if (tok === void 0) return void 0;
|
|
26503
|
+
if (tok === "(") {
|
|
26504
|
+
const inner = parseSum();
|
|
26505
|
+
if (tokens[pos++] !== ")") return void 0;
|
|
26506
|
+
return inner;
|
|
26507
|
+
}
|
|
26508
|
+
if (tok === "-") {
|
|
26509
|
+
const operand = parseFactor();
|
|
26510
|
+
return operand === void 0 ? void 0 : -operand;
|
|
26511
|
+
}
|
|
26512
|
+
if (/^\d+$/.test(tok)) return Number(tok);
|
|
26513
|
+
if (/^[A-Za-z_]/.test(tok)) return consts.get(tok);
|
|
26514
|
+
return void 0;
|
|
26515
|
+
};
|
|
26516
|
+
const parseTerm = () => {
|
|
26517
|
+
let value = parseFactor();
|
|
26518
|
+
if (value === void 0) return void 0;
|
|
26519
|
+
while (peek() === "*" || peek() === "/" || peek() === "//" || peek() === "%") {
|
|
26520
|
+
const op = tokens[pos++];
|
|
26521
|
+
const rhs = parseFactor();
|
|
26522
|
+
if (rhs === void 0) return void 0;
|
|
26523
|
+
if ((op === "/" || op === "//" || op === "%") && rhs === 0) return void 0;
|
|
26524
|
+
if (op === "*") value *= rhs;
|
|
26525
|
+
else if (op === "/") value /= rhs;
|
|
26526
|
+
else if (op === "//") value = Math.floor(value / rhs);
|
|
26527
|
+
else value = (value % rhs + rhs) % rhs;
|
|
26528
|
+
}
|
|
26529
|
+
return value;
|
|
26530
|
+
};
|
|
26531
|
+
function parseSum() {
|
|
26532
|
+
let value = parseTerm();
|
|
26533
|
+
if (value === void 0) return void 0;
|
|
26534
|
+
while (peek() === "+" || peek() === "-") {
|
|
26535
|
+
const op = tokens[pos++];
|
|
26536
|
+
const rhs = parseTerm();
|
|
26537
|
+
if (rhs === void 0) return void 0;
|
|
26538
|
+
value = op === "+" ? value + rhs : value - rhs;
|
|
26539
|
+
}
|
|
26540
|
+
return value;
|
|
26541
|
+
}
|
|
26542
|
+
const result = parseSum();
|
|
26543
|
+
return pos === tokens.length ? result : void 0;
|
|
26544
|
+
}
|
|
26545
|
+
function normalizeListIndex(index, length) {
|
|
26546
|
+
const resolved = index < 0 ? length + index : index;
|
|
26547
|
+
return resolved >= 0 && resolved < length ? resolved : void 0;
|
|
26548
|
+
}
|
|
26549
|
+
function computeDeadPythonBranchLines(lines) {
|
|
26550
|
+
const dead = /* @__PURE__ */ new Set();
|
|
26551
|
+
const consts = /* @__PURE__ */ new Map();
|
|
26552
|
+
const indentOf = (line) => line.length - line.trimStart().length;
|
|
26553
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26554
|
+
const line = lines[i2];
|
|
26555
|
+
const assign = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.+)$/);
|
|
26556
|
+
if (assign) {
|
|
26557
|
+
const value = evaluatePythonConstExpression(assign[2].trim(), consts);
|
|
26558
|
+
if (typeof value === "number") consts.set(assign[1], value);
|
|
26559
|
+
else consts.delete(assign[1]);
|
|
26560
|
+
}
|
|
26561
|
+
const ifMatch = line.match(/^(\s*)if\s+(.+?)\s*:\s*$/);
|
|
26562
|
+
if (!ifMatch) continue;
|
|
26563
|
+
const indent = ifMatch[1].length;
|
|
26564
|
+
const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
|
|
26565
|
+
if (decided !== true && decided !== false) continue;
|
|
26566
|
+
const thenStart = i2 + 1;
|
|
26567
|
+
let cursor = thenStart;
|
|
26568
|
+
while (cursor < lines.length && (lines[cursor].trim() === "" || indentOf(lines[cursor]) > indent)) {
|
|
26569
|
+
cursor++;
|
|
26570
|
+
}
|
|
26571
|
+
const thenEnd = cursor - 1;
|
|
26572
|
+
let elseStart = -1;
|
|
26573
|
+
let elseEnd = -1;
|
|
26574
|
+
if (cursor < lines.length && indentOf(lines[cursor]) === indent) {
|
|
26575
|
+
if (/^\s*elif\b/.test(lines[cursor])) continue;
|
|
26576
|
+
if (/^\s*else\s*:\s*$/.test(lines[cursor])) {
|
|
26577
|
+
elseStart = cursor + 1;
|
|
26578
|
+
let k = elseStart;
|
|
26579
|
+
while (k < lines.length && (lines[k].trim() === "" || indentOf(lines[k]) > indent)) k++;
|
|
26580
|
+
elseEnd = k - 1;
|
|
26581
|
+
}
|
|
26582
|
+
}
|
|
26583
|
+
if (decided === true) {
|
|
26584
|
+
for (let l = elseStart; l >= 0 && l <= elseEnd; l++) dead.add(l);
|
|
26585
|
+
} else {
|
|
26586
|
+
for (let l = thenStart; l <= thenEnd; l++) dead.add(l);
|
|
26587
|
+
}
|
|
26588
|
+
}
|
|
26589
|
+
return dead;
|
|
26590
|
+
}
|
|
26900
26591
|
function buildPythonTaintedVars(sourceCode) {
|
|
26901
26592
|
const tainted = /* @__PURE__ */ new Map();
|
|
26902
26593
|
const containerTainted = /* @__PURE__ */ new Map();
|
|
26903
26594
|
const lines = sourceCode.split("\n");
|
|
26595
|
+
const deadBranchLines = computeDeadPythonBranchLines(lines);
|
|
26596
|
+
const listElems = /* @__PURE__ */ new Map();
|
|
26597
|
+
const intConsts = /* @__PURE__ */ new Map();
|
|
26904
26598
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26905
26599
|
const line = lines[i2];
|
|
26906
26600
|
if (line.trimStart().startsWith("#")) continue;
|
|
26601
|
+
if (deadBranchLines.has(i2)) continue;
|
|
26907
26602
|
const subscriptAssign = line.match(/^\s*([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]\s*=\s*(.+)$/u);
|
|
26908
26603
|
if (subscriptAssign) {
|
|
26909
26604
|
const [, container, , key, rhs2] = subscriptAssign;
|
|
@@ -26920,10 +26615,48 @@ function buildPythonTaintedVars(sourceCode) {
|
|
|
26920
26615
|
}
|
|
26921
26616
|
const containerAppendMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/u);
|
|
26922
26617
|
if (containerAppendMatch) {
|
|
26923
|
-
const [, receiver, , argExpr] = containerAppendMatch;
|
|
26618
|
+
const [, receiver, method, argExpr] = containerAppendMatch;
|
|
26924
26619
|
const argIsTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(argExpr));
|
|
26925
26620
|
const argIsDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(argExpr));
|
|
26926
26621
|
if (argIsTainted || argIsDirectSource) tainted.set(receiver, tainted.get(receiver) ?? i2 + 1);
|
|
26622
|
+
const elems = listElems.get(receiver);
|
|
26623
|
+
if (elems) {
|
|
26624
|
+
if (method === "append") {
|
|
26625
|
+
elems.push(argIsTainted || argIsDirectSource);
|
|
26626
|
+
} else if (method === "insert") {
|
|
26627
|
+
const insertAt = argExpr.match(/^(-?\d+)\s*,\s*(.+)$/);
|
|
26628
|
+
if (insertAt) {
|
|
26629
|
+
const idx = normalizeListIndex(Number(insertAt[1]), elems.length);
|
|
26630
|
+
const valueExpr = insertAt[2];
|
|
26631
|
+
const valueTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(valueExpr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(valueExpr));
|
|
26632
|
+
if (idx !== void 0) elems.splice(idx, 0, valueTainted);
|
|
26633
|
+
else listElems.delete(receiver);
|
|
26634
|
+
} else {
|
|
26635
|
+
listElems.delete(receiver);
|
|
26636
|
+
}
|
|
26637
|
+
} else {
|
|
26638
|
+
listElems.delete(receiver);
|
|
26639
|
+
}
|
|
26640
|
+
}
|
|
26641
|
+
continue;
|
|
26642
|
+
}
|
|
26643
|
+
const popMatch = line.match(/^\s*(?:([\p{L}\p{N}_]+)\s*=\s*)?([\p{L}\p{N}_]+)\.pop\s*\(\s*(-?\d+)?\s*\)\s*$/u);
|
|
26644
|
+
const delMatch = line.match(/^\s*del\s+([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]\s*$/u);
|
|
26645
|
+
if (popMatch || delMatch) {
|
|
26646
|
+
const receiver = popMatch ? popMatch[2] : delMatch[1];
|
|
26647
|
+
const rawIndex = popMatch ? popMatch[3] !== void 0 ? Number(popMatch[3]) : -1 : Number(delMatch[2]);
|
|
26648
|
+
const elems = listElems.get(receiver);
|
|
26649
|
+
if (elems) {
|
|
26650
|
+
const idx = normalizeListIndex(rawIndex, elems.length);
|
|
26651
|
+
if (idx === void 0) listElems.delete(receiver);
|
|
26652
|
+
else {
|
|
26653
|
+
const [removed] = elems.splice(idx, 1);
|
|
26654
|
+
if (popMatch && popMatch[1]) {
|
|
26655
|
+
if (removed) tainted.set(popMatch[1], i2 + 1);
|
|
26656
|
+
else tainted.delete(popMatch[1]);
|
|
26657
|
+
}
|
|
26658
|
+
}
|
|
26659
|
+
}
|
|
26927
26660
|
continue;
|
|
26928
26661
|
}
|
|
26929
26662
|
const augAssign = line.match(/^\s*([\p{L}\p{N}_]+)\s*\+=\s*(.+)$/u);
|
|
@@ -26943,7 +26676,48 @@ function buildPythonTaintedVars(sourceCode) {
|
|
|
26943
26676
|
}
|
|
26944
26677
|
const assignMatch = line.match(/^\s*([\p{L}\p{N}_]+)\s*=\s*(.+)$/u);
|
|
26945
26678
|
if (!assignMatch) continue;
|
|
26946
|
-
const [, lhs,
|
|
26679
|
+
const [, lhs, rhsRaw] = assignMatch;
|
|
26680
|
+
const constValue = evaluatePythonConstExpression(rhsRaw.trim(), intConsts);
|
|
26681
|
+
if (typeof constValue === "number") intConsts.set(lhs, constValue);
|
|
26682
|
+
else intConsts.delete(lhs);
|
|
26683
|
+
let rhs = rhsRaw;
|
|
26684
|
+
const ternary = rhsRaw.match(/^(.+?)\s+if\s+(.+?)\s+else\s+(.+)$/);
|
|
26685
|
+
if (ternary) {
|
|
26686
|
+
const decided = evaluatePythonConstExpression(ternary[2].trim(), intConsts);
|
|
26687
|
+
if (decided === true) rhs = ternary[1].trim();
|
|
26688
|
+
else if (decided === false) rhs = ternary[3].trim();
|
|
26689
|
+
}
|
|
26690
|
+
const listLiteral = rhs.trim().match(/^\[\s*(.*?)\s*\]$/);
|
|
26691
|
+
if (listLiteral) {
|
|
26692
|
+
const inner = listLiteral[1];
|
|
26693
|
+
if (inner === "") {
|
|
26694
|
+
listElems.set(lhs, []);
|
|
26695
|
+
} else if (!inner.includes("[") && !inner.includes("(")) {
|
|
26696
|
+
listElems.set(
|
|
26697
|
+
lhs,
|
|
26698
|
+
inner.split(",").map((part) => {
|
|
26699
|
+
const expr = part.trim();
|
|
26700
|
+
return [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(expr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(expr));
|
|
26701
|
+
})
|
|
26702
|
+
);
|
|
26703
|
+
} else {
|
|
26704
|
+
listElems.delete(lhs);
|
|
26705
|
+
}
|
|
26706
|
+
} else {
|
|
26707
|
+
listElems.delete(lhs);
|
|
26708
|
+
}
|
|
26709
|
+
const indexRead = rhs.trim().match(/^([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]$/u);
|
|
26710
|
+
if (indexRead) {
|
|
26711
|
+
const elems = listElems.get(indexRead[1]);
|
|
26712
|
+
if (elems) {
|
|
26713
|
+
const idx = normalizeListIndex(Number(indexRead[2]), elems.length);
|
|
26714
|
+
if (idx !== void 0) {
|
|
26715
|
+
if (elems[idx]) tainted.set(lhs, i2 + 1);
|
|
26716
|
+
else tainted.delete(lhs);
|
|
26717
|
+
continue;
|
|
26718
|
+
}
|
|
26719
|
+
}
|
|
26720
|
+
}
|
|
26947
26721
|
const isDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(rhs));
|
|
26948
26722
|
let propagatedFrom;
|
|
26949
26723
|
const dictAccessMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]$/u);
|
|
@@ -28067,6 +27841,185 @@ function findPythonSetMembershipXssGuardSanitizers(code) {
|
|
|
28067
27841
|
}
|
|
28068
27842
|
return sanitizers;
|
|
28069
27843
|
}
|
|
27844
|
+
function findPythonTraversalRejectGuardSanitizers(code) {
|
|
27845
|
+
const sanitizers = [];
|
|
27846
|
+
const lines = code.split("\n");
|
|
27847
|
+
const guardOpen = /^(\s*)if\s+(['"])(\.\.[/\\]?)\2\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
|
|
27848
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
27849
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27850
|
+
const m = guardOpen.exec(lines[i2]);
|
|
27851
|
+
if (!m) continue;
|
|
27852
|
+
const guardIndent = m[1].length;
|
|
27853
|
+
const guardedVar = m[4];
|
|
27854
|
+
let bodyHasTerminator = false;
|
|
27855
|
+
let blockEnd = -1;
|
|
27856
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
27857
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
27858
|
+
const line = lines[j];
|
|
27859
|
+
if (line.trim() === "") continue;
|
|
27860
|
+
const indent = line.length - line.trimStart().length;
|
|
27861
|
+
if (indent <= guardIndent) {
|
|
27862
|
+
blockEnd = j - 1;
|
|
27863
|
+
break;
|
|
27864
|
+
}
|
|
27865
|
+
if (terminator.test(line)) bodyHasTerminator = true;
|
|
27866
|
+
}
|
|
27867
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
|
|
27868
|
+
if (!bodyHasTerminator) continue;
|
|
27869
|
+
const guarded = /* @__PURE__ */ new Set([guardedVar]);
|
|
27870
|
+
const assignRe = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.*)$/;
|
|
27871
|
+
const mentionsGuarded = (text) => {
|
|
27872
|
+
for (const name2 of guarded) {
|
|
27873
|
+
if (new RegExp(`\\b${name2}\\b`).test(text)) return true;
|
|
27874
|
+
}
|
|
27875
|
+
return false;
|
|
27876
|
+
};
|
|
27877
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
27878
|
+
const lineText = lines[l - 1];
|
|
27879
|
+
const assign = assignRe.exec(lineText);
|
|
27880
|
+
if (assign) {
|
|
27881
|
+
const target = assign[1];
|
|
27882
|
+
const rhs = assign[2];
|
|
27883
|
+
if (mentionsGuarded(rhs)) guarded.add(target);
|
|
27884
|
+
else if (target !== guardedVar) guarded.delete(target);
|
|
27885
|
+
}
|
|
27886
|
+
if (!mentionsGuarded(lineText)) continue;
|
|
27887
|
+
sanitizers.push({
|
|
27888
|
+
type: "python_traversal_reject_guard",
|
|
27889
|
+
method: "if",
|
|
27890
|
+
line: l,
|
|
27891
|
+
sanitizes: ["path_traversal"]
|
|
27892
|
+
});
|
|
27893
|
+
}
|
|
27894
|
+
}
|
|
27895
|
+
return sanitizers;
|
|
27896
|
+
}
|
|
27897
|
+
function findPythonStringLiteralGuardSanitizers(code) {
|
|
27898
|
+
const sanitizers = [];
|
|
27899
|
+
const lines = code.split("\n");
|
|
27900
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
27901
|
+
const Q = `(?:'(?:\\\\')'|"'"|'"'|"(?:\\\\")")`;
|
|
27902
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27903
|
+
const guard = lines[i2].match(
|
|
27904
|
+
new RegExp(
|
|
27905
|
+
`^(\\s*)if\\s+not\\s+([A-Za-z_][A-Za-z0-9_]*)\\.startswith\\(\\s*${Q}\\s*\\)\\s+or\\s+not\\s+\\2\\.endswith\\(\\s*${Q}\\s*\\)\\s+or\\s+${Q}\\s+in\\s+\\2\\[1:-1\\]\\s*:\\s*$`
|
|
27906
|
+
)
|
|
27907
|
+
);
|
|
27908
|
+
if (!guard) continue;
|
|
27909
|
+
const guardIndent = guard[1].length;
|
|
27910
|
+
const guardedVar = guard[2];
|
|
27911
|
+
let bodyHasTerminator = false;
|
|
27912
|
+
let blockEnd = -1;
|
|
27913
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
27914
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
27915
|
+
const line = lines[j];
|
|
27916
|
+
if (line.trim() === "") continue;
|
|
27917
|
+
const indent = line.length - line.trimStart().length;
|
|
27918
|
+
if (indent <= guardIndent) {
|
|
27919
|
+
blockEnd = j - 1;
|
|
27920
|
+
break;
|
|
27921
|
+
}
|
|
27922
|
+
if (terminator.test(line)) bodyHasTerminator = true;
|
|
27923
|
+
}
|
|
27924
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
|
|
27925
|
+
if (!bodyHasTerminator) continue;
|
|
27926
|
+
const varRe = new RegExp(`\\b${guardedVar}\\b`);
|
|
27927
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
27928
|
+
if (!varRe.test(lines[l - 1])) continue;
|
|
27929
|
+
sanitizers.push({
|
|
27930
|
+
type: "python_string_literal_guard",
|
|
27931
|
+
method: "if",
|
|
27932
|
+
line: l,
|
|
27933
|
+
sanitizes: ["code_injection"]
|
|
27934
|
+
});
|
|
27935
|
+
}
|
|
27936
|
+
}
|
|
27937
|
+
return sanitizers;
|
|
27938
|
+
}
|
|
27939
|
+
function findPythonDefaultSaxParserXxeSanitizers(code) {
|
|
27940
|
+
const sanitizers = [];
|
|
27941
|
+
const lines = code.split("\n");
|
|
27942
|
+
const parserVars = /* @__PURE__ */ new Set();
|
|
27943
|
+
for (const line of lines) {
|
|
27944
|
+
const made = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:xml\.)?sax\.make_parser\s*\(/);
|
|
27945
|
+
if (made) parserVars.add(made[1]);
|
|
27946
|
+
}
|
|
27947
|
+
if (parserVars.size === 0) return sanitizers;
|
|
27948
|
+
for (const line of lines) {
|
|
27949
|
+
for (const parser of [...parserVars]) {
|
|
27950
|
+
const enabled = new RegExp(
|
|
27951
|
+
`\\b${parser}\\.setFeature\\s*\\([^,]*external_(?:ges|pes)[^,]*,\\s*True\\s*\\)`
|
|
27952
|
+
);
|
|
27953
|
+
const resolver = new RegExp(`\\b${parser}\\.setEntityResolver\\s*\\(`);
|
|
27954
|
+
const rebound = new RegExp(`^\\s*${parser}\\s*=(?!=)`);
|
|
27955
|
+
if (enabled.test(line) || resolver.test(line)) parserVars.delete(parser);
|
|
27956
|
+
else if (rebound.test(line) && !/make_parser\s*\(/.test(line)) parserVars.delete(parser);
|
|
27957
|
+
}
|
|
27958
|
+
}
|
|
27959
|
+
if (parserVars.size === 0) return sanitizers;
|
|
27960
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27961
|
+
const line = lines[i2];
|
|
27962
|
+
const parseCall = /\b(?:minidom\.)?parseString\s*\(|\b(?:minidom\.)?parse\s*\(|\bsax\.parse(?:String)?\s*\(/;
|
|
27963
|
+
if (!parseCall.test(line)) continue;
|
|
27964
|
+
const usesDefaultParser = [...parserVars].some((p) => new RegExp(`\\b${p}\\b`).test(line));
|
|
27965
|
+
if (!usesDefaultParser) continue;
|
|
27966
|
+
sanitizers.push({
|
|
27967
|
+
type: "python_default_sax_parser",
|
|
27968
|
+
method: "make_parser",
|
|
27969
|
+
line: i2 + 1,
|
|
27970
|
+
sanitizes: ["xxe"]
|
|
27971
|
+
});
|
|
27972
|
+
}
|
|
27973
|
+
return sanitizers;
|
|
27974
|
+
}
|
|
27975
|
+
function findPythonUrlAllowlistRedirectSanitizers(code) {
|
|
27976
|
+
const sanitizers = [];
|
|
27977
|
+
const lines = code.split("\n");
|
|
27978
|
+
const parsedFrom = /* @__PURE__ */ new Map();
|
|
27979
|
+
for (const line of lines) {
|
|
27980
|
+
const m = line.match(
|
|
27981
|
+
/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:urllib\.parse\.)?urlparse\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/
|
|
27982
|
+
);
|
|
27983
|
+
if (m) parsedFrom.set(m[1], m[2]);
|
|
27984
|
+
}
|
|
27985
|
+
if (parsedFrom.size === 0) return sanitizers;
|
|
27986
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
27987
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27988
|
+
const guard = lines[i2].match(
|
|
27989
|
+
/^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\.netloc\s+not\s+in\s+[[({]/
|
|
27990
|
+
);
|
|
27991
|
+
if (!guard) continue;
|
|
27992
|
+
const guardIndent = guard[1].length;
|
|
27993
|
+
const rawVar = parsedFrom.get(guard[2]);
|
|
27994
|
+
if (!rawVar) continue;
|
|
27995
|
+
let bodyHasTerminator = false;
|
|
27996
|
+
let blockEnd = -1;
|
|
27997
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
27998
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
27999
|
+
const line = lines[j];
|
|
28000
|
+
if (line.trim() === "") continue;
|
|
28001
|
+
const indent = line.length - line.trimStart().length;
|
|
28002
|
+
if (indent <= guardIndent) {
|
|
28003
|
+
blockEnd = j - 1;
|
|
28004
|
+
break;
|
|
28005
|
+
}
|
|
28006
|
+
if (terminator.test(line)) bodyHasTerminator = true;
|
|
28007
|
+
}
|
|
28008
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
|
|
28009
|
+
if (!bodyHasTerminator) continue;
|
|
28010
|
+
const varRe = new RegExp(`\\b${rawVar}\\b`);
|
|
28011
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
28012
|
+
if (!varRe.test(lines[l - 1])) continue;
|
|
28013
|
+
sanitizers.push({
|
|
28014
|
+
type: "python_url_allowlist_guard",
|
|
28015
|
+
method: "if",
|
|
28016
|
+
line: l,
|
|
28017
|
+
sanitizes: ["open_redirect"]
|
|
28018
|
+
});
|
|
28019
|
+
}
|
|
28020
|
+
}
|
|
28021
|
+
return sanitizers;
|
|
28022
|
+
}
|
|
28070
28023
|
function findPythonDefusedXmlSanitizers(code) {
|
|
28071
28024
|
const sanitizers = [];
|
|
28072
28025
|
const lines = code.split("\n");
|
|
@@ -32588,15 +32541,19 @@ var SinkFilterPass = class {
|
|
|
32588
32541
|
filtered = filtered.filter((sink) => {
|
|
32589
32542
|
if (sink.type !== "xpath_injection") return true;
|
|
32590
32543
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
32591
|
-
const
|
|
32592
|
-
(v) => new RegExp(
|
|
32544
|
+
const taintedValueVars = [...pyTaintedVars.keys()].filter(
|
|
32545
|
+
(v) => new RegExp(`(?<![\\w.])${v}\\b(?!\\s*=(?!=))`).test(sinkLineText)
|
|
32593
32546
|
);
|
|
32547
|
+
const taintedVarOnLine = taintedValueVars[0];
|
|
32594
32548
|
const oopVarOnLine = [...oopFieldVars].find(
|
|
32595
32549
|
(v) => sinkLineText.includes(v)
|
|
32596
32550
|
);
|
|
32597
32551
|
if (oopVarOnLine) return true;
|
|
32598
32552
|
if (!taintedVarOnLine) return false;
|
|
32553
|
+
const neutralised = (v) => pySanitizedVars.has(v) || new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${v}\\b`).test(sinkLineText) || isXPathQuoteEscapedOnLine(sinkLineText, v);
|
|
32554
|
+
if (taintedValueVars.every(neutralised)) return false;
|
|
32599
32555
|
if (pySanitizedVars.has(taintedVarOnLine)) return false;
|
|
32556
|
+
if (isXPathQuoteEscapedOnLine(sinkLineText, taintedVarOnLine)) return false;
|
|
32600
32557
|
if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText)) return false;
|
|
32601
32558
|
return true;
|
|
32602
32559
|
});
|
|
@@ -33572,6 +33529,27 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
33572
33529
|
return true;
|
|
33573
33530
|
});
|
|
33574
33531
|
}
|
|
33532
|
+
function isXPathQuoteEscapedOnLine(lineText, varName) {
|
|
33533
|
+
const STRING_LITERAL = `'(?:[^'\\\\]|\\\\.)*'|"(?:[^"\\\\]|\\\\.)*"`;
|
|
33534
|
+
const replaceCall = new RegExp(
|
|
33535
|
+
`^${varName}\\.replace\\s*\\(\\s*(${STRING_LITERAL})\\s*,\\s*(${STRING_LITERAL})\\s*\\)`
|
|
33536
|
+
);
|
|
33537
|
+
const decode = (literal) => literal.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
33538
|
+
const occurrence = new RegExp(`(?<![\\w.])${varName}\\b`, "g");
|
|
33539
|
+
let match;
|
|
33540
|
+
let seen = 0;
|
|
33541
|
+
while ((match = occurrence.exec(lineText)) !== null) {
|
|
33542
|
+
seen++;
|
|
33543
|
+
const rest = lineText.slice(match.index);
|
|
33544
|
+
const call = replaceCall.exec(rest);
|
|
33545
|
+
if (!call) return false;
|
|
33546
|
+
const searched = decode(call[1]);
|
|
33547
|
+
const replacement = decode(call[2]);
|
|
33548
|
+
if (searched !== "'" && searched !== '"') return false;
|
|
33549
|
+
if (replacement.includes("'") || replacement.includes('"')) return false;
|
|
33550
|
+
}
|
|
33551
|
+
return seen > 0;
|
|
33552
|
+
}
|
|
33575
33553
|
|
|
33576
33554
|
// src/analysis/passes/sink-semantics-pass.ts
|
|
33577
33555
|
function buildRegistry(entries) {
|