circle-ir 3.123.0 → 3.125.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAC3H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqB9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0C/B,CAAC;AAiCF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,WAAW,EAAE,CAAC;IACjC,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,EAAE,cAAc,EAAE,CAAC;IACvC;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;OAGG;IACH,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAMD,qBAAa,mBAAoB,YAAW,YAAY,CAAC,qBAAqB,CAAC;IAC7E,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,qBAAqB;
|
|
1
|
+
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAC3H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqB9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0C/B,CAAC;AAiCF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,WAAW,EAAE,CAAC;IACjC,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,EAAE,cAAc,EAAE,CAAC;IACvC;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;OAGG;IACH,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAMD,qBAAa,mBAAoB,YAAW,YAAY,CAAC,qBAAqB,CAAC;IAC7E,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,qBAAqB;CAsM7C;AAgkBD,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAkG9E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAwC5G;AAED,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBjD;AAyKD,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBpG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GACpB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAmCrB;AA+MD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAsKvF;AAq0CD,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,WAAW,EAAE,CAcf;AAoLD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA+HnF;AAMD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA2BjF"}
|
|
@@ -211,6 +211,12 @@ export class LanguageSourcesPass {
|
|
|
211
211
|
if (language === 'python') {
|
|
212
212
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
213
213
|
additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
|
|
214
|
+
// Sprint 74 (#216 Pattern B): regex-allowlist wrapper functions,
|
|
215
|
+
// var-aware set-membership xss guard, and defusedxml import-alias
|
|
216
|
+
// recognition.
|
|
217
|
+
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
218
|
+
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
219
|
+
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
214
220
|
// Sprint 71 (#190): pattern-based misconfig findings for subscript/context
|
|
215
221
|
// assignment shapes (cors-wildcard-origin, xfo-csp-mismatch, tls-verify-
|
|
216
222
|
// disabled) that the language-agnostic detectors miss in Python.
|
|
@@ -230,6 +236,21 @@ export class LanguageSourcesPass {
|
|
|
230
236
|
ctx.addFinding(finding);
|
|
231
237
|
}
|
|
232
238
|
}
|
|
239
|
+
// -- JavaScript/TypeScript: Sprint 73 (#216 Pattern A + B) — ETE
|
|
240
|
+
// sanitizer-chain recognition (JSON.parse / bcrypt.hash / csv
|
|
241
|
+
// '-prefix) plus wrapper-function recognition for xss / log_injection.
|
|
242
|
+
if (language === 'javascript' || language === 'typescript' || language === 'htmljs') {
|
|
243
|
+
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
244
|
+
additionalSanitizers.push(...findJsCryptoHashSanitizers(code));
|
|
245
|
+
additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
|
|
246
|
+
additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
|
|
247
|
+
}
|
|
248
|
+
// -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
|
|
249
|
+
// fromJson recognized as ETE terminator (does not affect
|
|
250
|
+
// configured `deserialization` sinks).
|
|
251
|
+
if (language === 'java') {
|
|
252
|
+
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
253
|
+
}
|
|
233
254
|
// Sprint 70 (#151): cross-language env-secret → external-network exfiltration.
|
|
234
255
|
// Pattern findings only — no taint flow required (composed-flow shape that
|
|
235
256
|
// the engine misses because the env source and the egress sink are
|
|
@@ -2090,6 +2111,339 @@ function findPythonRangeCheckGuardSanitizers(code) {
|
|
|
2090
2111
|
}
|
|
2091
2112
|
return sanitizers;
|
|
2092
2113
|
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Detect Python regex-fullmatch allow-list wrapper functions
|
|
2116
|
+
* (cognium-dev #216 Sprint 74 Pattern B).
|
|
2117
|
+
*
|
|
2118
|
+
* Pattern recognized:
|
|
2119
|
+
*
|
|
2120
|
+
* def checked_uid(uid):
|
|
2121
|
+
* if not re.fullmatch(r"[a-zA-Z0-9_-]+", uid):
|
|
2122
|
+
* abort(400)
|
|
2123
|
+
* return uid
|
|
2124
|
+
*
|
|
2125
|
+
* Two-pass: (1) scan for `def NAME(arg):` declarations whose body
|
|
2126
|
+
* contains `if not re.fullmatch(<TIGHT_REGEX>, arg): <terminator>`
|
|
2127
|
+
* and returns `arg`; (2) for every line containing `NAME(...)` outside
|
|
2128
|
+
* the declaration, emit a sanitizer.
|
|
2129
|
+
*
|
|
2130
|
+
* "Tight" regex means a character-class allow-list that admits only
|
|
2131
|
+
* alphanumerics and a small fixed set of safe separators
|
|
2132
|
+
* ([A-Za-z0-9], \w, plus optional `-`, `_`, `.`). Such an allow-list
|
|
2133
|
+
* strips every injection metacharacter, so the sanitizer lists
|
|
2134
|
+
* ldap/xpath/sql/command/path-traversal/xss + ETE.
|
|
2135
|
+
*
|
|
2136
|
+
* Loose patterns like `.*` or `.+` are rejected (TP-controls cover
|
|
2137
|
+
* the bypass case where the wrapper return value is not what reaches
|
|
2138
|
+
* the sink).
|
|
2139
|
+
*/
|
|
2140
|
+
function findPythonRegexAllowlistWrapperSanitizers(code) {
|
|
2141
|
+
const sanitizers = [];
|
|
2142
|
+
const lines = code.split('\n');
|
|
2143
|
+
// Match `def NAME(arg):` — single argument, no default.
|
|
2144
|
+
const defOpen = /^(\s*)def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*:\s*$/;
|
|
2145
|
+
// Tight char-class allow-list: starts with `[` followed by safe ranges
|
|
2146
|
+
// and an explicit `+` / `*` / `{n,m}` quantifier; or `\w+`-style.
|
|
2147
|
+
const tightRegex = /r?["'](?:\^)?(?:\\w[+*]|\\d[+*]|\[[A-Za-z0-9_\\\-\.\s]+\][+*]|[A-Za-z0-9_\-\.]+)(?:\$)?["']/;
|
|
2148
|
+
const terminator = /\b(return\s+(?:None|"|'|\(|\[|\{|False|jsonify|make_response|redirect|abort)|raise\s+\w|abort\s*\(|sys\.exit\s*\()/;
|
|
2149
|
+
const wrappers = [];
|
|
2150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2151
|
+
const m = defOpen.exec(lines[i]);
|
|
2152
|
+
if (!m)
|
|
2153
|
+
continue;
|
|
2154
|
+
const defIndent = m[1].length;
|
|
2155
|
+
const wrapperName = m[2];
|
|
2156
|
+
const argName = m[3];
|
|
2157
|
+
// Walk the indented body of the def. Look for
|
|
2158
|
+
// `if not re.fullmatch(<tight>, <arg>): <terminator>` and `return arg`.
|
|
2159
|
+
let foundGuard = false;
|
|
2160
|
+
let returnsArg = false;
|
|
2161
|
+
let blockEnd = -1;
|
|
2162
|
+
const maxScan = Math.min(lines.length, i + 40);
|
|
2163
|
+
for (let j = i + 1; j < maxScan; j++) {
|
|
2164
|
+
const line = lines[j];
|
|
2165
|
+
if (line.trim() === '')
|
|
2166
|
+
continue;
|
|
2167
|
+
const indent = line.length - line.trimStart().length;
|
|
2168
|
+
if (indent <= defIndent) {
|
|
2169
|
+
blockEnd = j - 1;
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
if (!foundGuard) {
|
|
2173
|
+
const guard = new RegExp(`if\\s+not\\s+re\\.(?:fullmatch|match)\\s*\\(\\s*(${tightRegex.source})\\s*,\\s*${argName}\\s*\\)\\s*:`);
|
|
2174
|
+
if (guard.test(line)) {
|
|
2175
|
+
// Confirm the next deeper-indented line is a terminator.
|
|
2176
|
+
for (let k = j + 1; k < maxScan; k++) {
|
|
2177
|
+
const klin = lines[k];
|
|
2178
|
+
if (klin.trim() === '')
|
|
2179
|
+
continue;
|
|
2180
|
+
const kindent = klin.length - klin.trimStart().length;
|
|
2181
|
+
if (kindent <= indent)
|
|
2182
|
+
break;
|
|
2183
|
+
if (terminator.test(klin))
|
|
2184
|
+
foundGuard = true;
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (new RegExp(`^\\s*return\\s+${argName}\\s*$`).test(line)) {
|
|
2190
|
+
returnsArg = true;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (blockEnd === -1)
|
|
2194
|
+
blockEnd = Math.min(lines.length - 1, i + 39);
|
|
2195
|
+
if (!foundGuard || !returnsArg)
|
|
2196
|
+
continue;
|
|
2197
|
+
wrappers.push({ name: wrapperName, defLine: i, defIndent });
|
|
2198
|
+
}
|
|
2199
|
+
if (wrappers.length === 0)
|
|
2200
|
+
return sanitizers;
|
|
2201
|
+
// The kinds a tight-regex wrapper sanitizes.
|
|
2202
|
+
const wrapperKinds = [
|
|
2203
|
+
'ldap_injection',
|
|
2204
|
+
'xpath_injection',
|
|
2205
|
+
'sql_injection',
|
|
2206
|
+
'command_injection',
|
|
2207
|
+
'path_traversal',
|
|
2208
|
+
'xss',
|
|
2209
|
+
'external_taint_escape',
|
|
2210
|
+
];
|
|
2211
|
+
// Pass 2: var-aware emission. For each call of the form
|
|
2212
|
+
// `<VALIDATED> = NAME(...)`
|
|
2213
|
+
// mark every subsequent line in the same function block that
|
|
2214
|
+
// references `<VALIDATED>` as a sanitizer for the wrapper kinds.
|
|
2215
|
+
// This avoids over-suppressing unrelated tainted vars introduced
|
|
2216
|
+
// later in the same function.
|
|
2217
|
+
for (const w of wrappers) {
|
|
2218
|
+
const assignedCallRe = new RegExp(`^(\\s*)([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${w.name}\\s*\\(`);
|
|
2219
|
+
const bareCallRe = new RegExp(`\\b${w.name}\\s*\\(`);
|
|
2220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2221
|
+
if (i === w.defLine)
|
|
2222
|
+
continue;
|
|
2223
|
+
const line = lines[i];
|
|
2224
|
+
if (!bareCallRe.test(line))
|
|
2225
|
+
continue;
|
|
2226
|
+
// Skip calls inside the wrapper's own body.
|
|
2227
|
+
if (i > w.defLine) {
|
|
2228
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
2229
|
+
if (lineIndent > w.defIndent) {
|
|
2230
|
+
let stillInside = true;
|
|
2231
|
+
for (let k = w.defLine + 1; k < i; k++) {
|
|
2232
|
+
const kline = lines[k];
|
|
2233
|
+
if (kline.trim() === '')
|
|
2234
|
+
continue;
|
|
2235
|
+
const kindent = kline.length - kline.trimStart().length;
|
|
2236
|
+
if (kindent <= w.defIndent) {
|
|
2237
|
+
stillInside = false;
|
|
2238
|
+
break;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (stillInside)
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
// Emit a sanitizer at the call line itself (covers ETE on a
|
|
2246
|
+
// single-line `NAME(req.args.get(...))` shape).
|
|
2247
|
+
sanitizers.push({
|
|
2248
|
+
type: 'python_regex_allowlist_wrapper',
|
|
2249
|
+
method: w.name,
|
|
2250
|
+
line: i + 1,
|
|
2251
|
+
sanitizes: [...wrapperKinds],
|
|
2252
|
+
});
|
|
2253
|
+
// If the call result is assigned to a variable, track it and
|
|
2254
|
+
// emit sanitizers on every later line in the same function
|
|
2255
|
+
// block that mentions that variable name. End the scan when
|
|
2256
|
+
// we leave the call's enclosing block.
|
|
2257
|
+
const am = assignedCallRe.exec(line);
|
|
2258
|
+
if (!am)
|
|
2259
|
+
continue;
|
|
2260
|
+
const callIndent = am[1].length;
|
|
2261
|
+
const validated = am[2];
|
|
2262
|
+
const validatedRe = new RegExp(`\\b${validated}\\b`);
|
|
2263
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2264
|
+
const jline = lines[j];
|
|
2265
|
+
if (jline.trim() === '')
|
|
2266
|
+
continue;
|
|
2267
|
+
const jindent = jline.length - jline.trimStart().length;
|
|
2268
|
+
if (jindent < callIndent)
|
|
2269
|
+
break;
|
|
2270
|
+
if (validatedRe.test(jline)) {
|
|
2271
|
+
sanitizers.push({
|
|
2272
|
+
type: 'python_regex_allowlist_wrapper',
|
|
2273
|
+
method: w.name,
|
|
2274
|
+
line: j + 1,
|
|
2275
|
+
sanitizes: [...wrapperKinds],
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return sanitizers;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Detect Python set-membership allow-list guards that should sanitize
|
|
2285
|
+
* `xss` flows (cognium-dev #216 Sprint 74 Pattern B, SSTI fixture).
|
|
2286
|
+
*
|
|
2287
|
+
* Pattern recognized:
|
|
2288
|
+
*
|
|
2289
|
+
* t = request.args.get("t", "")
|
|
2290
|
+
* if t not in ALLOWED:
|
|
2291
|
+
* abort(403)
|
|
2292
|
+
* env.from_string("{{ " + t + " }}").render()
|
|
2293
|
+
*
|
|
2294
|
+
* The existing `findPythonNetlocAllowlistGuardSanitizers` already
|
|
2295
|
+
* handles the source pattern but cannot list `xss` as a blanket
|
|
2296
|
+
* sanitizer (over-suppression risk: a different unguarded variable
|
|
2297
|
+
* later in the same handler would be incorrectly cleared). This
|
|
2298
|
+
* detector is variable-aware: it only emits an `xss` sanitizer on
|
|
2299
|
+
* lines that actually reference the guarded variable.
|
|
2300
|
+
*/
|
|
2301
|
+
function findPythonSetMembershipXssGuardSanitizers(code) {
|
|
2302
|
+
const sanitizers = [];
|
|
2303
|
+
const lines = code.split('\n');
|
|
2304
|
+
// `if <ident> not in <ALLOWLIST>:`
|
|
2305
|
+
const guardOpen = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
|
|
2306
|
+
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
2307
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
2308
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2309
|
+
const m = guardOpen.exec(lines[i]);
|
|
2310
|
+
if (!m)
|
|
2311
|
+
continue;
|
|
2312
|
+
const guardIndent = m[1].length;
|
|
2313
|
+
const guardedVar = m[2];
|
|
2314
|
+
const allowName = m[3];
|
|
2315
|
+
if (!allowlistName.test(allowName))
|
|
2316
|
+
continue;
|
|
2317
|
+
let bodyHasTerminator = false;
|
|
2318
|
+
let blockEnd = -1;
|
|
2319
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
2320
|
+
for (let j = i + 1; j < maxScan; j++) {
|
|
2321
|
+
const line = lines[j];
|
|
2322
|
+
if (line.trim() === '')
|
|
2323
|
+
continue;
|
|
2324
|
+
const indent = line.length - line.trimStart().length;
|
|
2325
|
+
if (indent <= guardIndent) {
|
|
2326
|
+
blockEnd = j - 1;
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
if (terminator.test(line))
|
|
2330
|
+
bodyHasTerminator = true;
|
|
2331
|
+
}
|
|
2332
|
+
if (blockEnd === -1)
|
|
2333
|
+
blockEnd = Math.min(lines.length - 1, i + 25);
|
|
2334
|
+
if (!bodyHasTerminator)
|
|
2335
|
+
continue;
|
|
2336
|
+
// Var-aware emission: only lines that reference the guarded var.
|
|
2337
|
+
const varRe = new RegExp(`\\b${guardedVar}\\b`);
|
|
2338
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
2339
|
+
const lineText = lines[l - 1];
|
|
2340
|
+
if (!varRe.test(lineText))
|
|
2341
|
+
continue;
|
|
2342
|
+
sanitizers.push({
|
|
2343
|
+
type: 'python_set_membership_xss_guard',
|
|
2344
|
+
method: 'if',
|
|
2345
|
+
line: l,
|
|
2346
|
+
sanitizes: ['xss', 'external_taint_escape'],
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return sanitizers;
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Detect Python `defusedxml` import-alias usage as xxe sanitizer
|
|
2354
|
+
* (cognium-dev #216 Sprint 74 Pattern B).
|
|
2355
|
+
*
|
|
2356
|
+
* defusedxml is the canonical Python defense against XML External
|
|
2357
|
+
* Entity (CWE-611) attacks — its parsers reject DTD and entity
|
|
2358
|
+
* expansion by design. Recognize three import shapes:
|
|
2359
|
+
*
|
|
2360
|
+
* import defusedxml.ElementTree as ET
|
|
2361
|
+
* from defusedxml.ElementTree import fromstring
|
|
2362
|
+
* import defusedxml as DX
|
|
2363
|
+
*
|
|
2364
|
+
* For every line that calls `<alias>.<method>(...)` (module-alias
|
|
2365
|
+
* shape) or bare `<name>(...)` (from-import shape) emit a sanitizer
|
|
2366
|
+
* with `sanitizes: ['xxe', 'external_taint_escape']`.
|
|
2367
|
+
*/
|
|
2368
|
+
function findPythonDefusedXmlSanitizers(code) {
|
|
2369
|
+
const sanitizers = [];
|
|
2370
|
+
const lines = code.split('\n');
|
|
2371
|
+
// Aliases bound to a defusedxml.* module (or defusedxml itself).
|
|
2372
|
+
const moduleAliases = new Set();
|
|
2373
|
+
// Names imported FROM defusedxml.*.
|
|
2374
|
+
const fromNames = new Set();
|
|
2375
|
+
const importAs = /^\s*import\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+as\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/;
|
|
2376
|
+
const importBare = /^\s*import\s+defusedxml(?:\.([A-Za-z_][A-Za-z0-9_.]*))?\s*$/;
|
|
2377
|
+
const fromImport = /^\s*from\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+import\s+(.+)$/;
|
|
2378
|
+
for (const raw of lines) {
|
|
2379
|
+
const line = raw.replace(/#.*$/, '');
|
|
2380
|
+
let m;
|
|
2381
|
+
if ((m = importAs.exec(line))) {
|
|
2382
|
+
moduleAliases.add(m[1]);
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if ((m = importBare.exec(line))) {
|
|
2386
|
+
// `import defusedxml.ElementTree` → callers use full
|
|
2387
|
+
// `defusedxml.ElementTree.fromstring`. Bind the leaf segment
|
|
2388
|
+
// when present, else `defusedxml`.
|
|
2389
|
+
if (m[1]) {
|
|
2390
|
+
moduleAliases.add(m[1].split('.').pop());
|
|
2391
|
+
}
|
|
2392
|
+
moduleAliases.add('defusedxml');
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if ((m = fromImport.exec(line))) {
|
|
2396
|
+
const items = m[1].split(',');
|
|
2397
|
+
for (const item of items) {
|
|
2398
|
+
const trimmed = item.trim().replace(/[()\\]/g, '');
|
|
2399
|
+
if (!trimmed)
|
|
2400
|
+
continue;
|
|
2401
|
+
const asMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
2402
|
+
if (asMatch) {
|
|
2403
|
+
fromNames.add(asMatch[2]);
|
|
2404
|
+
}
|
|
2405
|
+
else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
|
|
2406
|
+
fromNames.add(trimmed);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (moduleAliases.size === 0 && fromNames.size === 0)
|
|
2412
|
+
return sanitizers;
|
|
2413
|
+
// Emit per-line sanitizers at each call site.
|
|
2414
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2415
|
+
const line = lines[i];
|
|
2416
|
+
// Skip import lines themselves.
|
|
2417
|
+
if (/^\s*(?:import|from)\s+/.test(line))
|
|
2418
|
+
continue;
|
|
2419
|
+
let matched = false;
|
|
2420
|
+
for (const alias of moduleAliases) {
|
|
2421
|
+
const re = new RegExp(`\\b${alias}\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*\\s*\\(`);
|
|
2422
|
+
if (re.test(line)) {
|
|
2423
|
+
matched = true;
|
|
2424
|
+
break;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
if (!matched) {
|
|
2428
|
+
for (const name of fromNames) {
|
|
2429
|
+
const re = new RegExp(`\\b${name}\\s*\\(`);
|
|
2430
|
+
if (re.test(line)) {
|
|
2431
|
+
matched = true;
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (!matched)
|
|
2437
|
+
continue;
|
|
2438
|
+
sanitizers.push({
|
|
2439
|
+
type: 'python_defusedxml_import',
|
|
2440
|
+
method: 'defusedxml',
|
|
2441
|
+
line: i + 1,
|
|
2442
|
+
sanitizes: ['xxe', 'external_taint_escape'],
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
return sanitizers;
|
|
2446
|
+
}
|
|
2093
2447
|
/**
|
|
2094
2448
|
* Detect Rust HashSet/HashMap host allow-list guards (cognium-dev #115 FP-23).
|
|
2095
2449
|
*
|
|
@@ -2238,6 +2592,179 @@ function findRustCanonicalizeGuardSanitizers(code) {
|
|
|
2238
2592
|
return sanitizers;
|
|
2239
2593
|
}
|
|
2240
2594
|
// ---------------------------------------------------------------------------
|
|
2595
|
+
// Sprint 73 (#216 Pattern A + B) — JS/Java safe-handler sanitizer
|
|
2596
|
+
// recognition for the synthetic `external_taint_escape` (CWE-668) fallback
|
|
2597
|
+
// flow, plus JS user-defined wrapper-function recognition for `xss` /
|
|
2598
|
+
// `log_injection`.
|
|
2599
|
+
//
|
|
2600
|
+
// Suppression mechanism: `taint-propagation-pass.ts:198-232` already
|
|
2601
|
+
// filters ETE flows when ANY sanitizer between source and sink lists
|
|
2602
|
+
// `external_taint_escape` in its `sanitizes` array. These detectors emit
|
|
2603
|
+
// the sanitizer entries so the existing filter can do its job.
|
|
2604
|
+
//
|
|
2605
|
+
// Conservative scoping: each detector restricts `sanitizes` to the
|
|
2606
|
+
// minimum justifiable set so configured (real) sinks are unaffected.
|
|
2607
|
+
// ---------------------------------------------------------------------------
|
|
2608
|
+
/**
|
|
2609
|
+
* Java: Jackson `mapper.readValue(...)` and Gson `gson.fromJson(...)`
|
|
2610
|
+
* construct typed POJOs from JSON. Without `enableDefaultTyping` they
|
|
2611
|
+
* cannot instantiate attacker-chosen classes, so the synthetic ETE
|
|
2612
|
+
* fallback over-fires. The configured `deserialization` sink remains
|
|
2613
|
+
* unaffected (different sink_type, gated at the sink line).
|
|
2614
|
+
*/
|
|
2615
|
+
function findJavaSafeJsonParseSanitizers(code) {
|
|
2616
|
+
const sanitizers = [];
|
|
2617
|
+
const lines = code.split('\n');
|
|
2618
|
+
const safeParse = /\.(?:readValue|fromJson)\s*\(/;
|
|
2619
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2620
|
+
if (!safeParse.test(lines[i]))
|
|
2621
|
+
continue;
|
|
2622
|
+
sanitizers.push({
|
|
2623
|
+
type: 'java_safe_json_parse',
|
|
2624
|
+
method: 'readValue',
|
|
2625
|
+
line: i + 1,
|
|
2626
|
+
sanitizes: ['external_taint_escape'],
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return sanitizers;
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* JS: `JSON.parse(...)` produces only primitives / plain objects. It
|
|
2633
|
+
* cannot execute code (unlike `eval` / `Function` / `vm.runInNewContext`,
|
|
2634
|
+
* which remain configured sinks).
|
|
2635
|
+
*/
|
|
2636
|
+
function findJsSafeJsonParseSanitizers(code) {
|
|
2637
|
+
const sanitizers = [];
|
|
2638
|
+
const lines = code.split('\n');
|
|
2639
|
+
const safeParse = /\bJSON\.parse\s*\(/;
|
|
2640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2641
|
+
if (!safeParse.test(lines[i]))
|
|
2642
|
+
continue;
|
|
2643
|
+
sanitizers.push({
|
|
2644
|
+
type: 'js_safe_json_parse',
|
|
2645
|
+
method: 'JSON.parse',
|
|
2646
|
+
line: i + 1,
|
|
2647
|
+
sanitizes: ['external_taint_escape'],
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
return sanitizers;
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* JS: one-way crypto hashes destroy original content. Bcrypt/argon2/
|
|
2654
|
+
* scrypt/createHash...digest cannot be reversed. The `weak-password-hash`
|
|
2655
|
+
* rule fires independently for MD5/SHA1 algorithms.
|
|
2656
|
+
*/
|
|
2657
|
+
function findJsCryptoHashSanitizers(code) {
|
|
2658
|
+
const sanitizers = [];
|
|
2659
|
+
const lines = code.split('\n');
|
|
2660
|
+
// bcrypt.hash(...) / bcrypt.hashSync(...)
|
|
2661
|
+
// argon2.hash(...)
|
|
2662
|
+
// crypto.scrypt(...) / crypto.scryptSync(...)
|
|
2663
|
+
// crypto.createHash(...) / hash.digest(...)
|
|
2664
|
+
const hashCall = /\b(?:bcrypt|argon2)\s*\.\s*hash(?:Sync)?\s*\(|\bcrypto\s*\.\s*(?:scrypt(?:Sync)?|createHash)\s*\(|\.digest\s*\(/;
|
|
2665
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2666
|
+
if (!hashCall.test(lines[i]))
|
|
2667
|
+
continue;
|
|
2668
|
+
sanitizers.push({
|
|
2669
|
+
type: 'js_crypto_hash',
|
|
2670
|
+
method: 'hash',
|
|
2671
|
+
line: i + 1,
|
|
2672
|
+
sanitizes: ['external_taint_escape'],
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
return sanitizers;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* JS: Excel-formula-injection sanitizer. Prepending a literal single
|
|
2679
|
+
* quote (`'`) to a CSV cell prevents Excel/LibreOffice from interpreting
|
|
2680
|
+
* the value as a formula. Scoped to ETE only — the `'` prefix does NOT
|
|
2681
|
+
* mitigate xss, command_injection, sql_injection, etc.
|
|
2682
|
+
*/
|
|
2683
|
+
function findJsCsvFormulaPrefixSanitizers(code) {
|
|
2684
|
+
const sanitizers = [];
|
|
2685
|
+
const lines = code.split('\n');
|
|
2686
|
+
// Template literal `'${x}...` — leading `'` inside backticks.
|
|
2687
|
+
const tickPrefix = /`'\$\{/;
|
|
2688
|
+
// Concat form "'" + x or '\'' + x
|
|
2689
|
+
const concatPrefix = /["']\\?'["']\s*\+\s*[A-Za-z_]/;
|
|
2690
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2691
|
+
if (tickPrefix.test(lines[i]) || concatPrefix.test(lines[i])) {
|
|
2692
|
+
sanitizers.push({
|
|
2693
|
+
type: 'js_csv_formula_prefix',
|
|
2694
|
+
method: "'-prefix",
|
|
2695
|
+
line: i + 1,
|
|
2696
|
+
sanitizes: ['external_taint_escape'],
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return sanitizers;
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* JS: user-defined wrapper functions that perform a `.replace(...)`
|
|
2704
|
+
* against a recognizable threat-character class. Two-pass:
|
|
2705
|
+
*
|
|
2706
|
+
* 1. Discover wrappers — `function NAME(...) { ... .replace(/.../...) }`
|
|
2707
|
+
* or arrow form `const NAME = (...) => { ... .replace(...) }`.
|
|
2708
|
+
* Classify by the character class:
|
|
2709
|
+
* - `[&<>"']` → xss + external_taint_escape
|
|
2710
|
+
* - `[\r\n\t]` → log_injection + external_taint_escape
|
|
2711
|
+
* 2. For every call site `NAME(...)`, emit a sanitizer at that line
|
|
2712
|
+
* listing the wrapper's kinds.
|
|
2713
|
+
*
|
|
2714
|
+
* Conservative: requires explicit `.replace` against a recognizable
|
|
2715
|
+
* threat-set. Custom wrappers using different escape strategies (e.g.
|
|
2716
|
+
* URL encode) are not yet covered.
|
|
2717
|
+
*/
|
|
2718
|
+
function findJsWrapperFunctionSanitizers(code) {
|
|
2719
|
+
const sanitizers = [];
|
|
2720
|
+
const lines = code.split('\n');
|
|
2721
|
+
const wrappers = new Map();
|
|
2722
|
+
const fnDeclRe = /\bfunction\s+([A-Za-z_]\w*)\s*\(/;
|
|
2723
|
+
const arrowDeclRe = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:\([^)]*\)|[A-Za-z_]\w*)\s*=>/;
|
|
2724
|
+
// Threat-char classes — must appear inside a .replace(/.../, ...) call.
|
|
2725
|
+
const xssCharClass = /\.replace\s*\(\s*\/[^/]*[&<>"'][^/]*\//;
|
|
2726
|
+
const crlfCharClass = /\.replace\s*\(\s*\/[^/]*(?:\\r|\\n|\\t)[^/]*\//;
|
|
2727
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2728
|
+
const m = fnDeclRe.exec(lines[i]) ?? arrowDeclRe.exec(lines[i]);
|
|
2729
|
+
if (!m)
|
|
2730
|
+
continue;
|
|
2731
|
+
const name = m[1];
|
|
2732
|
+
// Scan up to 8 lines for the body's .replace(...) call.
|
|
2733
|
+
const body = lines.slice(i, Math.min(lines.length, i + 8)).join('\n');
|
|
2734
|
+
const kinds = new Set();
|
|
2735
|
+
if (xssCharClass.test(body)) {
|
|
2736
|
+
kinds.add('xss');
|
|
2737
|
+
kinds.add('external_taint_escape');
|
|
2738
|
+
}
|
|
2739
|
+
if (crlfCharClass.test(body)) {
|
|
2740
|
+
kinds.add('log_injection');
|
|
2741
|
+
kinds.add('external_taint_escape');
|
|
2742
|
+
}
|
|
2743
|
+
if (kinds.size > 0)
|
|
2744
|
+
wrappers.set(name, kinds);
|
|
2745
|
+
}
|
|
2746
|
+
if (wrappers.size === 0)
|
|
2747
|
+
return sanitizers;
|
|
2748
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2749
|
+
for (const [name, kinds] of wrappers) {
|
|
2750
|
+
// Skip the declaration line itself.
|
|
2751
|
+
const declSelf = new RegExp(`\\b(?:function|const|let|var)\\s+${name}\\b`);
|
|
2752
|
+
if (declSelf.test(lines[i]))
|
|
2753
|
+
continue;
|
|
2754
|
+
const callRe = new RegExp(`\\b${name}\\s*\\(`);
|
|
2755
|
+
if (!callRe.test(lines[i]))
|
|
2756
|
+
continue;
|
|
2757
|
+
sanitizers.push({
|
|
2758
|
+
type: 'js_wrapper_function',
|
|
2759
|
+
method: name,
|
|
2760
|
+
line: i + 1,
|
|
2761
|
+
sanitizes: [...kinds],
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return sanitizers;
|
|
2766
|
+
}
|
|
2767
|
+
// ---------------------------------------------------------------------------
|
|
2241
2768
|
// Sprint 70 (#151) — external-secret-exfiltration composed-flow detection.
|
|
2242
2769
|
//
|
|
2243
2770
|
// Models the trust-corpus FN-TQ-01 shape: an env-read secret variable that
|