PDM wheel and project-local file-write boundaries¶
Source: hourly offensive-security scan, 2026-06-10. Primary entries: GitHub advisories GHSA-78v8-vpjp-cjqh / CVE-2026-47764 for PDM wheel installation path traversal through InstallDestination.write_to_fs(), and GHSA-ghq2-5c67-fprm / CVE-2026-47763 for project-local state and config writes following symlinks.
This batch is durable for operators because it gives a reusable Python package-manager testing pattern: attacker-controlled package or repository content crossing from the project tree into arbitrary filesystem writes by the invoking user.
Why it matters for assessments¶
PDM is often run by developers, CI jobs, release workers, and automation that trust repository-local metadata. The two advisories expose adjacent file-boundary primitives:
- a malicious wheel can include traversal entries that are joined into the install destination without the base installer's resolved-path containment check;
- a malicious repository can place project-local PDM state files as symlinks so later
pdmoperations overwrite the symlink target.
Neither proof requires leaking real secrets. Good evidence shows that a controlled canary file outside the project or install root was created or modified by a normal package-manager action.
What to map¶
- Find projects, CI workflows, devcontainer images, release scripts, or build agents that run
pdm install,pdm sync,pdm add,pdm config -l, orpdm useagainst contributor-controlled branches or dependencies. - Record the PDM version and interpreter path. The wheel traversal advisory affects PDM
<= 2.22.4; the symlink-write advisory affects PDM< 2.27.0. - Identify whether the actor can influence wheel contents, direct URL dependencies, package indexes, lockfile entries, or checked-out repository files.
- Map the privilege context: local developer shell, CI runner, release builder, container root, package-publishing job, or shared workstation.
- Pick the lowest-impact writable canary target outside the project or install root. Do not target shell startup files, SSH material, package credentials, service units, or production config.
Wheel traversal validation boundary¶
Use a lab package or an explicitly approved dependency-confusion/supply-chain test. Do not publish a malicious wheel to a public package name or replace a real dependency.
Safe proof shape:
- Build a disposable wheel containing only a traversal-path canary entry that writes a marker such as
skillz-pdm-wheel-canaryto a temporary directory outside the intended install root. - Install it with the same PDM path and options used by the target workflow, preferably in a temporary virtual environment or disposable CI job.
- Confirm whether the marker lands outside the expected installation destination.
- Capture redacted evidence: PDM version, command, wheel source under tester control, expected destination, actual canary path, and file metadata.
- Stop at canary write proof. Do not overwrite executable startup paths or sensitive files.
A strong finding demonstrates both attacker control over the wheel source and execution of a normal PDM install path in a context whose filesystem privileges matter.
Project-local symlink write validation boundary¶
Use a temporary clone or approved lab repository. The advisory notes pdm.toml as a stable sink, with related sinks including .pdm-python and .python-version.
Minimal canary flow:
# Use the same interpreter/PDM entrypoint as the tested workflow.
PDM_PY=/path/to/python-with-pdm
lab=$(mktemp -d)
target="$lab/outside-target.toml"
cat > "$target" <<'EOF'
[seed]
value = 1
EOF
mkdir "$lab/repo"
ln -s "$target" "$lab/repo/pdm.toml"
cat > "$lab/repo/pyproject.toml" <<'EOF'
[project]
name = "skillz-pdm-symlink-canary"
version = "0.0.1"
EOF
(
cd "$lab/repo" &&
"$PDM_PY" -m pdm config -l venv.in_project false
)
printf '\n--- canary target ---\n'
cat "$target"
Expected vulnerable signal: the TOML file outside the repository gains a [venv] section or other PDM-written configuration. If pdm.toml does not parse as TOML, this specific sink may fail before the write path; use a parseable canary target for this proof.
Reporting heuristics¶
- Lead with the boundary: untrusted package or repository content caused PDM to write outside the intended project/install root as the invoking user.
- Include PDM version, affected command, attacker-controlled input source, expected containment root, actual canary target, and the runner/user privilege context.
- For CI impact, show whether the job also has release tokens, package-publishing rights, repository write tokens, cache directories, or artifact-signing access; do not print token values.
- Keep proof narrow and reversible. Use temporary canary files, not real dotfiles, credentials, deployment manifests, or shell profiles.
- Avoid claiming code execution unless you separately prove that the file-write primitive reaches an executable or auto-loaded path in the assessed environment.
October 5 follow-up: Papermerge upload traversal — the file-write-to-auto-loaded-path proof this page demands¶
GHSA-5557-484j-55xw / CVE-2026-105314 (high, Papermerge 3.5.3): a standard user (not admin) achieves RCE via directory traversal in /api/documents/upload — writing a Python .pth file into site-packages, whose line is executed by every subsequent Python interpreter start in that environment. This is exactly the escalation the reporting heuristic above asks you to complete: the write primitive alone is a file-confidentiality/integrity finding; the .pth-into-site-packages sink converts it into guaranteed, persistent, interpreter-start code execution with zero further attacker action.
- Durable auto-loaded-path checklist for any Python-service upload/copy primitive with traversal:
site-packages/*.pth,site-packages/usercustomize.py,PYTHONSTARTUP-reachable files, cron/systemd paths owned by the service user, and the app's own plugin/auto-import directories..pthis the quietest: it needs no import of your file, fires on interpreter start, and survives app restarts. - Deferred-execution timing matters in reporting: the payload fires on the next interpreter start (worker respawn, restart, cron), not on upload — record the upload→fire interval and prove with an inert marker
.pth(e.g., writes a temp-file timestamp) in a disposable instance, then delete the canary and verify no marker re-fires. - Role boundary is the headline: a standard user reaching interpreter-start execution as the service account is a privilege crossing, not just storage abuse; test upload traversal with the lowest authenticated role first.