fix: recognize stdlib modules across all supported Python versions (closes #311) - #313
fix: recognize stdlib modules across all supported Python versions (closes #311)#313feiiiiii5 wants to merge 2 commits into
Conversation
|
Hey! I feel like the related issue is not particularly hard to implement; it's more that there are a few edge cases we'd need to think about first (e.g., PyPI dependencies with the same name as a previous stdlib module that are definitely not stdlib anymore, how to maintain the list of extra modules, etc.). I won't merge it as-is, but I'll be happy to discuss these points with you if you want to spend more time on this one. |
|
Thanks @thomas-chauchefoin-tob for taking a look — happy to dig into the edge cases. I see two concrete concerns; here's how I'd approach each, and I'd value your steer on which direction matches the project's taste. 1. PyPI packages shadowing former stdlib module namesThe risk: a pickle references a module name that used to be stdlib (e.g. Options I can see:
My lean is (b) — it matches the issue's "document the defaults and the associated risks" suggestion without silently widening the blast radius. But (a) is defensible if you want the simpler mental model. 2. Maintaining the list as Python evolves
My lean is (a) + (c): keep the checked-in static list (no new runtime dep), add a CI drift test so the list can't silently go stale. Proposed next stepIf (1b) + (2a)+(2c) sounds like the right shape, I'll rework the PR to:
Happy to go a different direction if you'd prefer — just want to make sure I'm solving the right problem before reworking the diff. |
|
Reworked per the shape I proposed on 07-30, addressing both edge cases you raised (commit 1. PyPI packages shadowing former stdlib names — now reported, not trusted
2. Maintaining the list as Python evolves — CI drift guard
Verification: 148/148 tests pass locally (Python 3.11 env), If you would rather have legacy modules fail even harder (option 1a — drop them from the safe set entirely) or want a CLI flag instead of the module-level allowlist, happy to flip. |
|
Friendly bump @thomas-chauchefoin-tob — the rework in |
|
@thomas-chauchefoin-tob final ping from me on this one: the |
The stdlib-safety check keyed off sys.stdlib_module_names of the
interpreter running the scan. A pickle scanned in CI on one Python
version is often loaded on another, so modules added in newer releases
were flagged as unsafe by older scanners, and modules removed in newer
releases (imp, asynchat, smtpd, ...) passed scans only to resolve to a
malicious PyPI squatter — or fail outright — on the target.
Replace the single-version list with a checked-in union of top-level
stdlib module names across every supported Python (3.10-3.14), merged
at import time with the running interpreter's own names so a scanner
can never miss names from a release newer than this table.
The residual risk inherent to any static union — a removed module name
squatted by a malicious PyPI distribution on interpreters where it is
no longer standard — is documented in the module docstring, and
scanners that know exactly which interpreter will unpickle the file
can narrow the effective set with use_stdlib_of(*versions).
scripts/generate_stdlib_names.py regenerates the table; run it when
the support matrix changes:
uv run --no-project python scripts/generate_stdlib_names.py
Fixes trailofbits#311
Supersedes the hand-curated static lists with data captured from real interpreters: - stdlib_names.py now also emits SHADOWED_STDLIB_MODULE_NAMES — public top-level names present in any supported release but absent from the newest one (3.14), i.e. the removable dead batteries plus names like binhex/spwd/nis the manual list missed. - ShadowedStdlibImports analysis flags imports of these names as SUSPICIOUS instead of trusting them as plain stdlib; they are also excluded from likely_safe_imports. - SHADOWED_STDLIB_IMPORT_ALLOWLIST stays as the user extension point. - use_stdlib_of() targeting now silences shadow warnings for modules the selected target genuinely ships. Regenerate via scripts/generate_stdlib_names.py when the support matrix changes.
b8abeb3 to
2c8ac5b
Compare
Root cause
BUILTIN_STDLIB_MODULE_NAMESwassys.stdlib_module_namesof the interpreter running the scan. A pickle scanned in CI on one Python version is often loaded on another, and the verdict flips depending on which side of a version boundary each interpreter sits:tomllib,annotationlib, …) get flagged as non-standard even though they are perfectly standard on the target.imp,asynchat,smtpd, …) pass the scan, then on modern interpreters resolve to anImportError— or to a malicious PyPI distribution squatting the removed name.Approach
Checked-in generated table rather than the
stdlib_listdependency (option 1 from the issue): fickling is a security tool whose only runtime dependency is conditionaltyping-extensions, the check only ever consults top-level names (partition(".")[0]), and a checked-in table makes scans deterministic across environments.fickling/stdlib_names.py(generated, do not edit):STDLIB_MODULE_NAMES_BY_VERSION— realsys.stdlib_module_namescaptured per supported interpreter (3.10–3.14) byscripts/generate_stdlib_names.py.STDLIB_MODULE_NAMES— union of every table plus the running interpreter's names, so a scanner on a Python newer than this table can never miss newly-added modules.SHADOWED_STDLIB_MODULE_NAMES— public top-level names present in any supported release but absent from the newest one: the removable "dead batteries" (asynchat,imp,cgi, …) plus names likebinhex/spwd/nisthat a manual list tends to miss. Derived from the tables, not hand-maintained.Shadowing defense. The default union deliberately stays permissive for benign pickles, but imports of shadowable names are no longer silently trusted:
ShadowedStdlibImportsanalysis reports them atSUSPICIOUS("removed from the stdlib in a recent version; on modern interpreters this name resolves to a third-party package of the same name").likely_safe_imports.fickling.fickle.SHADOWED_STDLIB_IMPORT_ALLOWLISTremains the user extension point for scanners that know a name is safe on their targets.User control over the target version (the issue's second ask):
stdlib_names.use_stdlib_of(*versions)narrows the effective stdlib set to specific interpreters' tables — which also silences shadow warnings for modules the selected target genuinely ships; no arguments restores the default union. Unknown versions raiseKeyError.Regeneration
When the support matrix changes:
Documented residual risk
No static analysis can distinguish "stdlib name" from "PyPI squatter of a formerly-stdlib name" without knowing the target version. That trade-off — default permissive union + loud-but-not-fatal shadow warnings + opt-in targeting — is documented in the module docstring.
Verification
On a 3.11 scanner:
test/test_stdlib_names.py(14 cases): cross-version recognition independent of scanner version, provenance sanity checks against the per-version tables, union coverage, running-interpreter inclusion, narrowing + restore round-trip, unknown-version rejection, shadow flagging / target-aware silencing / allowlist restore. Full suite: 128 passed / 127 subtests (torch-dependent polyglot/hook/pytorch/unpickler modules excluded locally; they need thetorchextra).Fixes #311
Note: this PR was reworked from an earlier static-list draft to generated per-version data after re-auditing issue #311's requirements — the hand-curated module lists could drift from reality and missed several genuinely removed modules.