circle-ir 3.193.0 → 3.195.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 +101 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts +75 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts.map +1 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js +353 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts +67 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js +193 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js.map +1 -0
- 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/analyzer.d.ts +19 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +30 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +587 -597
- package/dist/core/circle-ir-core.cjs +90 -15
- package/dist/core/circle-ir-core.js +90 -15
- package/dist/core/extractors/dfg.js +46 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- 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/types/config.d.ts +15 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -9935,6 +9935,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9935
9935
|
function buildGoDFG(tree) {
|
|
9936
9936
|
const defs = [];
|
|
9937
9937
|
const uses = [];
|
|
9938
|
+
const extraChains = [];
|
|
9938
9939
|
let defIdCounter = 1;
|
|
9939
9940
|
let useIdCounter = 1;
|
|
9940
9941
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -9956,7 +9957,7 @@ function buildGoDFG(tree) {
|
|
|
9956
9957
|
}
|
|
9957
9958
|
const body2 = func2.childForFieldName("body");
|
|
9958
9959
|
if (body2) {
|
|
9959
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
9960
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
9960
9961
|
defIdCounter = defs.length + 1;
|
|
9961
9962
|
useIdCounter = uses.length + 1;
|
|
9962
9963
|
}
|
|
@@ -9971,6 +9972,15 @@ function buildGoDFG(tree) {
|
|
|
9971
9972
|
}
|
|
9972
9973
|
}
|
|
9973
9974
|
const chains = computeChains(defs, uses);
|
|
9975
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
9976
|
+
for (const ch of extraChains) {
|
|
9977
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
9978
|
+
if (!seen.has(key)) {
|
|
9979
|
+
seen.add(key);
|
|
9980
|
+
chains.push(ch);
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
9974
9984
|
return { defs, uses, chains };
|
|
9975
9985
|
}
|
|
9976
9986
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -9995,28 +10005,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
9995
10005
|
}
|
|
9996
10006
|
}
|
|
9997
10007
|
}
|
|
9998
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
10008
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
10009
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
10010
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
10011
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
10012
|
+
const u = uses[ui];
|
|
10013
|
+
if (!u || u.def_id === null) continue;
|
|
10014
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
10015
|
+
const d = defs[di];
|
|
10016
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
10017
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
}
|
|
10021
|
+
};
|
|
9999
10022
|
walkTree(node, (child) => {
|
|
10000
10023
|
if (child.type === "short_var_declaration") {
|
|
10001
10024
|
const left = child.childForFieldName("left");
|
|
10002
10025
|
const right = child.childForFieldName("right");
|
|
10026
|
+
const usesBefore = uses.length;
|
|
10003
10027
|
if (right) {
|
|
10004
10028
|
extractGoUses(right, uses, scopeStack);
|
|
10005
10029
|
}
|
|
10030
|
+
const defsBefore = defs.length;
|
|
10006
10031
|
if (left) {
|
|
10007
10032
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10008
10033
|
}
|
|
10034
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10009
10035
|
} else if (child.type === "var_declaration") {
|
|
10010
10036
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
10011
10037
|
} else if (child.type === "assignment_statement") {
|
|
10012
10038
|
const left = child.childForFieldName("left");
|
|
10013
10039
|
const right = child.childForFieldName("right");
|
|
10040
|
+
const usesBefore = uses.length;
|
|
10014
10041
|
if (right) {
|
|
10015
10042
|
extractGoUses(right, uses, scopeStack);
|
|
10016
10043
|
}
|
|
10044
|
+
const defsBefore = defs.length;
|
|
10017
10045
|
if (left) {
|
|
10018
10046
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10019
10047
|
}
|
|
10048
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10020
10049
|
} else if (child.type === "for_statement") {
|
|
10021
10050
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10022
10051
|
if (rangeClause) {
|
|
@@ -11331,7 +11360,11 @@ var DEFAULT_SOURCES = [
|
|
|
11331
11360
|
{ method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11332
11361
|
{ method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11333
11362
|
{ method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
11334
|
-
|
|
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"] },
|
|
11335
11368
|
// Rust std library
|
|
11336
11369
|
{ method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
11337
11370
|
{ method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
@@ -12609,7 +12642,11 @@ var DEFAULT_SINKS = [
|
|
|
12609
12642
|
// Also match without receiver (destructured imports: const { exec } = require('child_process'))
|
|
12610
12643
|
// `exec` is intentionally classless: catches Node.js child_process.exec AND
|
|
12611
12644
|
// Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
|
|
12612
|
-
|
|
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"] },
|
|
12613
12650
|
// `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
|
|
12614
12651
|
{ method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12615
12652
|
{ method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
@@ -12637,7 +12674,11 @@ var DEFAULT_SINKS = [
|
|
|
12637
12674
|
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
12638
12675
|
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
12639
12676
|
// Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
|
|
12640
|
-
|
|
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"] },
|
|
12641
12682
|
// sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
|
|
12642
12683
|
// `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
|
|
12643
12684
|
// target `Connection.all`, so class-scoped patterns matching `Connection`
|
|
@@ -13196,10 +13237,30 @@ var DEFAULT_SINKS = [
|
|
|
13196
13237
|
{ method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13197
13238
|
{ method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13198
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.
|
|
13199
13251
|
{ method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
13200
13252
|
// cognium-dev #264 — Go stdlib `log` package format-string entry points.
|
|
13201
13253
|
// log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
|
|
13202
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.
|
|
13203
13264
|
{ method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13204
13265
|
{ method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
13205
13266
|
{ method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
@@ -13213,6 +13274,14 @@ var DEFAULT_SINKS = [
|
|
|
13213
13274
|
{ method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
13214
13275
|
// Express: res.location(url) and res.redirect(url) — Location header.
|
|
13215
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.
|
|
13216
13285
|
{ method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
13217
13286
|
// Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
|
|
13218
13287
|
// (Header is a map; the actual `value` is arg 1 of the call). We flag the
|
|
@@ -14133,18 +14202,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
|
|
|
14133
14202
|
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
14134
14203
|
return { sources, sinks, sanitizers };
|
|
14135
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
|
+
}
|
|
14136
14215
|
function expandPromisifyAliases(patterns, sourceLines, language) {
|
|
14137
14216
|
if (!sourceLines || sourceLines.length === 0) return patterns;
|
|
14138
14217
|
if (language !== "javascript" && language !== "typescript") return patterns;
|
|
14139
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*\)/;
|
|
14140
14219
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
14141
14220
|
for (const p of patterns) {
|
|
14142
|
-
if (p
|
|
14221
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14143
14222
|
if (p.class !== void 0) continue;
|
|
14144
14223
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
14145
14224
|
}
|
|
14146
14225
|
for (const p of patterns) {
|
|
14147
|
-
if (p
|
|
14226
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14148
14227
|
if (innerToPattern.has(p.method)) continue;
|
|
14149
14228
|
innerToPattern.set(p.method, p);
|
|
14150
14229
|
}
|
|
@@ -14178,7 +14257,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
|
|
|
14178
14257
|
const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
|
|
14179
14258
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
14180
14259
|
for (const p of patterns) {
|
|
14181
|
-
if (p
|
|
14260
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
14182
14261
|
if (p.class !== void 0) continue;
|
|
14183
14262
|
if (p.method !== "eval" && p.method !== "Function") continue;
|
|
14184
14263
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
@@ -14886,9 +14965,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14886
14965
|
return false;
|
|
14887
14966
|
}
|
|
14888
14967
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14889
|
-
const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
|
|
14890
|
-
(p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
|
|
14891
|
-
);
|
|
14968
|
+
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14892
14969
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
14893
14970
|
for (const call of calls) {
|
|
14894
14971
|
for (const pattern of patternsForLanguage) {
|
|
@@ -15233,10 +15310,8 @@ var SINK_FQN_EXCLUSIONS = {
|
|
|
15233
15310
|
]
|
|
15234
15311
|
};
|
|
15235
15312
|
function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
15236
|
-
if (pattern
|
|
15237
|
-
|
|
15238
|
-
return false;
|
|
15239
|
-
}
|
|
15313
|
+
if (!sinkPatternAppliesTo(pattern, language)) {
|
|
15314
|
+
return false;
|
|
15240
15315
|
}
|
|
15241
15316
|
const callMethodName = call.method_name;
|
|
15242
15317
|
const patternMethod = pattern.method;
|
|
@@ -22117,107 +22192,30 @@ var JavaPlugin = class extends BaseLanguagePlugin {
|
|
|
22117
22192
|
severity: "high",
|
|
22118
22193
|
confidence: 0.9,
|
|
22119
22194
|
returnTainted: true
|
|
22120
|
-
},
|
|
22121
|
-
// Servlet API
|
|
22122
|
-
{
|
|
22123
|
-
method: "getParameter",
|
|
22124
|
-
class: "HttpServletRequest",
|
|
22125
|
-
type: "http_param",
|
|
22126
|
-
severity: "high",
|
|
22127
|
-
confidence: 0.95,
|
|
22128
|
-
returnTainted: true
|
|
22129
|
-
},
|
|
22130
|
-
{
|
|
22131
|
-
method: "getParameterValues",
|
|
22132
|
-
class: "HttpServletRequest",
|
|
22133
|
-
type: "http_param",
|
|
22134
|
-
severity: "high",
|
|
22135
|
-
confidence: 0.95,
|
|
22136
|
-
returnTainted: true
|
|
22137
|
-
},
|
|
22138
|
-
{
|
|
22139
|
-
method: "getHeader",
|
|
22140
|
-
class: "HttpServletRequest",
|
|
22141
|
-
type: "http_header",
|
|
22142
|
-
severity: "high",
|
|
22143
|
-
confidence: 0.9,
|
|
22144
|
-
returnTainted: true
|
|
22145
|
-
},
|
|
22146
|
-
{
|
|
22147
|
-
method: "getCookies",
|
|
22148
|
-
class: "HttpServletRequest",
|
|
22149
|
-
type: "http_cookie",
|
|
22150
|
-
severity: "high",
|
|
22151
|
-
confidence: 0.9,
|
|
22152
|
-
returnTainted: true
|
|
22153
|
-
},
|
|
22154
|
-
{
|
|
22155
|
-
method: "getInputStream",
|
|
22156
|
-
class: "HttpServletRequest",
|
|
22157
|
-
type: "http_body",
|
|
22158
|
-
severity: "high",
|
|
22159
|
-
confidence: 0.85,
|
|
22160
|
-
returnTainted: true
|
|
22161
|
-
},
|
|
22162
|
-
{
|
|
22163
|
-
method: "getReader",
|
|
22164
|
-
class: "HttpServletRequest",
|
|
22165
|
-
type: "http_body",
|
|
22166
|
-
severity: "high",
|
|
22167
|
-
confidence: 0.85,
|
|
22168
|
-
returnTainted: true
|
|
22169
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).
|
|
22170
22203
|
];
|
|
22171
22204
|
}
|
|
22172
22205
|
/**
|
|
22173
22206
|
* Java-specific taint sink patterns.
|
|
22174
|
-
*
|
|
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`).
|
|
22175
22216
|
*/
|
|
22176
22217
|
getBuiltinSinks() {
|
|
22177
22218
|
return [
|
|
22178
|
-
// SQL Injection
|
|
22179
|
-
{
|
|
22180
|
-
method: "executeQuery",
|
|
22181
|
-
class: "Statement",
|
|
22182
|
-
type: "sql_injection",
|
|
22183
|
-
cwe: "CWE-89",
|
|
22184
|
-
severity: "critical",
|
|
22185
|
-
argPositions: [0]
|
|
22186
|
-
},
|
|
22187
|
-
{
|
|
22188
|
-
method: "executeUpdate",
|
|
22189
|
-
class: "Statement",
|
|
22190
|
-
type: "sql_injection",
|
|
22191
|
-
cwe: "CWE-89",
|
|
22192
|
-
severity: "critical",
|
|
22193
|
-
argPositions: [0]
|
|
22194
|
-
},
|
|
22195
|
-
{
|
|
22196
|
-
method: "execute",
|
|
22197
|
-
class: "Statement",
|
|
22198
|
-
type: "sql_injection",
|
|
22199
|
-
cwe: "CWE-89",
|
|
22200
|
-
severity: "critical",
|
|
22201
|
-
argPositions: [0]
|
|
22202
|
-
},
|
|
22203
|
-
// Command Injection
|
|
22204
|
-
{
|
|
22205
|
-
method: "exec",
|
|
22206
|
-
class: "Runtime",
|
|
22207
|
-
type: "command_injection",
|
|
22208
|
-
cwe: "CWE-78",
|
|
22209
|
-
severity: "critical",
|
|
22210
|
-
argPositions: [0]
|
|
22211
|
-
},
|
|
22212
|
-
{
|
|
22213
|
-
method: "start",
|
|
22214
|
-
class: "ProcessBuilder",
|
|
22215
|
-
type: "command_injection",
|
|
22216
|
-
cwe: "CWE-78",
|
|
22217
|
-
severity: "critical",
|
|
22218
|
-
argPositions: []
|
|
22219
|
-
// Constructor args are dangerous
|
|
22220
|
-
},
|
|
22221
22219
|
// Path Traversal
|
|
22222
22220
|
{
|
|
22223
22221
|
method: "FileInputStream",
|
|
@@ -22233,41 +22231,6 @@ var JavaPlugin = class extends BaseLanguagePlugin {
|
|
|
22233
22231
|
severity: "high",
|
|
22234
22232
|
argPositions: [0]
|
|
22235
22233
|
},
|
|
22236
|
-
// XSS
|
|
22237
|
-
{
|
|
22238
|
-
method: "write",
|
|
22239
|
-
class: "PrintWriter",
|
|
22240
|
-
type: "xss",
|
|
22241
|
-
cwe: "CWE-79",
|
|
22242
|
-
severity: "high",
|
|
22243
|
-
argPositions: [0]
|
|
22244
|
-
},
|
|
22245
|
-
{
|
|
22246
|
-
method: "println",
|
|
22247
|
-
class: "PrintWriter",
|
|
22248
|
-
type: "xss",
|
|
22249
|
-
cwe: "CWE-79",
|
|
22250
|
-
severity: "high",
|
|
22251
|
-
argPositions: [0]
|
|
22252
|
-
},
|
|
22253
|
-
// LDAP Injection
|
|
22254
|
-
{
|
|
22255
|
-
method: "search",
|
|
22256
|
-
class: "DirContext",
|
|
22257
|
-
type: "ldap_injection",
|
|
22258
|
-
cwe: "CWE-90",
|
|
22259
|
-
severity: "high",
|
|
22260
|
-
argPositions: [0, 1]
|
|
22261
|
-
},
|
|
22262
|
-
// XPath Injection
|
|
22263
|
-
{
|
|
22264
|
-
method: "evaluate",
|
|
22265
|
-
class: "XPath",
|
|
22266
|
-
type: "xpath_injection",
|
|
22267
|
-
cwe: "CWE-643",
|
|
22268
|
-
severity: "high",
|
|
22269
|
-
argPositions: [0]
|
|
22270
|
-
},
|
|
22271
22234
|
// Deserialization
|
|
22272
22235
|
{
|
|
22273
22236
|
method: "readObject",
|
|
@@ -22826,42 +22789,17 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22826
22789
|
}
|
|
22827
22790
|
/**
|
|
22828
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.
|
|
22829
22799
|
*/
|
|
22830
22800
|
getBuiltinSinks() {
|
|
22831
22801
|
return [
|
|
22832
|
-
// Command Injection
|
|
22833
|
-
{
|
|
22834
|
-
method: "exec",
|
|
22835
|
-
class: "child_process",
|
|
22836
|
-
type: "command_injection",
|
|
22837
|
-
cwe: "CWE-78",
|
|
22838
|
-
severity: "critical",
|
|
22839
|
-
argPositions: [0]
|
|
22840
|
-
},
|
|
22841
|
-
{
|
|
22842
|
-
method: "execSync",
|
|
22843
|
-
class: "child_process",
|
|
22844
|
-
type: "command_injection",
|
|
22845
|
-
cwe: "CWE-78",
|
|
22846
|
-
severity: "critical",
|
|
22847
|
-
argPositions: [0]
|
|
22848
|
-
},
|
|
22849
|
-
{
|
|
22850
|
-
method: "spawn",
|
|
22851
|
-
class: "child_process",
|
|
22852
|
-
type: "command_injection",
|
|
22853
|
-
cwe: "CWE-78",
|
|
22854
|
-
severity: "critical",
|
|
22855
|
-
argPositions: [0, 1]
|
|
22856
|
-
},
|
|
22857
22802
|
// Code Injection
|
|
22858
|
-
{
|
|
22859
|
-
method: "eval",
|
|
22860
|
-
type: "code_injection",
|
|
22861
|
-
cwe: "CWE-94",
|
|
22862
|
-
severity: "critical",
|
|
22863
|
-
argPositions: [0]
|
|
22864
|
-
},
|
|
22865
22803
|
{
|
|
22866
22804
|
method: "Function",
|
|
22867
22805
|
type: "code_injection",
|
|
@@ -22885,46 +22823,7 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22885
22823
|
argPositions: [0]
|
|
22886
22824
|
// When first arg is string
|
|
22887
22825
|
},
|
|
22888
|
-
// Path Traversal
|
|
22889
|
-
{
|
|
22890
|
-
method: "readFileSync",
|
|
22891
|
-
class: "fs",
|
|
22892
|
-
type: "path_traversal",
|
|
22893
|
-
cwe: "CWE-22",
|
|
22894
|
-
severity: "high",
|
|
22895
|
-
argPositions: [0]
|
|
22896
|
-
},
|
|
22897
|
-
{
|
|
22898
|
-
method: "writeFileSync",
|
|
22899
|
-
class: "fs",
|
|
22900
|
-
type: "path_traversal",
|
|
22901
|
-
cwe: "CWE-22",
|
|
22902
|
-
severity: "high",
|
|
22903
|
-
argPositions: [0]
|
|
22904
|
-
},
|
|
22905
|
-
{
|
|
22906
|
-
method: "createReadStream",
|
|
22907
|
-
class: "fs",
|
|
22908
|
-
type: "path_traversal",
|
|
22909
|
-
cwe: "CWE-22",
|
|
22910
|
-
severity: "high",
|
|
22911
|
-
argPositions: [0]
|
|
22912
|
-
},
|
|
22913
22826
|
// XSS (DOM)
|
|
22914
|
-
{
|
|
22915
|
-
method: "innerHTML",
|
|
22916
|
-
type: "xss",
|
|
22917
|
-
cwe: "CWE-79",
|
|
22918
|
-
severity: "high",
|
|
22919
|
-
argPositions: [0]
|
|
22920
|
-
},
|
|
22921
|
-
{
|
|
22922
|
-
method: "outerHTML",
|
|
22923
|
-
type: "xss",
|
|
22924
|
-
cwe: "CWE-79",
|
|
22925
|
-
severity: "high",
|
|
22926
|
-
argPositions: [0]
|
|
22927
|
-
},
|
|
22928
22827
|
{
|
|
22929
22828
|
method: "document.write",
|
|
22930
22829
|
type: "xss",
|
|
@@ -22976,13 +22875,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22976
22875
|
severity: "critical",
|
|
22977
22876
|
argPositions: [0]
|
|
22978
22877
|
},
|
|
22979
|
-
{
|
|
22980
|
-
method: "raw",
|
|
22981
|
-
type: "sql_injection",
|
|
22982
|
-
cwe: "CWE-89",
|
|
22983
|
-
severity: "critical",
|
|
22984
|
-
argPositions: [0]
|
|
22985
|
-
},
|
|
22986
22878
|
// Prisma ORM - unsafe raw query methods ($executeRaw/$queryRaw with template literals are safe/parameterized)
|
|
22987
22879
|
{
|
|
22988
22880
|
method: "$executeRawUnsafe",
|
|
@@ -22998,30 +22890,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
22998
22890
|
severity: "critical",
|
|
22999
22891
|
argPositions: [0]
|
|
23000
22892
|
},
|
|
23001
|
-
// SSRF
|
|
23002
|
-
{
|
|
23003
|
-
method: "fetch",
|
|
23004
|
-
type: "ssrf",
|
|
23005
|
-
cwe: "CWE-918",
|
|
23006
|
-
severity: "high",
|
|
23007
|
-
argPositions: [0]
|
|
23008
|
-
},
|
|
23009
|
-
{
|
|
23010
|
-
method: "get",
|
|
23011
|
-
class: "axios",
|
|
23012
|
-
type: "ssrf",
|
|
23013
|
-
cwe: "CWE-918",
|
|
23014
|
-
severity: "high",
|
|
23015
|
-
argPositions: [0]
|
|
23016
|
-
},
|
|
23017
|
-
{
|
|
23018
|
-
method: "request",
|
|
23019
|
-
class: "http",
|
|
23020
|
-
type: "ssrf",
|
|
23021
|
-
cwe: "CWE-918",
|
|
23022
|
-
severity: "high",
|
|
23023
|
-
argPositions: [0]
|
|
23024
|
-
},
|
|
23025
22893
|
// NoSQL Injection
|
|
23026
22894
|
{
|
|
23027
22895
|
method: "find",
|
|
@@ -23030,13 +22898,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23030
22898
|
severity: "high",
|
|
23031
22899
|
argPositions: [0]
|
|
23032
22900
|
},
|
|
23033
|
-
{
|
|
23034
|
-
method: "findOne",
|
|
23035
|
-
type: "nosql_injection",
|
|
23036
|
-
cwe: "CWE-943",
|
|
23037
|
-
severity: "high",
|
|
23038
|
-
argPositions: [0]
|
|
23039
|
-
},
|
|
23040
22901
|
// Prototype Pollution (JS-specific)
|
|
23041
22902
|
{
|
|
23042
22903
|
method: "merge",
|
|
@@ -23130,14 +22991,9 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
23130
22991
|
// =========================================================
|
|
23131
22992
|
// Next.js Sinks
|
|
23132
22993
|
// =========================================================
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
type: "open_redirect",
|
|
23137
|
-
cwe: "CWE-601",
|
|
23138
|
-
severity: "high",
|
|
23139
|
-
argPositions: [0]
|
|
23140
|
-
},
|
|
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).
|
|
23141
22997
|
{
|
|
23142
22998
|
// Router push with user-controlled URL
|
|
23143
22999
|
method: "push",
|
|
@@ -23419,13 +23275,15 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23419
23275
|
returnTainted: true
|
|
23420
23276
|
},
|
|
23421
23277
|
// Standard library sources
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
|
|
23427
|
-
|
|
23428
|
-
|
|
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).
|
|
23429
23287
|
{
|
|
23430
23288
|
method: "argv",
|
|
23431
23289
|
class: "sys",
|
|
@@ -23441,137 +23299,28 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23441
23299
|
severity: "medium",
|
|
23442
23300
|
confidence: 0.85,
|
|
23443
23301
|
returnTainted: true
|
|
23444
|
-
},
|
|
23445
|
-
{
|
|
23446
|
-
method: "getenv",
|
|
23447
|
-
class: "os",
|
|
23448
|
-
type: "env_var",
|
|
23449
|
-
severity: "medium",
|
|
23450
|
-
confidence: 0.85,
|
|
23451
|
-
returnTainted: true
|
|
23452
|
-
},
|
|
23453
|
-
// File reading
|
|
23454
|
-
{
|
|
23455
|
-
method: "read",
|
|
23456
|
-
type: "file_input",
|
|
23457
|
-
severity: "medium",
|
|
23458
|
-
confidence: 0.8,
|
|
23459
|
-
returnTainted: true
|
|
23460
|
-
},
|
|
23461
|
-
{
|
|
23462
|
-
method: "readline",
|
|
23463
|
-
type: "file_input",
|
|
23464
|
-
severity: "medium",
|
|
23465
|
-
confidence: 0.8,
|
|
23466
|
-
returnTainted: true
|
|
23467
|
-
},
|
|
23468
|
-
{
|
|
23469
|
-
method: "readlines",
|
|
23470
|
-
type: "file_input",
|
|
23471
|
-
severity: "medium",
|
|
23472
|
-
confidence: 0.8,
|
|
23473
|
-
returnTainted: true
|
|
23474
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).
|
|
23475
23309
|
];
|
|
23476
23310
|
}
|
|
23477
23311
|
/**
|
|
23478
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.
|
|
23479
23320
|
*/
|
|
23480
23321
|
getBuiltinSinks() {
|
|
23481
23322
|
return [
|
|
23482
|
-
// Command Injection
|
|
23483
|
-
{
|
|
23484
|
-
method: "system",
|
|
23485
|
-
class: "os",
|
|
23486
|
-
type: "command_injection",
|
|
23487
|
-
cwe: "CWE-78",
|
|
23488
|
-
severity: "critical",
|
|
23489
|
-
argPositions: [0]
|
|
23490
|
-
},
|
|
23491
|
-
{
|
|
23492
|
-
method: "popen",
|
|
23493
|
-
class: "os",
|
|
23494
|
-
type: "command_injection",
|
|
23495
|
-
cwe: "CWE-78",
|
|
23496
|
-
severity: "critical",
|
|
23497
|
-
argPositions: [0]
|
|
23498
|
-
},
|
|
23499
|
-
{
|
|
23500
|
-
method: "run",
|
|
23501
|
-
class: "subprocess",
|
|
23502
|
-
type: "command_injection",
|
|
23503
|
-
cwe: "CWE-78",
|
|
23504
|
-
severity: "critical",
|
|
23505
|
-
argPositions: [0]
|
|
23506
|
-
},
|
|
23507
|
-
{
|
|
23508
|
-
method: "call",
|
|
23509
|
-
class: "subprocess",
|
|
23510
|
-
type: "command_injection",
|
|
23511
|
-
cwe: "CWE-78",
|
|
23512
|
-
severity: "critical",
|
|
23513
|
-
argPositions: [0]
|
|
23514
|
-
},
|
|
23515
|
-
{
|
|
23516
|
-
method: "Popen",
|
|
23517
|
-
class: "subprocess",
|
|
23518
|
-
type: "command_injection",
|
|
23519
|
-
cwe: "CWE-78",
|
|
23520
|
-
severity: "critical",
|
|
23521
|
-
argPositions: [0]
|
|
23522
|
-
},
|
|
23523
|
-
// Code Injection
|
|
23524
|
-
{
|
|
23525
|
-
method: "eval",
|
|
23526
|
-
type: "code_injection",
|
|
23527
|
-
cwe: "CWE-94",
|
|
23528
|
-
severity: "critical",
|
|
23529
|
-
argPositions: [0]
|
|
23530
|
-
},
|
|
23531
|
-
{
|
|
23532
|
-
method: "exec",
|
|
23533
|
-
type: "code_injection",
|
|
23534
|
-
cwe: "CWE-94",
|
|
23535
|
-
severity: "critical",
|
|
23536
|
-
argPositions: [0]
|
|
23537
|
-
},
|
|
23538
|
-
{
|
|
23539
|
-
method: "compile",
|
|
23540
|
-
type: "code_injection",
|
|
23541
|
-
cwe: "CWE-94",
|
|
23542
|
-
severity: "high",
|
|
23543
|
-
argPositions: [0]
|
|
23544
|
-
},
|
|
23545
|
-
// SQL Injection
|
|
23546
|
-
{
|
|
23547
|
-
method: "execute",
|
|
23548
|
-
type: "sql_injection",
|
|
23549
|
-
cwe: "CWE-89",
|
|
23550
|
-
severity: "critical",
|
|
23551
|
-
argPositions: [0]
|
|
23552
|
-
},
|
|
23553
|
-
{
|
|
23554
|
-
method: "executemany",
|
|
23555
|
-
type: "sql_injection",
|
|
23556
|
-
cwe: "CWE-89",
|
|
23557
|
-
severity: "critical",
|
|
23558
|
-
argPositions: [0]
|
|
23559
|
-
},
|
|
23560
|
-
{
|
|
23561
|
-
method: "raw",
|
|
23562
|
-
type: "sql_injection",
|
|
23563
|
-
cwe: "CWE-89",
|
|
23564
|
-
severity: "critical",
|
|
23565
|
-
argPositions: [0]
|
|
23566
|
-
},
|
|
23567
23323
|
// Path Traversal
|
|
23568
|
-
{
|
|
23569
|
-
method: "open",
|
|
23570
|
-
type: "path_traversal",
|
|
23571
|
-
cwe: "CWE-22",
|
|
23572
|
-
severity: "high",
|
|
23573
|
-
argPositions: [0]
|
|
23574
|
-
},
|
|
23575
23324
|
{
|
|
23576
23325
|
method: "read",
|
|
23577
23326
|
class: "pathlib",
|
|
@@ -23581,13 +23330,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23581
23330
|
argPositions: [0]
|
|
23582
23331
|
},
|
|
23583
23332
|
// XSS (template injection)
|
|
23584
|
-
{
|
|
23585
|
-
method: "Markup",
|
|
23586
|
-
type: "xss",
|
|
23587
|
-
cwe: "CWE-79",
|
|
23588
|
-
severity: "high",
|
|
23589
|
-
argPositions: [0]
|
|
23590
|
-
},
|
|
23591
23333
|
{
|
|
23592
23334
|
method: "safe",
|
|
23593
23335
|
type: "xss",
|
|
@@ -23596,22 +23338,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23596
23338
|
argPositions: [0]
|
|
23597
23339
|
},
|
|
23598
23340
|
// SSRF
|
|
23599
|
-
{
|
|
23600
|
-
method: "get",
|
|
23601
|
-
class: "requests",
|
|
23602
|
-
type: "ssrf",
|
|
23603
|
-
cwe: "CWE-918",
|
|
23604
|
-
severity: "high",
|
|
23605
|
-
argPositions: [0]
|
|
23606
|
-
},
|
|
23607
|
-
{
|
|
23608
|
-
method: "post",
|
|
23609
|
-
class: "requests",
|
|
23610
|
-
type: "ssrf",
|
|
23611
|
-
cwe: "CWE-918",
|
|
23612
|
-
severity: "high",
|
|
23613
|
-
argPositions: [0]
|
|
23614
|
-
},
|
|
23615
23341
|
{
|
|
23616
23342
|
method: "urlopen",
|
|
23617
23343
|
class: "urllib",
|
|
@@ -23639,34 +23365,11 @@ var PythonPlugin = class extends BaseLanguagePlugin {
|
|
|
23639
23365
|
argPositions: [1]
|
|
23640
23366
|
},
|
|
23641
23367
|
// Deserialization
|
|
23642
|
-
{
|
|
23643
|
-
method: "loads",
|
|
23644
|
-
class: "pickle",
|
|
23645
|
-
type: "deserialization",
|
|
23646
|
-
cwe: "CWE-502",
|
|
23647
|
-
severity: "critical",
|
|
23648
|
-
argPositions: [0]
|
|
23649
|
-
},
|
|
23650
|
-
{
|
|
23651
|
-
method: "load",
|
|
23652
|
-
class: "pickle",
|
|
23653
|
-
type: "deserialization",
|
|
23654
|
-
cwe: "CWE-502",
|
|
23655
|
-
severity: "critical",
|
|
23656
|
-
argPositions: [0]
|
|
23657
|
-
},
|
|
23658
23368
|
// NOTE: yaml.safe_load / yaml.SafeLoader are intentionally NOT sinks.
|
|
23659
23369
|
// safe_load constructs only standard scalar/list/dict types and cannot
|
|
23660
23370
|
// instantiate arbitrary Python objects, so it is not a CWE-502 sink
|
|
23661
|
-
// (unlike yaml.load
|
|
23662
|
-
|
|
23663
|
-
method: "load",
|
|
23664
|
-
class: "yaml",
|
|
23665
|
-
type: "deserialization",
|
|
23666
|
-
cwe: "CWE-502",
|
|
23667
|
-
severity: "critical",
|
|
23668
|
-
argPositions: [0]
|
|
23669
|
-
},
|
|
23371
|
+
// (unlike yaml.load — registered in DEFAULT_SINKS — and
|
|
23372
|
+
// yaml.unsafe_load / yaml.full_load below).
|
|
23670
23373
|
{
|
|
23671
23374
|
method: "unsafe_load",
|
|
23672
23375
|
class: "yaml",
|
|
@@ -23825,34 +23528,17 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23825
23528
|
getBuiltinSources() {
|
|
23826
23529
|
return [
|
|
23827
23530
|
// Actix-web request extractors
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
confidence: 0.95,
|
|
23840
|
-
returnTainted: true
|
|
23841
|
-
},
|
|
23842
|
-
{
|
|
23843
|
-
method: "Path",
|
|
23844
|
-
type: "http_path",
|
|
23845
|
-
severity: "high",
|
|
23846
|
-
confidence: 0.95,
|
|
23847
|
-
returnTainted: true
|
|
23848
|
-
},
|
|
23849
|
-
{
|
|
23850
|
-
method: "Form",
|
|
23851
|
-
type: "http_body",
|
|
23852
|
-
severity: "high",
|
|
23853
|
-
confidence: 0.95,
|
|
23854
|
-
returnTainted: true
|
|
23855
|
-
},
|
|
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.
|
|
23856
23542
|
// Standard library sources
|
|
23857
23543
|
{
|
|
23858
23544
|
method: "args",
|
|
@@ -23902,20 +23588,22 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23902
23588
|
severity: "medium",
|
|
23903
23589
|
confidence: 0.8,
|
|
23904
23590
|
returnTainted: true
|
|
23905
|
-
},
|
|
23906
|
-
// Network input
|
|
23907
|
-
{
|
|
23908
|
-
method: "read",
|
|
23909
|
-
class: "TcpStream",
|
|
23910
|
-
type: "network_input",
|
|
23911
|
-
severity: "high",
|
|
23912
|
-
confidence: 0.85,
|
|
23913
|
-
returnTainted: true
|
|
23914
23591
|
}
|
|
23592
|
+
// NOTE: `TcpStream.read` (network_input) is registered identically in
|
|
23593
|
+
// DEFAULT_SOURCES — duplicate removed in the #4 follow-up consolidation.
|
|
23915
23594
|
];
|
|
23916
23595
|
}
|
|
23917
23596
|
/**
|
|
23918
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`).
|
|
23919
23607
|
*/
|
|
23920
23608
|
getBuiltinSinks() {
|
|
23921
23609
|
return [
|
|
@@ -23928,30 +23616,7 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23928
23616
|
severity: "critical",
|
|
23929
23617
|
argPositions: [0]
|
|
23930
23618
|
},
|
|
23931
|
-
{
|
|
23932
|
-
method: "arg",
|
|
23933
|
-
class: "Command",
|
|
23934
|
-
type: "command_injection",
|
|
23935
|
-
cwe: "CWE-78",
|
|
23936
|
-
severity: "critical",
|
|
23937
|
-
argPositions: [0]
|
|
23938
|
-
},
|
|
23939
|
-
{
|
|
23940
|
-
method: "args",
|
|
23941
|
-
class: "Command",
|
|
23942
|
-
type: "command_injection",
|
|
23943
|
-
cwe: "CWE-78",
|
|
23944
|
-
severity: "critical",
|
|
23945
|
-
argPositions: [0]
|
|
23946
|
-
},
|
|
23947
23619
|
// SQL Injection
|
|
23948
|
-
{
|
|
23949
|
-
method: "execute",
|
|
23950
|
-
type: "sql_injection",
|
|
23951
|
-
cwe: "CWE-89",
|
|
23952
|
-
severity: "critical",
|
|
23953
|
-
argPositions: [0]
|
|
23954
|
-
},
|
|
23955
23620
|
{
|
|
23956
23621
|
method: "query",
|
|
23957
23622
|
type: "sql_injection",
|
|
@@ -23967,22 +23632,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
23967
23632
|
argPositions: [0]
|
|
23968
23633
|
},
|
|
23969
23634
|
// Path Traversal
|
|
23970
|
-
{
|
|
23971
|
-
method: "open",
|
|
23972
|
-
class: "File",
|
|
23973
|
-
type: "path_traversal",
|
|
23974
|
-
cwe: "CWE-22",
|
|
23975
|
-
severity: "high",
|
|
23976
|
-
argPositions: [0]
|
|
23977
|
-
},
|
|
23978
|
-
{
|
|
23979
|
-
method: "create",
|
|
23980
|
-
class: "File",
|
|
23981
|
-
type: "path_traversal",
|
|
23982
|
-
cwe: "CWE-22",
|
|
23983
|
-
severity: "high",
|
|
23984
|
-
argPositions: [0]
|
|
23985
|
-
},
|
|
23986
23635
|
{
|
|
23987
23636
|
method: "read_to_string",
|
|
23988
23637
|
class: "std::fs",
|
|
@@ -24022,40 +23671,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
|
|
|
24022
23671
|
severity: "critical",
|
|
24023
23672
|
argPositions: [0]
|
|
24024
23673
|
},
|
|
24025
|
-
// Deserialization (with serde)
|
|
24026
|
-
{
|
|
24027
|
-
method: "from_str",
|
|
24028
|
-
class: "serde_json",
|
|
24029
|
-
type: "deserialization",
|
|
24030
|
-
cwe: "CWE-502",
|
|
24031
|
-
severity: "medium",
|
|
24032
|
-
argPositions: [0]
|
|
24033
|
-
},
|
|
24034
|
-
{
|
|
24035
|
-
method: "from_slice",
|
|
24036
|
-
class: "serde_json",
|
|
24037
|
-
type: "deserialization",
|
|
24038
|
-
cwe: "CWE-502",
|
|
24039
|
-
severity: "medium",
|
|
24040
|
-
argPositions: [0]
|
|
24041
|
-
},
|
|
24042
|
-
// SSRF
|
|
24043
|
-
{
|
|
24044
|
-
method: "get",
|
|
24045
|
-
class: "reqwest",
|
|
24046
|
-
type: "ssrf",
|
|
24047
|
-
cwe: "CWE-918",
|
|
24048
|
-
severity: "high",
|
|
24049
|
-
argPositions: [0]
|
|
24050
|
-
},
|
|
24051
|
-
{
|
|
24052
|
-
method: "post",
|
|
24053
|
-
class: "reqwest",
|
|
24054
|
-
type: "ssrf",
|
|
24055
|
-
cwe: "CWE-918",
|
|
24056
|
-
severity: "high",
|
|
24057
|
-
argPositions: [0]
|
|
24058
|
-
},
|
|
24059
23674
|
// Regex DoS
|
|
24060
23675
|
{
|
|
24061
23676
|
method: "new",
|
|
@@ -24170,14 +23785,13 @@ var BashPlugin = class extends BaseLanguagePlugin {
|
|
|
24170
23785
|
*/
|
|
24171
23786
|
getBuiltinSources() {
|
|
24172
23787
|
return [
|
|
24173
|
-
// read
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
},
|
|
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.
|
|
24181
23795
|
// curl/wget output as taint sources for supply-chain attack patterns
|
|
24182
23796
|
// (e.g., curl ... | sh, $(curl ...) piped to eval/bash).
|
|
24183
23797
|
{
|
|
@@ -24203,14 +23817,11 @@ var BashPlugin = class extends BaseLanguagePlugin {
|
|
|
24203
23817
|
*/
|
|
24204
23818
|
getBuiltinSinks() {
|
|
24205
23819
|
return [
|
|
24206
|
-
//
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
severity: "critical",
|
|
24212
|
-
argPositions: [0]
|
|
24213
|
-
},
|
|
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).
|
|
24214
23825
|
// Command injection: spawning a sub-shell with -c flag
|
|
24215
23826
|
{
|
|
24216
23827
|
method: "bash",
|
|
@@ -24652,30 +24263,15 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24652
24263
|
returnTainted: true
|
|
24653
24264
|
},
|
|
24654
24265
|
// Gin framework
|
|
24655
|
-
|
|
24656
|
-
|
|
24657
|
-
|
|
24658
|
-
|
|
24659
|
-
|
|
24660
|
-
|
|
24661
|
-
|
|
24662
|
-
|
|
24663
|
-
|
|
24664
|
-
method: "Param",
|
|
24665
|
-
class: "Context",
|
|
24666
|
-
type: "http_path",
|
|
24667
|
-
severity: "high",
|
|
24668
|
-
confidence: 0.95,
|
|
24669
|
-
returnTainted: true
|
|
24670
|
-
},
|
|
24671
|
-
{
|
|
24672
|
-
method: "PostForm",
|
|
24673
|
-
class: "Context",
|
|
24674
|
-
type: "http_param",
|
|
24675
|
-
severity: "high",
|
|
24676
|
-
confidence: 0.95,
|
|
24677
|
-
returnTainted: true
|
|
24678
|
-
},
|
|
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.
|
|
24679
24275
|
{
|
|
24680
24276
|
method: "GetRawData",
|
|
24681
24277
|
class: "Context",
|
|
@@ -24721,6 +24317,10 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24721
24317
|
}
|
|
24722
24318
|
/**
|
|
24723
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.
|
|
24724
24324
|
*/
|
|
24725
24325
|
getBuiltinSinks() {
|
|
24726
24326
|
return [
|
|
@@ -24897,14 +24497,9 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
24897
24497
|
severity: "medium",
|
|
24898
24498
|
argPositions: [0]
|
|
24899
24499
|
},
|
|
24900
|
-
|
|
24901
|
-
|
|
24902
|
-
|
|
24903
|
-
type: "deserialization",
|
|
24904
|
-
cwe: "CWE-502",
|
|
24905
|
-
severity: "medium",
|
|
24906
|
-
argPositions: [0]
|
|
24907
|
-
},
|
|
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.
|
|
24908
24503
|
// Log Injection — `log.Printf("event=%s", userInput)` allows
|
|
24909
24504
|
// CRLF/log forging when the format string interpolates user data.
|
|
24910
24505
|
// CWE-117. (cognium-dev #107)
|
|
@@ -26254,6 +25849,7 @@ var LanguageSourcesPass = class {
|
|
|
26254
25849
|
additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
|
|
26255
25850
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
26256
25851
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
25852
|
+
additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
|
|
26257
25853
|
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
26258
25854
|
additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
|
|
26259
25855
|
const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
|
|
@@ -28038,6 +27634,59 @@ function findPythonSetMembershipXssGuardSanitizers(code) {
|
|
|
28038
27634
|
}
|
|
28039
27635
|
return sanitizers;
|
|
28040
27636
|
}
|
|
27637
|
+
function findPythonTraversalRejectGuardSanitizers(code) {
|
|
27638
|
+
const sanitizers = [];
|
|
27639
|
+
const lines = code.split("\n");
|
|
27640
|
+
const guardOpen = /^(\s*)if\s+(['"])(\.\.[/\\]?)\2\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
|
|
27641
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
27642
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27643
|
+
const m = guardOpen.exec(lines[i2]);
|
|
27644
|
+
if (!m) continue;
|
|
27645
|
+
const guardIndent = m[1].length;
|
|
27646
|
+
const guardedVar = m[4];
|
|
27647
|
+
let bodyHasTerminator = false;
|
|
27648
|
+
let blockEnd = -1;
|
|
27649
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
27650
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
27651
|
+
const line = lines[j];
|
|
27652
|
+
if (line.trim() === "") continue;
|
|
27653
|
+
const indent = line.length - line.trimStart().length;
|
|
27654
|
+
if (indent <= guardIndent) {
|
|
27655
|
+
blockEnd = j - 1;
|
|
27656
|
+
break;
|
|
27657
|
+
}
|
|
27658
|
+
if (terminator.test(line)) bodyHasTerminator = true;
|
|
27659
|
+
}
|
|
27660
|
+
if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
|
|
27661
|
+
if (!bodyHasTerminator) continue;
|
|
27662
|
+
const guarded = /* @__PURE__ */ new Set([guardedVar]);
|
|
27663
|
+
const assignRe = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.*)$/;
|
|
27664
|
+
const mentionsGuarded = (text) => {
|
|
27665
|
+
for (const name2 of guarded) {
|
|
27666
|
+
if (new RegExp(`\\b${name2}\\b`).test(text)) return true;
|
|
27667
|
+
}
|
|
27668
|
+
return false;
|
|
27669
|
+
};
|
|
27670
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
27671
|
+
const lineText = lines[l - 1];
|
|
27672
|
+
const assign = assignRe.exec(lineText);
|
|
27673
|
+
if (assign) {
|
|
27674
|
+
const target = assign[1];
|
|
27675
|
+
const rhs = assign[2];
|
|
27676
|
+
if (mentionsGuarded(rhs)) guarded.add(target);
|
|
27677
|
+
else if (target !== guardedVar) guarded.delete(target);
|
|
27678
|
+
}
|
|
27679
|
+
if (!mentionsGuarded(lineText)) continue;
|
|
27680
|
+
sanitizers.push({
|
|
27681
|
+
type: "python_traversal_reject_guard",
|
|
27682
|
+
method: "if",
|
|
27683
|
+
line: l,
|
|
27684
|
+
sanitizes: ["path_traversal"]
|
|
27685
|
+
});
|
|
27686
|
+
}
|
|
27687
|
+
}
|
|
27688
|
+
return sanitizers;
|
|
27689
|
+
}
|
|
28041
27690
|
function findPythonDefusedXmlSanitizers(code) {
|
|
28042
27691
|
const sanitizers = [];
|
|
28043
27692
|
const lines = code.split("\n");
|
|
@@ -28591,12 +28240,12 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
|
28591
28240
|
function findJavaPathGetFileNameSanitizers(code) {
|
|
28592
28241
|
const sanitizers = [];
|
|
28593
28242
|
const lines = code.split("\n");
|
|
28594
|
-
const
|
|
28243
|
+
const assignmentRe2 = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
28595
28244
|
const rhsHasGetFileNameRe = /\.\s*getFileName\s*\(\s*\)/;
|
|
28596
28245
|
const rhsHasPathsChainRe = /\b(?:Paths\s*\.\s*get|Path\s*\.\s*of)\s*\(/;
|
|
28597
28246
|
const candidates = [];
|
|
28598
28247
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28599
|
-
const m =
|
|
28248
|
+
const m = assignmentRe2.exec(lines[i2]);
|
|
28600
28249
|
if (!m) continue;
|
|
28601
28250
|
const rhs = m[2];
|
|
28602
28251
|
if (!rhsHasGetFileNameRe.test(rhs)) continue;
|
|
@@ -33831,6 +33480,333 @@ var DeserializationSafetyGatePass = class {
|
|
|
33831
33480
|
}
|
|
33832
33481
|
};
|
|
33833
33482
|
|
|
33483
|
+
// src/analysis/passes/prompt-injection-safety-gate-pass.ts
|
|
33484
|
+
function isDelimiterLiteral(lit) {
|
|
33485
|
+
const s = lit.trim();
|
|
33486
|
+
if (s.length === 0) return true;
|
|
33487
|
+
return /^<\/?[\w.-]+\s*\/?>$/.test(s) || /^\[\/?[\w.-]+\]$/.test(s) || /^`{1,3}[\w-]*$/.test(s) || /^"""[\w-]*$/.test(s) || /^[#*=_~-]{2,}$/.test(s) || /^<{2,}[\w-]*$/.test(s) || /^[\w-]*>{2,}$/.test(s);
|
|
33488
|
+
}
|
|
33489
|
+
function scanValue(code, start2) {
|
|
33490
|
+
let depth = 0;
|
|
33491
|
+
let str = null;
|
|
33492
|
+
let out2 = "";
|
|
33493
|
+
for (let i2 = start2; i2 < code.length; i2++) {
|
|
33494
|
+
const c = code[i2];
|
|
33495
|
+
if (str) {
|
|
33496
|
+
out2 += c;
|
|
33497
|
+
if (c === str && code[i2 - 1] !== "\\") str = null;
|
|
33498
|
+
continue;
|
|
33499
|
+
}
|
|
33500
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
33501
|
+
str = c;
|
|
33502
|
+
out2 += c;
|
|
33503
|
+
continue;
|
|
33504
|
+
}
|
|
33505
|
+
if (c === "(" || c === "[" || c === "{") {
|
|
33506
|
+
depth++;
|
|
33507
|
+
out2 += c;
|
|
33508
|
+
continue;
|
|
33509
|
+
}
|
|
33510
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
33511
|
+
if (depth === 0) break;
|
|
33512
|
+
depth--;
|
|
33513
|
+
out2 += c;
|
|
33514
|
+
continue;
|
|
33515
|
+
}
|
|
33516
|
+
if (c === "," && depth === 0) break;
|
|
33517
|
+
out2 += c;
|
|
33518
|
+
}
|
|
33519
|
+
return out2.trim();
|
|
33520
|
+
}
|
|
33521
|
+
function stringLiterals(expr) {
|
|
33522
|
+
return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
33523
|
+
}
|
|
33524
|
+
function hasIdentifier(expr) {
|
|
33525
|
+
return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " "));
|
|
33526
|
+
}
|
|
33527
|
+
function isPureLiteral(valueExpr) {
|
|
33528
|
+
const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " ");
|
|
33529
|
+
return !/[A-Za-z_$][\w$]*/.test(stripped);
|
|
33530
|
+
}
|
|
33531
|
+
function isInstructionConcat(valueExpr) {
|
|
33532
|
+
if (!valueExpr.includes("+")) return false;
|
|
33533
|
+
if (!hasIdentifier(valueExpr)) return false;
|
|
33534
|
+
const lits = stringLiterals(valueExpr);
|
|
33535
|
+
if (lits.length === 0) return false;
|
|
33536
|
+
return !lits.every(isDelimiterLiteral);
|
|
33537
|
+
}
|
|
33538
|
+
function isMixedTemplate(v) {
|
|
33539
|
+
const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
|
|
33540
|
+
if (fstr) {
|
|
33541
|
+
const body2 = fstr[2];
|
|
33542
|
+
if (/\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\{[^}]*\}/g, ""))) return true;
|
|
33543
|
+
}
|
|
33544
|
+
const tmpl = v.match(/`([^`]*)`/);
|
|
33545
|
+
if (tmpl) {
|
|
33546
|
+
const body2 = tmpl[1];
|
|
33547
|
+
if (/\$\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\$\{[^}]*\}/g, ""))) return true;
|
|
33548
|
+
}
|
|
33549
|
+
return false;
|
|
33550
|
+
}
|
|
33551
|
+
function assignmentRe(varName, flags2 = "") {
|
|
33552
|
+
return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags2);
|
|
33553
|
+
}
|
|
33554
|
+
function resolveAssignmentRHS(varName, codeLines) {
|
|
33555
|
+
const re = assignmentRe(varName);
|
|
33556
|
+
for (const line of codeLines) {
|
|
33557
|
+
const m = line.match(re);
|
|
33558
|
+
if (m && m.index !== void 0) {
|
|
33559
|
+
return scanValue(line, m.index + m[0].length);
|
|
33560
|
+
}
|
|
33561
|
+
}
|
|
33562
|
+
return void 0;
|
|
33563
|
+
}
|
|
33564
|
+
function classifyExpr(v) {
|
|
33565
|
+
if (isMixedTemplate(v)) return "unsafe";
|
|
33566
|
+
if (v.includes("+")) {
|
|
33567
|
+
if (isInstructionConcat(v)) return "unsafe";
|
|
33568
|
+
const lits = stringLiterals(v);
|
|
33569
|
+
if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v)) return "safe";
|
|
33570
|
+
return "unknown";
|
|
33571
|
+
}
|
|
33572
|
+
return "unknown";
|
|
33573
|
+
}
|
|
33574
|
+
function classifyContentPlacement(val, codeLines) {
|
|
33575
|
+
const v = val.trim();
|
|
33576
|
+
if (/^[A-Za-z_$][\w$]*$/.test(v)) {
|
|
33577
|
+
const rhs = resolveAssignmentRHS(v, codeLines);
|
|
33578
|
+
if (rhs === void 0) return "unknown";
|
|
33579
|
+
return classifyExpr(rhs);
|
|
33580
|
+
}
|
|
33581
|
+
return classifyExpr(v);
|
|
33582
|
+
}
|
|
33583
|
+
function classifyPromptCall(callCode, codeLines) {
|
|
33584
|
+
const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
|
|
33585
|
+
let m;
|
|
33586
|
+
let sawDynamic = false;
|
|
33587
|
+
let allSafe = true;
|
|
33588
|
+
while ((m = contentRe.exec(callCode)) !== null) {
|
|
33589
|
+
const val = scanValue(callCode, m.index + m[0].length);
|
|
33590
|
+
if (val === "" || isPureLiteral(val)) continue;
|
|
33591
|
+
sawDynamic = true;
|
|
33592
|
+
const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
|
|
33593
|
+
const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
|
|
33594
|
+
const role = roleM ? roleM[1].toLowerCase() : "user";
|
|
33595
|
+
if (role === "system" || role === "assistant") return "unsafe";
|
|
33596
|
+
const placement = classifyContentPlacement(val, codeLines);
|
|
33597
|
+
if (placement === "unsafe") return "unsafe";
|
|
33598
|
+
if (placement !== "safe") allSafe = false;
|
|
33599
|
+
}
|
|
33600
|
+
if (!sawDynamic) return "unknown";
|
|
33601
|
+
return allSafe ? "safe" : "unknown";
|
|
33602
|
+
}
|
|
33603
|
+
function scanRhsAcrossLines(text, start2) {
|
|
33604
|
+
let depth = 0;
|
|
33605
|
+
let str = null;
|
|
33606
|
+
let out2 = "";
|
|
33607
|
+
for (let i2 = start2; i2 < text.length; i2++) {
|
|
33608
|
+
const c = text[i2];
|
|
33609
|
+
if (str) {
|
|
33610
|
+
out2 += c;
|
|
33611
|
+
if (c === str && text[i2 - 1] !== "\\") str = null;
|
|
33612
|
+
continue;
|
|
33613
|
+
}
|
|
33614
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
33615
|
+
str = c;
|
|
33616
|
+
out2 += c;
|
|
33617
|
+
continue;
|
|
33618
|
+
}
|
|
33619
|
+
if (c === "(" || c === "[" || c === "{") {
|
|
33620
|
+
depth++;
|
|
33621
|
+
out2 += c;
|
|
33622
|
+
continue;
|
|
33623
|
+
}
|
|
33624
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
33625
|
+
if (depth === 0) break;
|
|
33626
|
+
depth--;
|
|
33627
|
+
out2 += c;
|
|
33628
|
+
continue;
|
|
33629
|
+
}
|
|
33630
|
+
if (c === "\n" && depth === 0) break;
|
|
33631
|
+
out2 += c;
|
|
33632
|
+
}
|
|
33633
|
+
return out2.trim();
|
|
33634
|
+
}
|
|
33635
|
+
function resolveBuilderRhs(varName, joined) {
|
|
33636
|
+
const re = assignmentRe(varName, "g");
|
|
33637
|
+
const m = re.exec(joined);
|
|
33638
|
+
if (m) return scanRhsAcrossLines(joined, m.index + m[0].length);
|
|
33639
|
+
return void 0;
|
|
33640
|
+
}
|
|
33641
|
+
function collectBuilderRegion(callCode, joined) {
|
|
33642
|
+
const parts2 = [];
|
|
33643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33644
|
+
const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map((x) => x[0]);
|
|
33645
|
+
let budget = 40;
|
|
33646
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
33647
|
+
const v = stack.pop();
|
|
33648
|
+
if (seen.has(v)) continue;
|
|
33649
|
+
seen.add(v);
|
|
33650
|
+
const rhs = resolveBuilderRhs(v, joined);
|
|
33651
|
+
if (rhs) {
|
|
33652
|
+
parts2.push(rhs);
|
|
33653
|
+
for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g)) stack.push(mm[0]);
|
|
33654
|
+
}
|
|
33655
|
+
}
|
|
33656
|
+
return parts2.join("\n");
|
|
33657
|
+
}
|
|
33658
|
+
function classifyPromptSink(callCode, codeLines) {
|
|
33659
|
+
const direct = classifyPromptCall(callCode, codeLines);
|
|
33660
|
+
if (direct !== "unknown") return direct;
|
|
33661
|
+
const region = collectBuilderRegion(callCode, codeLines.join("\n"));
|
|
33662
|
+
if (!region) return "unknown";
|
|
33663
|
+
return classifyPromptCall(region, codeLines);
|
|
33664
|
+
}
|
|
33665
|
+
var PromptInjectionSafetyGatePass = class {
|
|
33666
|
+
name = "prompt-injection-safety-gate";
|
|
33667
|
+
category = "security";
|
|
33668
|
+
run(ctx) {
|
|
33669
|
+
const { graph, code } = ctx;
|
|
33670
|
+
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
33671
|
+
if (sinks.length === 0) return { droppedSafe: 0 };
|
|
33672
|
+
const codeLines = code.split("\n");
|
|
33673
|
+
let droppedSafe = 0;
|
|
33674
|
+
const kept = sinks.filter((sink) => {
|
|
33675
|
+
if (sink.type !== "prompt_injection") return true;
|
|
33676
|
+
const callCode = sink.code ?? codeLines[sink.line - 1] ?? "";
|
|
33677
|
+
if (!callCode) return true;
|
|
33678
|
+
if (classifyPromptSink(callCode, codeLines) === "safe") {
|
|
33679
|
+
droppedSafe++;
|
|
33680
|
+
return false;
|
|
33681
|
+
}
|
|
33682
|
+
return true;
|
|
33683
|
+
});
|
|
33684
|
+
if (droppedSafe > 0) {
|
|
33685
|
+
sinks.length = 0;
|
|
33686
|
+
sinks.push(...kept);
|
|
33687
|
+
}
|
|
33688
|
+
return { droppedSafe };
|
|
33689
|
+
}
|
|
33690
|
+
};
|
|
33691
|
+
|
|
33692
|
+
// src/analysis/passes/speculative-prompt-param-source-pass.ts
|
|
33693
|
+
function returnOrAssignRhs(text) {
|
|
33694
|
+
const t = text.trim();
|
|
33695
|
+
const ret = t.match(/^return\s+(.+?);?$/);
|
|
33696
|
+
if (ret) return ret[1];
|
|
33697
|
+
const asg = t.match(/^[A-Za-z_$][\w$.]*\s*(?::=|(?<![=<>!])=(?!=))\s*(.+?);?$/);
|
|
33698
|
+
if (asg) return asg[1];
|
|
33699
|
+
return void 0;
|
|
33700
|
+
}
|
|
33701
|
+
function looksLikePromptText(lit) {
|
|
33702
|
+
const s = lit.trim();
|
|
33703
|
+
if (/\b(you are|you're|assistant|system prompt|follow (the )?(policy|instructions|rules)|act as|your task|instructions?\s*:|ignore (all )?previous|do not reveal|respond (to|with|as)|answer the (question|user)|you (must|should|will)|helpful (ai|assistant))\b/i.test(s)) {
|
|
33704
|
+
return true;
|
|
33705
|
+
}
|
|
33706
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
33707
|
+
return words.length >= 4 && s.length >= 20;
|
|
33708
|
+
}
|
|
33709
|
+
function literalsOf(expr) {
|
|
33710
|
+
return [...expr.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
|
|
33711
|
+
}
|
|
33712
|
+
function detectPromptConstructionFlows(codeLines, types) {
|
|
33713
|
+
const flows = [];
|
|
33714
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33715
|
+
for (const type of types) {
|
|
33716
|
+
for (const method of type.methods) {
|
|
33717
|
+
const params = (method.parameters ?? []).map((p) => ({ name: p.name, line: p.line ?? method.start_line })).filter((p) => p.name && p.name !== "_");
|
|
33718
|
+
if (params.length === 0) continue;
|
|
33719
|
+
for (let line = method.start_line; line <= method.end_line; line++) {
|
|
33720
|
+
const text = codeLines[line - 1] ?? "";
|
|
33721
|
+
const rhs = returnOrAssignRhs(text);
|
|
33722
|
+
if (!rhs || !isInstructionConcat(rhs)) continue;
|
|
33723
|
+
if (!literalsOf(rhs).some(looksLikePromptText)) continue;
|
|
33724
|
+
const param = params.find((p) => new RegExp(`\\b${p.name}\\b`).test(rhs));
|
|
33725
|
+
if (!param) continue;
|
|
33726
|
+
const key = `${param.line}|${line}`;
|
|
33727
|
+
if (seen.has(key)) continue;
|
|
33728
|
+
seen.add(key);
|
|
33729
|
+
flows.push({
|
|
33730
|
+
source_line: param.line,
|
|
33731
|
+
sink_line: line,
|
|
33732
|
+
source_type: "http_body",
|
|
33733
|
+
sink_type: "prompt_injection",
|
|
33734
|
+
path: [
|
|
33735
|
+
{ variable: param.name, line: param.line, type: "source" },
|
|
33736
|
+
{ variable: param.name, line, type: "sink" }
|
|
33737
|
+
],
|
|
33738
|
+
confidence: 1,
|
|
33739
|
+
sanitized: false
|
|
33740
|
+
});
|
|
33741
|
+
}
|
|
33742
|
+
}
|
|
33743
|
+
}
|
|
33744
|
+
return flows;
|
|
33745
|
+
}
|
|
33746
|
+
function looksLikeTextParam(type) {
|
|
33747
|
+
if (!type) return true;
|
|
33748
|
+
const t = type.trim();
|
|
33749
|
+
if (t.startsWith("*") || t.startsWith("&")) return false;
|
|
33750
|
+
if (/\b(Client|Context|Writer|Reader|Conn|DB|Logger|Handler|Server|Request|Response|Pool|Session|Engine|Service|Repository|Config)\b/.test(t)) {
|
|
33751
|
+
return false;
|
|
33752
|
+
}
|
|
33753
|
+
return /(?:^|\b)(string|str|String|text|any|object|interface\{\}|\[\]byte|\[\]string|List\[str\]|Optional\[str\])(?:\b|$)/i.test(t) || // TS unions / plain identifiers without a handle keyword default to text.
|
|
33754
|
+
/^[A-Za-z_$][\w$<>[\], |]*$/.test(t);
|
|
33755
|
+
}
|
|
33756
|
+
var SpeculativePromptParamSourcePass = class {
|
|
33757
|
+
constructor(enabled) {
|
|
33758
|
+
this.enabled = enabled;
|
|
33759
|
+
}
|
|
33760
|
+
enabled;
|
|
33761
|
+
name = "speculative-prompt-param-source";
|
|
33762
|
+
category = "security";
|
|
33763
|
+
run(ctx) {
|
|
33764
|
+
if (!this.enabled) return { added: 0 };
|
|
33765
|
+
if (!ctx.hasResult("sink-filter")) return { added: 0 };
|
|
33766
|
+
const sinkFilter = ctx.getResult("sink-filter");
|
|
33767
|
+
const { types } = ctx.graph.ir;
|
|
33768
|
+
const promptSinks = sinkFilter.sinks.filter(
|
|
33769
|
+
(s) => s.type === "prompt_injection"
|
|
33770
|
+
);
|
|
33771
|
+
if (promptSinks.length === 0) return { added: 0 };
|
|
33772
|
+
const existing = new Set(
|
|
33773
|
+
sinkFilter.sources.filter((s) => typeof s.variable === "string").map((s) => `${s.variable}:${s.line}`)
|
|
33774
|
+
);
|
|
33775
|
+
const added = [];
|
|
33776
|
+
for (const type of types) {
|
|
33777
|
+
for (const method of type.methods) {
|
|
33778
|
+
const hasPromptSink = promptSinks.some(
|
|
33779
|
+
(s) => s.line >= method.start_line && s.line <= method.end_line
|
|
33780
|
+
);
|
|
33781
|
+
if (!hasPromptSink) continue;
|
|
33782
|
+
for (const param of method.parameters) {
|
|
33783
|
+
if (!param.name || param.name === "_") continue;
|
|
33784
|
+
if (!looksLikeTextParam(param.type)) continue;
|
|
33785
|
+
const line = param.line ?? method.start_line;
|
|
33786
|
+
const key = `${param.name}:${line}`;
|
|
33787
|
+
if (existing.has(key)) continue;
|
|
33788
|
+
existing.add(key);
|
|
33789
|
+
added.push({
|
|
33790
|
+
// `http_body` reaches `prompt_injection` via the flow
|
|
33791
|
+
// generators; a generic untrusted request-shaped type.
|
|
33792
|
+
type: "http_body",
|
|
33793
|
+
location: `speculative untrusted parameter '${param.name}' in ${method.name}`,
|
|
33794
|
+
severity: "high",
|
|
33795
|
+
line,
|
|
33796
|
+
confidence: 1,
|
|
33797
|
+
variable: param.name,
|
|
33798
|
+
in_method: method.name
|
|
33799
|
+
});
|
|
33800
|
+
}
|
|
33801
|
+
}
|
|
33802
|
+
}
|
|
33803
|
+
if (added.length > 0) {
|
|
33804
|
+
sinkFilter.sources.push(...added);
|
|
33805
|
+
}
|
|
33806
|
+
return { added: added.length };
|
|
33807
|
+
}
|
|
33808
|
+
};
|
|
33809
|
+
|
|
33834
33810
|
// src/analysis/passes/cli-main-reflection-suppress-pass.ts
|
|
33835
33811
|
var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
|
|
33836
33812
|
"forName",
|
|
@@ -44077,6 +44053,9 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
44077
44053
|
if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
|
|
44078
44054
|
if (!disabledPasses.has("deserialization-safety-gate"))
|
|
44079
44055
|
pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
|
|
44056
|
+
if (!disabledPasses.has("prompt-injection-safety-gate"))
|
|
44057
|
+
pipeline.add(new PromptInjectionSafetyGatePass());
|
|
44058
|
+
pipeline.add(new SpeculativePromptParamSourcePass(options.speculativeParamSources === true));
|
|
44080
44059
|
if (!disabledPasses.has("cli-main-reflection-suppress"))
|
|
44081
44060
|
pipeline.add(new CliMainReflectionSuppressPass());
|
|
44082
44061
|
if (!disabledPasses.has("library-profile-sink-gate"))
|
|
@@ -44156,6 +44135,17 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
44156
44135
|
flows: interProc.additionalFlows,
|
|
44157
44136
|
interprocedural: interProc.interprocedural
|
|
44158
44137
|
};
|
|
44138
|
+
if (options.speculativeParamSources === true) {
|
|
44139
|
+
const existingFlowKeys = new Set(
|
|
44140
|
+
(taint.flows ?? []).map((f) => `${f.source_line}|${f.sink_line}|${f.sink_type}`)
|
|
44141
|
+
);
|
|
44142
|
+
const constructionFlows = detectPromptConstructionFlows(code.split("\n"), types).filter(
|
|
44143
|
+
(f) => !existingFlowKeys.has(`${f.source_line}|${f.sink_line}|${f.sink_type}`)
|
|
44144
|
+
);
|
|
44145
|
+
if (constructionFlows.length > 0) {
|
|
44146
|
+
taint.flows = [...taint.flows ?? [], ...constructionFlows];
|
|
44147
|
+
}
|
|
44148
|
+
}
|
|
44159
44149
|
if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
|
|
44160
44150
|
const sinkTagsByKey = /* @__PURE__ */ new Map();
|
|
44161
44151
|
for (const s of taint.sinks) {
|