circle-ir 3.132.0 → 3.133.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/dist/analysis/passes/language-sources-pass.d.ts +39 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +566 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +438 -0
- package/package.json +1 -1
|
@@ -321,4 +321,43 @@ export declare function findRustAppendHeaderTupleOpenRedirectFindings(code: stri
|
|
|
321
321
|
* never fires on literal-only assignments.
|
|
322
322
|
*/
|
|
323
323
|
export declare function findJsDomOpenRedirectFindings(code: string, file: string): SastFinding[];
|
|
324
|
+
/**
|
|
325
|
+
* Sprint 83 detector A — Go `plugin.Open(<tainted>)` / `plugin.Lookup(...)`.
|
|
326
|
+
* Loading a Go plugin executes the loaded module's init() functions and
|
|
327
|
+
* makes its exported symbols callable, which is a code-injection sink
|
|
328
|
+
* equivalent to dynamic library loading. Fires when the path argument
|
|
329
|
+
* traces back to an `*http.Request` extractor (FormValue / URL.Query /
|
|
330
|
+
* PostFormValue / Header.Get / Cookie) within the same function.
|
|
331
|
+
*/
|
|
332
|
+
export declare function findGoPluginOpenCodeInjectionFindings(code: string, file: string): SastFinding[];
|
|
333
|
+
/**
|
|
334
|
+
* Sprint 83 detector B — JS indirect eval forms.
|
|
335
|
+
* Configured `eval` sink matches direct `eval(x)` but misses:
|
|
336
|
+
* - `(0, eval)(x)` comma-operator indirect call
|
|
337
|
+
* - `globalThis.eval(x)` / `global.eval(x)` / `window.eval(x)` / `self.eval(x)`
|
|
338
|
+
* - aliased eval: `const f = eval; f(x)` then `f(taint)`
|
|
339
|
+
* Fires when the argument traces back to req.body / req.query / req.params /
|
|
340
|
+
* req.headers / req.cookies (Express/Koa-style request extractor).
|
|
341
|
+
*/
|
|
342
|
+
export declare function findJsIndirectEvalCodeInjectionFindings(code: string, file: string): SastFinding[];
|
|
343
|
+
/**
|
|
344
|
+
* Sprint 83 detector C — Python `code` stdlib REPL / compile_command.
|
|
345
|
+
* `code.InteractiveInterpreter().runsource(s)`, `runcode(c)`, `push(line)` and
|
|
346
|
+
* `code.InteractiveConsole().push(line)`, plus `code.compile_command(s)` —
|
|
347
|
+
* all execute or compile arbitrary Python source. Fires when the argument
|
|
348
|
+
* traces back to a Flask `request.*` extractor; gated on `import code`
|
|
349
|
+
* to avoid colliding with user-defined `code` variables.
|
|
350
|
+
*/
|
|
351
|
+
export declare function findPythonInteractiveInterpreterCodeInjectionFindings(code: string, file: string): SastFinding[];
|
|
352
|
+
/**
|
|
353
|
+
* Sprint 83 detector D — Rust eval-crate / dynamic-load sinks.
|
|
354
|
+
* Rust has no language-level eval; the canonical sinks are:
|
|
355
|
+
* - `evalexpr::eval(...)` (and `_with_context|_boolean|_int|_float|_string|_tuple|_empty`)
|
|
356
|
+
* - `libloading::Library::new(...)` (dynamic library load)
|
|
357
|
+
* - `mlua::Lua::new().load(<src>).{exec,eval,call}(...)` / rlua equivalent
|
|
358
|
+
* Fires when the argument traces back to an Actix-web extractor: a plain
|
|
359
|
+
* `body: String|Bytes`, a `web::Query<T>` / `web::Path<T>` / `web::Form<T>` /
|
|
360
|
+
* `web::Json<T>` / `HttpRequest` param, or `axum::body::Bytes`/`String` etc.
|
|
361
|
+
*/
|
|
362
|
+
export declare function findRustEvalCrateCodeInjectionFindings(code: string, file: string): SastFinding[];
|
|
324
363
|
//# sourceMappingURL=language-sources-pass.d.ts.map
|
|
@@ -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":"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"}
|
|
@@ -219,6 +219,10 @@ export class LanguageSourcesPass {
|
|
|
219
219
|
for (const finding of findGoLocationHeaderOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
220
220
|
ctx.addFinding(finding);
|
|
221
221
|
}
|
|
222
|
+
// Sprint 83 (#189): Go code_injection — plugin.Open / plugin.Lookup.
|
|
223
|
+
for (const finding of findGoPluginOpenCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
224
|
+
ctx.addFinding(finding);
|
|
225
|
+
}
|
|
222
226
|
}
|
|
223
227
|
// -- Python: safe-handler sanitizer detectors (cognium-dev #114 Sprint 31) --
|
|
224
228
|
if (language === 'python') {
|
|
@@ -252,6 +256,11 @@ export class LanguageSourcesPass {
|
|
|
252
256
|
for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
253
257
|
ctx.addFinding(finding);
|
|
254
258
|
}
|
|
259
|
+
// Sprint 83 (#189): Python code_injection — code.InteractiveInterpreter
|
|
260
|
+
// / InteractiveConsole / compile_command on Flask request input.
|
|
261
|
+
for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
262
|
+
ctx.addFinding(finding);
|
|
263
|
+
}
|
|
255
264
|
}
|
|
256
265
|
// -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
|
|
257
266
|
if (language === 'rust') {
|
|
@@ -285,6 +294,11 @@ export class LanguageSourcesPass {
|
|
|
285
294
|
for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
286
295
|
ctx.addFinding(finding);
|
|
287
296
|
}
|
|
297
|
+
// Sprint 83 (#189): Rust code_injection — evalexpr crate, libloading,
|
|
298
|
+
// mlua/rlua dynamic-load shapes on Actix/Axum request bodies.
|
|
299
|
+
for (const finding of findRustEvalCrateCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
300
|
+
ctx.addFinding(finding);
|
|
301
|
+
}
|
|
288
302
|
}
|
|
289
303
|
// -- JavaScript/TypeScript: Sprint 73 (#216 Pattern A + B) — ETE
|
|
290
304
|
// sanitizer-chain recognition (JSON.parse / bcrypt.hash / csv
|
|
@@ -318,6 +332,11 @@ export class LanguageSourcesPass {
|
|
|
318
332
|
for (const finding of findJsDomOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
319
333
|
ctx.addFinding(finding);
|
|
320
334
|
}
|
|
335
|
+
// Sprint 83 (#189): JS code_injection — indirect eval forms
|
|
336
|
+
// ((0, eval)(x), globalThis.eval(x), aliased eval).
|
|
337
|
+
for (const finding of findJsIndirectEvalCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
338
|
+
ctx.addFinding(finding);
|
|
339
|
+
}
|
|
321
340
|
}
|
|
322
341
|
// -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
|
|
323
342
|
// fromJson recognized as ETE terminator (does not affect
|
|
@@ -5135,4 +5154,551 @@ export function findJsDomOpenRedirectFindings(code, file) {
|
|
|
5135
5154
|
}
|
|
5136
5155
|
return out;
|
|
5137
5156
|
}
|
|
5157
|
+
// ---------------------------------------------------------------------------
|
|
5158
|
+
// Sprint 83 (issue #189 — code_injection cluster, 8 FN cells)
|
|
5159
|
+
// ---------------------------------------------------------------------------
|
|
5160
|
+
// Per-language pattern detectors that close 4 remaining code_injection FN
|
|
5161
|
+
// shapes that the configured-sink path does not cover:
|
|
5162
|
+
// - Go plugin.Open / plugin.Lookup with *http.Request-derived path
|
|
5163
|
+
// - JS indirect eval forms: (0, eval)(x), globalThis.eval(x), aliased eval
|
|
5164
|
+
// - Python code.InteractiveInterpreter / InteractiveConsole / compile_command
|
|
5165
|
+
// - Rust evalexpr crate, libloading dynamic load, mlua/rlua .load().exec
|
|
5166
|
+
// ---------------------------------------------------------------------------
|
|
5167
|
+
/**
|
|
5168
|
+
* Sprint 83 detector A — Go `plugin.Open(<tainted>)` / `plugin.Lookup(...)`.
|
|
5169
|
+
* Loading a Go plugin executes the loaded module's init() functions and
|
|
5170
|
+
* makes its exported symbols callable, which is a code-injection sink
|
|
5171
|
+
* equivalent to dynamic library loading. Fires when the path argument
|
|
5172
|
+
* traces back to an `*http.Request` extractor (FormValue / URL.Query /
|
|
5173
|
+
* PostFormValue / Header.Get / Cookie) within the same function.
|
|
5174
|
+
*/
|
|
5175
|
+
export function findGoPluginOpenCodeInjectionFindings(code, file) {
|
|
5176
|
+
const findings = [];
|
|
5177
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
5178
|
+
return findings;
|
|
5179
|
+
if (!/\bplugin\s*\.\s*(?:Open|Lookup)\s*\(/.test(code))
|
|
5180
|
+
return findings;
|
|
5181
|
+
const lines = code.split('\n');
|
|
5182
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\.Query\(\)\.Get|Header\.Get|Cookie)\s*\(/;
|
|
5183
|
+
const httpReqParamRe = /\*\s*http\.Request\b/;
|
|
5184
|
+
const callRe = /\bplugin\s*\.\s*(?:Open|Lookup)\s*\(\s*([^)]*)\s*\)/;
|
|
5185
|
+
const sinkLabel = (op) => op === 'Lookup'
|
|
5186
|
+
? 'Go plugin.Lookup'
|
|
5187
|
+
: 'Go plugin.Open';
|
|
5188
|
+
const funcs = [];
|
|
5189
|
+
let cur = null;
|
|
5190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5191
|
+
const t = lines[i].trim();
|
|
5192
|
+
if (/^func\b/.test(t)) {
|
|
5193
|
+
if (cur) {
|
|
5194
|
+
cur.end = i - 1;
|
|
5195
|
+
funcs.push(cur);
|
|
5196
|
+
}
|
|
5197
|
+
cur = { start: i, end: lines.length - 1 };
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
if (cur)
|
|
5201
|
+
funcs.push(cur);
|
|
5202
|
+
for (const fn of funcs) {
|
|
5203
|
+
const header = lines[fn.start];
|
|
5204
|
+
if (!httpReqParamRe.test(header))
|
|
5205
|
+
continue;
|
|
5206
|
+
const taintedVars = new Set();
|
|
5207
|
+
// Up to 3 passes: discover transitively-tainted vars from request extractors.
|
|
5208
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
5209
|
+
const before = taintedVars.size;
|
|
5210
|
+
for (let i = fn.start; i <= fn.end; i++) {
|
|
5211
|
+
const line = lines[i];
|
|
5212
|
+
const trimmed = line.trim();
|
|
5213
|
+
const assignMatch = trimmed.match(/^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/);
|
|
5214
|
+
if (!assignMatch)
|
|
5215
|
+
continue;
|
|
5216
|
+
const lhs = assignMatch[1];
|
|
5217
|
+
const rhs = assignMatch[2];
|
|
5218
|
+
if (taintedVars.has(lhs))
|
|
5219
|
+
continue;
|
|
5220
|
+
if (reqExtractRe.test(rhs)) {
|
|
5221
|
+
taintedVars.add(lhs);
|
|
5222
|
+
continue;
|
|
5223
|
+
}
|
|
5224
|
+
// Aliasing existing taint
|
|
5225
|
+
for (const v of taintedVars) {
|
|
5226
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5227
|
+
if (re.test(rhs)) {
|
|
5228
|
+
taintedVars.add(lhs);
|
|
5229
|
+
break;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
if (taintedVars.size === before)
|
|
5234
|
+
break;
|
|
5235
|
+
}
|
|
5236
|
+
if (taintedVars.size === 0)
|
|
5237
|
+
continue;
|
|
5238
|
+
for (let i = fn.start; i <= fn.end; i++) {
|
|
5239
|
+
const line = lines[i];
|
|
5240
|
+
const m = line.match(callRe);
|
|
5241
|
+
if (!m)
|
|
5242
|
+
continue;
|
|
5243
|
+
const arg = m[1].trim();
|
|
5244
|
+
if (arg.length === 0)
|
|
5245
|
+
continue;
|
|
5246
|
+
// skip clear literal-only constants
|
|
5247
|
+
if (/^"[^"]*"$/.test(arg))
|
|
5248
|
+
continue;
|
|
5249
|
+
let tainted = false;
|
|
5250
|
+
// direct extractor call as argument
|
|
5251
|
+
if (reqExtractRe.test(arg))
|
|
5252
|
+
tainted = true;
|
|
5253
|
+
else {
|
|
5254
|
+
for (const v of taintedVars) {
|
|
5255
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5256
|
+
if (re.test(arg)) {
|
|
5257
|
+
tainted = true;
|
|
5258
|
+
break;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
if (!tainted)
|
|
5263
|
+
continue;
|
|
5264
|
+
const op = /\bplugin\s*\.\s*Lookup\b/.test(line) ? 'Lookup' : 'Open';
|
|
5265
|
+
findings.push({
|
|
5266
|
+
id: `code_injection-${file}-${i + 1}-go-plugin-${op.toLowerCase()}`,
|
|
5267
|
+
pass: 'language-sources',
|
|
5268
|
+
category: 'security',
|
|
5269
|
+
rule_id: 'code_injection',
|
|
5270
|
+
cwe: 'CWE-94',
|
|
5271
|
+
severity: 'critical',
|
|
5272
|
+
level: 'error',
|
|
5273
|
+
message: `Code injection: ${sinkLabel(op)} called with a path/symbol derived ` +
|
|
5274
|
+
'from an *http.Request without an allow-list. Loading a plugin ' +
|
|
5275
|
+
'runs its init() and exposes arbitrary exported symbols. Restrict ' +
|
|
5276
|
+
'the path to a trusted directory or use a fixed allow-list.',
|
|
5277
|
+
file,
|
|
5278
|
+
line: i + 1,
|
|
5279
|
+
snippet: line.trim(),
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return findings;
|
|
5284
|
+
}
|
|
5285
|
+
/**
|
|
5286
|
+
* Sprint 83 detector B — JS indirect eval forms.
|
|
5287
|
+
* Configured `eval` sink matches direct `eval(x)` but misses:
|
|
5288
|
+
* - `(0, eval)(x)` comma-operator indirect call
|
|
5289
|
+
* - `globalThis.eval(x)` / `global.eval(x)` / `window.eval(x)` / `self.eval(x)`
|
|
5290
|
+
* - aliased eval: `const f = eval; f(x)` then `f(taint)`
|
|
5291
|
+
* Fires when the argument traces back to req.body / req.query / req.params /
|
|
5292
|
+
* req.headers / req.cookies (Express/Koa-style request extractor).
|
|
5293
|
+
*/
|
|
5294
|
+
export function findJsIndirectEvalCodeInjectionFindings(code, file) {
|
|
5295
|
+
const findings = [];
|
|
5296
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
5297
|
+
return findings;
|
|
5298
|
+
if (!/\beval\b/.test(code))
|
|
5299
|
+
return findings;
|
|
5300
|
+
const lines = code.split('\n');
|
|
5301
|
+
// request extractor (assignment rhs)
|
|
5302
|
+
const reqExtractRe = /\breq(?:uest)?\s*\.\s*(?:body|query|params|headers|cookies)\b/;
|
|
5303
|
+
// First: discover indirect-eval aliases: `const f = eval;`, `let f = eval`
|
|
5304
|
+
const aliasRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:globalThis\s*\.\s*eval|global\s*\.\s*eval|window\s*\.\s*eval|self\s*\.\s*eval|eval)\s*;?\s*$/;
|
|
5305
|
+
const aliases = new Set();
|
|
5306
|
+
for (const line of lines) {
|
|
5307
|
+
const m = line.match(aliasRe);
|
|
5308
|
+
if (m)
|
|
5309
|
+
aliases.add(m[1]);
|
|
5310
|
+
}
|
|
5311
|
+
// Discover transitively-tainted vars
|
|
5312
|
+
const taintedVars = new Set();
|
|
5313
|
+
const assignRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/;
|
|
5314
|
+
const reassignRe = /^\s*(\w+)\s*=\s*(.+?);?\s*$/;
|
|
5315
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
5316
|
+
const before = taintedVars.size;
|
|
5317
|
+
for (const line of lines) {
|
|
5318
|
+
const m = line.match(assignRe) || line.match(reassignRe);
|
|
5319
|
+
if (!m)
|
|
5320
|
+
continue;
|
|
5321
|
+
const lhs = m[1];
|
|
5322
|
+
const rhs = m[2];
|
|
5323
|
+
if (taintedVars.has(lhs))
|
|
5324
|
+
continue;
|
|
5325
|
+
if (lhs === 'const' || lhs === 'let' || lhs === 'var')
|
|
5326
|
+
continue;
|
|
5327
|
+
if (reqExtractRe.test(rhs)) {
|
|
5328
|
+
taintedVars.add(lhs);
|
|
5329
|
+
continue;
|
|
5330
|
+
}
|
|
5331
|
+
for (const v of taintedVars) {
|
|
5332
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5333
|
+
if (re.test(rhs)) {
|
|
5334
|
+
taintedVars.add(lhs);
|
|
5335
|
+
break;
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
if (taintedVars.size === before)
|
|
5340
|
+
break;
|
|
5341
|
+
}
|
|
5342
|
+
// Patterns: (0, eval)(x); globalThis.eval(x); aliased f(x)
|
|
5343
|
+
const indirectCommaRe = /\(\s*0\s*,\s*eval\s*\)\s*\(\s*([^)]*)\s*\)/;
|
|
5344
|
+
const indirectMemberRe = /\b(?:globalThis|global|window|self)\s*\.\s*eval\s*\(\s*([^)]*)\s*\)/;
|
|
5345
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5346
|
+
const line = lines[i];
|
|
5347
|
+
const trimmed = line.trim();
|
|
5348
|
+
// skip comment lines and alias-declaration lines themselves
|
|
5349
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
5350
|
+
continue;
|
|
5351
|
+
if (aliasRe.test(line))
|
|
5352
|
+
continue;
|
|
5353
|
+
let arg = null;
|
|
5354
|
+
let formLabel = '';
|
|
5355
|
+
let m = trimmed.match(indirectCommaRe);
|
|
5356
|
+
if (m) {
|
|
5357
|
+
arg = m[1].trim();
|
|
5358
|
+
formLabel = '(0, eval)(...) indirect eval';
|
|
5359
|
+
}
|
|
5360
|
+
if (!arg) {
|
|
5361
|
+
m = trimmed.match(indirectMemberRe);
|
|
5362
|
+
if (m) {
|
|
5363
|
+
arg = m[1].trim();
|
|
5364
|
+
formLabel = 'globalThis.eval / window.eval / self.eval indirect eval';
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
if (!arg && aliases.size > 0) {
|
|
5368
|
+
for (const a of aliases) {
|
|
5369
|
+
const aliasCallRe = new RegExp(`\\b${a}\\s*\\(\\s*([^)]*)\\s*\\)`);
|
|
5370
|
+
const mm = trimmed.match(aliasCallRe);
|
|
5371
|
+
if (mm) {
|
|
5372
|
+
arg = mm[1].trim();
|
|
5373
|
+
formLabel = `aliased eval reference \`${a}(...)\``;
|
|
5374
|
+
break;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
if (arg === null)
|
|
5379
|
+
continue;
|
|
5380
|
+
if (arg.length === 0)
|
|
5381
|
+
continue;
|
|
5382
|
+
// skip literal-only string args
|
|
5383
|
+
if (/^['"`][^'"`]*['"`]$/.test(arg))
|
|
5384
|
+
continue;
|
|
5385
|
+
let tainted = false;
|
|
5386
|
+
if (reqExtractRe.test(arg))
|
|
5387
|
+
tainted = true;
|
|
5388
|
+
else {
|
|
5389
|
+
for (const v of taintedVars) {
|
|
5390
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5391
|
+
if (re.test(arg)) {
|
|
5392
|
+
tainted = true;
|
|
5393
|
+
break;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
if (!tainted)
|
|
5398
|
+
continue;
|
|
5399
|
+
findings.push({
|
|
5400
|
+
id: `code_injection-${file}-${i + 1}-js-indirect-eval`,
|
|
5401
|
+
pass: 'language-sources',
|
|
5402
|
+
category: 'security',
|
|
5403
|
+
rule_id: 'code_injection',
|
|
5404
|
+
cwe: 'CWE-94',
|
|
5405
|
+
severity: 'critical',
|
|
5406
|
+
level: 'error',
|
|
5407
|
+
message: `Code injection: ${formLabel} called with a value derived from ` +
|
|
5408
|
+
'an HTTP request body/query/headers. Indirect eval forms still ' +
|
|
5409
|
+
'execute arbitrary code in the global scope. Remove the eval and ' +
|
|
5410
|
+
'parse the input with a safe data parser instead.',
|
|
5411
|
+
file,
|
|
5412
|
+
line: i + 1,
|
|
5413
|
+
snippet: trimmed,
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
return findings;
|
|
5417
|
+
}
|
|
5418
|
+
/**
|
|
5419
|
+
* Sprint 83 detector C — Python `code` stdlib REPL / compile_command.
|
|
5420
|
+
* `code.InteractiveInterpreter().runsource(s)`, `runcode(c)`, `push(line)` and
|
|
5421
|
+
* `code.InteractiveConsole().push(line)`, plus `code.compile_command(s)` —
|
|
5422
|
+
* all execute or compile arbitrary Python source. Fires when the argument
|
|
5423
|
+
* traces back to a Flask `request.*` extractor; gated on `import code`
|
|
5424
|
+
* to avoid colliding with user-defined `code` variables.
|
|
5425
|
+
*/
|
|
5426
|
+
export function findPythonInteractiveInterpreterCodeInjectionFindings(code, file) {
|
|
5427
|
+
const findings = [];
|
|
5428
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
5429
|
+
return findings;
|
|
5430
|
+
// Require `import code` namespace gate to avoid clashing with user-defined
|
|
5431
|
+
// identifiers named `code`.
|
|
5432
|
+
if (!/^\s*import\s+code\b/m.test(code))
|
|
5433
|
+
return findings;
|
|
5434
|
+
if (!/\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole|compile_command)\b/.test(code)) {
|
|
5435
|
+
return findings;
|
|
5436
|
+
}
|
|
5437
|
+
const lines = code.split('\n');
|
|
5438
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|files|json|cookies|headers|get_data|get_json)\b/;
|
|
5439
|
+
// Discover transitively-tainted vars
|
|
5440
|
+
const taintedVars = new Set();
|
|
5441
|
+
const assignRe = /^\s*(\w+)\s*=\s*(.+?)\s*(?:#.*)?$/;
|
|
5442
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
5443
|
+
const before = taintedVars.size;
|
|
5444
|
+
for (const line of lines) {
|
|
5445
|
+
const m = line.match(assignRe);
|
|
5446
|
+
if (!m)
|
|
5447
|
+
continue;
|
|
5448
|
+
const lhs = m[1];
|
|
5449
|
+
const rhs = m[2];
|
|
5450
|
+
if (taintedVars.has(lhs))
|
|
5451
|
+
continue;
|
|
5452
|
+
if (reqExtractRe.test(rhs)) {
|
|
5453
|
+
taintedVars.add(lhs);
|
|
5454
|
+
continue;
|
|
5455
|
+
}
|
|
5456
|
+
for (const v of taintedVars) {
|
|
5457
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5458
|
+
if (re.test(rhs)) {
|
|
5459
|
+
taintedVars.add(lhs);
|
|
5460
|
+
break;
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
if (taintedVars.size === before)
|
|
5465
|
+
break;
|
|
5466
|
+
}
|
|
5467
|
+
// Sink call patterns
|
|
5468
|
+
// a) code.InteractiveInterpreter(...).{runsource,runcode,push}(arg)
|
|
5469
|
+
// b) code.InteractiveConsole(...).{runsource,runcode,push,interact}(arg)
|
|
5470
|
+
// c) code.compile_command(arg, ...)
|
|
5471
|
+
const callRe = /\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole)\s*\([^)]*\)\s*\.\s*(runsource|runcode|push|interact)\s*\(\s*([^),]+)/;
|
|
5472
|
+
const compileRe = /\bcode\s*\.\s*compile_command\s*\(\s*([^),]+)/;
|
|
5473
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5474
|
+
const line = lines[i];
|
|
5475
|
+
const trimmed = line.trim();
|
|
5476
|
+
let arg = null;
|
|
5477
|
+
let formLabel = '';
|
|
5478
|
+
const m1 = trimmed.match(callRe);
|
|
5479
|
+
if (m1) {
|
|
5480
|
+
arg = m1[2].trim();
|
|
5481
|
+
formLabel = `code.${/Interpreter/.test(trimmed) ? 'InteractiveInterpreter' : 'InteractiveConsole'}().${m1[1]}`;
|
|
5482
|
+
}
|
|
5483
|
+
if (!arg) {
|
|
5484
|
+
const m2 = trimmed.match(compileRe);
|
|
5485
|
+
if (m2) {
|
|
5486
|
+
arg = m2[1].trim();
|
|
5487
|
+
formLabel = 'code.compile_command';
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
if (arg === null)
|
|
5491
|
+
continue;
|
|
5492
|
+
if (arg.length === 0)
|
|
5493
|
+
continue;
|
|
5494
|
+
if (/^['"][^'"]*['"]$/.test(arg))
|
|
5495
|
+
continue;
|
|
5496
|
+
let tainted = false;
|
|
5497
|
+
if (reqExtractRe.test(arg))
|
|
5498
|
+
tainted = true;
|
|
5499
|
+
else {
|
|
5500
|
+
for (const v of taintedVars) {
|
|
5501
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5502
|
+
if (re.test(arg)) {
|
|
5503
|
+
tainted = true;
|
|
5504
|
+
break;
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
if (!tainted)
|
|
5509
|
+
continue;
|
|
5510
|
+
findings.push({
|
|
5511
|
+
id: `code_injection-${file}-${i + 1}-py-interactive-interpreter`,
|
|
5512
|
+
pass: 'language-sources',
|
|
5513
|
+
category: 'security',
|
|
5514
|
+
rule_id: 'code_injection',
|
|
5515
|
+
cwe: 'CWE-94',
|
|
5516
|
+
severity: 'critical',
|
|
5517
|
+
level: 'error',
|
|
5518
|
+
message: `Code injection: ${formLabel}(...) called with a value derived from ` +
|
|
5519
|
+
'a Flask request extractor. The Python `code` module compiles and ' +
|
|
5520
|
+
'executes arbitrary source. Remove the call and validate input ' +
|
|
5521
|
+
'against a fixed allow-list instead.',
|
|
5522
|
+
file,
|
|
5523
|
+
line: i + 1,
|
|
5524
|
+
snippet: trimmed,
|
|
5525
|
+
});
|
|
5526
|
+
}
|
|
5527
|
+
return findings;
|
|
5528
|
+
}
|
|
5529
|
+
/**
|
|
5530
|
+
* Sprint 83 detector D — Rust eval-crate / dynamic-load sinks.
|
|
5531
|
+
* Rust has no language-level eval; the canonical sinks are:
|
|
5532
|
+
* - `evalexpr::eval(...)` (and `_with_context|_boolean|_int|_float|_string|_tuple|_empty`)
|
|
5533
|
+
* - `libloading::Library::new(...)` (dynamic library load)
|
|
5534
|
+
* - `mlua::Lua::new().load(<src>).{exec,eval,call}(...)` / rlua equivalent
|
|
5535
|
+
* Fires when the argument traces back to an Actix-web extractor: a plain
|
|
5536
|
+
* `body: String|Bytes`, a `web::Query<T>` / `web::Path<T>` / `web::Form<T>` /
|
|
5537
|
+
* `web::Json<T>` / `HttpRequest` param, or `axum::body::Bytes`/`String` etc.
|
|
5538
|
+
*/
|
|
5539
|
+
export function findRustEvalCrateCodeInjectionFindings(code, file) {
|
|
5540
|
+
const findings = [];
|
|
5541
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
5542
|
+
return findings;
|
|
5543
|
+
if (!/\b(?:evalexpr\s*::\s*eval|libloading\s*::\s*Library\s*::\s*new|\.\s*load\s*\([^)]*\)\s*\.\s*(?:exec|eval|call))/.test(code)) {
|
|
5544
|
+
return findings;
|
|
5545
|
+
}
|
|
5546
|
+
const lines = code.split('\n');
|
|
5547
|
+
// Discover per-function tainted params: scan `fn ...(params)` headers and
|
|
5548
|
+
// mark params with extractor types as tainted.
|
|
5549
|
+
const extractorTypeRe = /:\s*(?:String|Bytes|bytes::Bytes|axum::body::Bytes|web::Query\b|web::Path\b|web::Form\b|web::Json\b|HttpRequest\b|actix_web::HttpRequest\b)/;
|
|
5550
|
+
const fns = [];
|
|
5551
|
+
let cur = null;
|
|
5552
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5553
|
+
const t = lines[i];
|
|
5554
|
+
if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
|
|
5555
|
+
if (cur) {
|
|
5556
|
+
cur.end = i - 1;
|
|
5557
|
+
fns.push(cur);
|
|
5558
|
+
}
|
|
5559
|
+
cur = { start: i, end: lines.length - 1, tainted: new Set() };
|
|
5560
|
+
// collect param idents whose type matches extractor
|
|
5561
|
+
const headerJoined = (() => {
|
|
5562
|
+
let j = i;
|
|
5563
|
+
let s = '';
|
|
5564
|
+
while (j < lines.length && !/\{\s*$/.test(s)) {
|
|
5565
|
+
s += lines[j];
|
|
5566
|
+
if (/\{\s*$/.test(lines[j]))
|
|
5567
|
+
break;
|
|
5568
|
+
j++;
|
|
5569
|
+
if (j - i > 12)
|
|
5570
|
+
break;
|
|
5571
|
+
}
|
|
5572
|
+
return s;
|
|
5573
|
+
})();
|
|
5574
|
+
// params are between first '(' and matching ')'
|
|
5575
|
+
const open = headerJoined.indexOf('(');
|
|
5576
|
+
const close = headerJoined.lastIndexOf(')');
|
|
5577
|
+
if (open !== -1 && close > open) {
|
|
5578
|
+
const params = headerJoined.substring(open + 1, close);
|
|
5579
|
+
// Split by top-level commas
|
|
5580
|
+
let depth = 0;
|
|
5581
|
+
let buf = '';
|
|
5582
|
+
const parts = [];
|
|
5583
|
+
for (const ch of params) {
|
|
5584
|
+
if (ch === '<' || ch === '(')
|
|
5585
|
+
depth++;
|
|
5586
|
+
else if (ch === '>' || ch === ')')
|
|
5587
|
+
depth--;
|
|
5588
|
+
if (ch === ',' && depth === 0) {
|
|
5589
|
+
parts.push(buf);
|
|
5590
|
+
buf = '';
|
|
5591
|
+
continue;
|
|
5592
|
+
}
|
|
5593
|
+
buf += ch;
|
|
5594
|
+
}
|
|
5595
|
+
if (buf.trim().length > 0)
|
|
5596
|
+
parts.push(buf);
|
|
5597
|
+
for (const p of parts) {
|
|
5598
|
+
const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
|
|
5599
|
+
if (!pm)
|
|
5600
|
+
continue;
|
|
5601
|
+
if (extractorTypeRe.test(p))
|
|
5602
|
+
cur.tainted.add(pm[1]);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
if (cur)
|
|
5608
|
+
fns.push(cur);
|
|
5609
|
+
// Inside each function, scan assignments (let / let mut) to propagate
|
|
5610
|
+
// taint from existing tainted vars into new bindings.
|
|
5611
|
+
for (const fn of fns) {
|
|
5612
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
5613
|
+
const before = fn.tainted.size;
|
|
5614
|
+
for (let i = fn.start; i <= fn.end; i++) {
|
|
5615
|
+
const t = lines[i].trim();
|
|
5616
|
+
const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
|
|
5617
|
+
if (!m)
|
|
5618
|
+
continue;
|
|
5619
|
+
const lhs = m[1];
|
|
5620
|
+
const rhs = m[2];
|
|
5621
|
+
if (fn.tainted.has(lhs))
|
|
5622
|
+
continue;
|
|
5623
|
+
for (const v of fn.tainted) {
|
|
5624
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5625
|
+
if (re.test(rhs)) {
|
|
5626
|
+
fn.tainted.add(lhs);
|
|
5627
|
+
break;
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
if (fn.tainted.size === before)
|
|
5632
|
+
break;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
const evalExprRe = /\bevalexpr\s*::\s*eval(?:_with_context|_boolean|_int|_float|_string|_tuple|_empty)?\s*\(\s*([^)]+)\s*\)/;
|
|
5636
|
+
const libloadingRe = /\blibloading\s*::\s*Library\s*::\s*new\s*\(\s*([^)]+)\s*\)/;
|
|
5637
|
+
const luaLoadRe = /\.\s*load\s*\(\s*([^)]+)\s*\)\s*\.\s*(?:exec|eval|call)\b/;
|
|
5638
|
+
for (const fn of fns) {
|
|
5639
|
+
if (fn.tainted.size === 0)
|
|
5640
|
+
continue;
|
|
5641
|
+
for (let i = fn.start; i <= fn.end; i++) {
|
|
5642
|
+
const line = lines[i];
|
|
5643
|
+
const trimmed = line.trim();
|
|
5644
|
+
let arg = null;
|
|
5645
|
+
let formLabel = '';
|
|
5646
|
+
let m = trimmed.match(evalExprRe);
|
|
5647
|
+
if (m) {
|
|
5648
|
+
arg = m[1].trim();
|
|
5649
|
+
formLabel = 'evalexpr::eval';
|
|
5650
|
+
}
|
|
5651
|
+
if (!arg) {
|
|
5652
|
+
m = trimmed.match(libloadingRe);
|
|
5653
|
+
if (m) {
|
|
5654
|
+
arg = m[1].trim();
|
|
5655
|
+
formLabel = 'libloading::Library::new';
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
if (!arg) {
|
|
5659
|
+
m = trimmed.match(luaLoadRe);
|
|
5660
|
+
if (m) {
|
|
5661
|
+
arg = m[1].trim();
|
|
5662
|
+
formLabel = 'mlua/rlua Lua::load().{exec|eval|call}';
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
if (arg === null)
|
|
5666
|
+
continue;
|
|
5667
|
+
if (arg.length === 0)
|
|
5668
|
+
continue;
|
|
5669
|
+
// unwrap leading '&' borrow
|
|
5670
|
+
let unwrapped = arg.replace(/^&\s*/, '').trim();
|
|
5671
|
+
if (/^"[^"]*"$/.test(unwrapped))
|
|
5672
|
+
continue;
|
|
5673
|
+
let tainted = false;
|
|
5674
|
+
for (const v of fn.tainted) {
|
|
5675
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
5676
|
+
if (re.test(unwrapped)) {
|
|
5677
|
+
tainted = true;
|
|
5678
|
+
break;
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
if (!tainted)
|
|
5682
|
+
continue;
|
|
5683
|
+
findings.push({
|
|
5684
|
+
id: `code_injection-${file}-${i + 1}-rust-eval-crate`,
|
|
5685
|
+
pass: 'language-sources',
|
|
5686
|
+
category: 'security',
|
|
5687
|
+
rule_id: 'code_injection',
|
|
5688
|
+
cwe: 'CWE-94',
|
|
5689
|
+
severity: 'critical',
|
|
5690
|
+
level: 'error',
|
|
5691
|
+
message: `Code injection: ${formLabel}(...) called with a value derived ` +
|
|
5692
|
+
'from an HTTP request extractor (body / Query / Path / Form / ' +
|
|
5693
|
+
'Json / HttpRequest). The expression / library / Lua chunk is ' +
|
|
5694
|
+
'executed as code. Remove the dynamic-eval path or restrict ' +
|
|
5695
|
+
'input to a fixed allow-list.',
|
|
5696
|
+
file,
|
|
5697
|
+
line: i + 1,
|
|
5698
|
+
snippet: trimmed,
|
|
5699
|
+
});
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
return findings;
|
|
5703
|
+
}
|
|
5138
5704
|
//# sourceMappingURL=language-sources-pass.js.map
|