Skip to content

feat(code-injection): report dynamic imports and runpy reached by input - #250

Merged
SizzleUnrlsd merged 4 commits into
mainfrom
feat/dynamic-import-sinks
Oct 13, 2026
Merged

SizzleUnrlsd merged 4 commits into
mainfrom
feat/dynamic-import-sinks

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Part 1 of #240. sys.path and getattr(...)() are left for a next step.

Summary

New sinks:

  • importlib.import_module, __import__, importlib.__import__, runpy.run_module and runpy.run_path are CODE sinks.
  • Only the argument naming the module or the file is a sink, by position or keyword (name, mod_name, path_name). The package of a relative name, the globals and the run name are data.
  • __import__ was not resolved at all: BUILTIN_NAMES dropped every name starting with _. It now resolves to python.builtins.__import__.

How findings are reported:

  • Findings are titled Untrusted module import or Untrusted script execution; the rule id stays code-injection.
  • A constant package prefix ("app.plugins." + name, f-string) gives a hotspot. Reading it needs ir.ssa and abstract.module_strings, as in ssrf, so the manifest and the requires assertion in tests/test_detectors.py change accordingly.
  • As decided in code-injection: dynamic imports and runpy are not modelled as sinks #240, the environment and the command line reach these sinks as they reach eval.

Verification

  • tests/test_dynamic_import_sinks.py, new: 20 tests, 13 of which fail on main. They also cover negative cases (constant name, allowlist, data arguments only) and pin that eval still reports environment and command-line input.
  • Full suite green, apart from the local test_packaging metadata issue. ruff check and mypy clean.
  • Regression suite: no snapshot change.

Part of #240: sys.path and getattr are left for a next step.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Oct 13, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Oct 13, 2026
A check of the shape of a module or file name (isidentifier(), isalnum(), re.fullmatch) limits its characters, not the code it names. For the dynamic import and runpy sinks it leaves a hotspot; only membership in a constant allowlist or equality with a constant proves them. Part of #240.
Sink.merged rebuilt the sink without selects, so another plugin declaring the same import sink brought back shape checks as proofs. The merged sink selects when either declaration does, in either order. Part of #240.
@SizzleUnrlsd
SizzleUnrlsd merged commit c0aaeee into main Oct 13, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/dynamic-import-sinks branch October 13, 2026 22:00
@SizzleUnrlsd
SizzleUnrlsd restored the feat/dynamic-import-sinks branch October 14, 2026 00:04
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/dynamic-import-sinks branch October 14, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant