fix(behavioral): detect builtins.* and importlib.import_module sink evasions#180
fix(behavioral): detect builtins.* and importlib.import_module sink evasions#180zied-jlassi wants to merge 1 commit into
Conversation
…vasions
The import-alias normalization rewrites `from builtins import exec` and `import builtins; builtins.exec(...)` to the qualified `builtins.exec`, which the bare-name sink checks (call_name == "exec", _EXEC_SINKS) then miss. Collapse `builtins.<x>` back to the bare builtin so these re-enter the existing detection, covering exec/eval/compile/__import__. Add a dynamic-import resolver so `importlib.import_module('os').system(...)` (sibling of __import__) resolves to os.system / the subprocess family and hits the sink ladders.
Baseline detection unchanged; composes with the getattr branch (NVIDIA#166) with no double-fire; FP-neighbors stay clean.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
rng1995
left a comment
There was a problem hiding this comment.
[Automated SkillSpector Review]
Approved.
High-quality anti-evasion work closing two disjoint bypasses: from builtins import exec / builtins.exec(...) (collapsed to the bare builtin via _strip_builtins_prefix — semantically exact since builtins.exec is exec) and importlib.import_module('os').system(...) (resolved to os.system via resolve_dynamic_import_call). Both are wired as fallbacks only when primary resolution returns None, so they don't double-fire with the getattr branch (#166). Resolution stays precise (literal-only module/attr), and FP-neighbors (from mymod import exec_helper, import_module('json').loads) are explicitly tested. 15 new tests across AST + taint. Directly strengthens detection — no blockers.
Summary
Complementary to #166 (the
getattrbranch). #166 closed reflectivegetattr(os,'system')evasion; this PR closes the disjoint import /builtins/importlibbranch, which #166 does not cover.Problem
The import-alias normalization rewrites
from builtins import execandimport builtins; builtins.exec(...)to the qualifiedbuiltins.exec, but the analyzers match dangerous builtins by bare name (call_name == "exec",_EXEC_SINKS) — so these get missed while plainos.systemis caught. Separately,importlib.import_module('os').system(...)(and thesubprocessfamily, and bare-importedimport_module) is never matched: only__import__is.Fix
builtins.<x>back to the bare builtin at the alias-normalization chokepoint —builtins.exec is exec, so this is semantically exact and reuses the existing checks. Coversexec/eval/compile/__import__.resolve_dynamic_import_call(sibling of__import__) resolvingimportlib.import_module('mod').attrto the canonicalmod.attrsink, re-entering the existingos./subprocess.ladders. Covers the subprocess family too.0 new dependencies, backward-compatible, composes with #166 (no double-fire;
getattr(os,'system')stays a single AST9).Tests
Added
TestBuiltinsImportEvasion,TestImportlibDynamicChainEvasion(behavioral_ast) andTestBuiltinsImportlibSinkEvasion(taint): every evasion form detected; baseline still detected; FP-neighbors (from mymod import exec_helper,import_module('json').loads) stay clean. Full behavioral analyzer suite green (104/104 incl. #166's AST9 tests).Prepared with AI assistance; every line was reviewed and validated by the author.