{"id":"PYSEC-2026-2862","summary":"PDM: Project-Local State and Config Writes Follow Symlinks","details":"## Summary\n\nPDM writes several project-local state or configuration files without symlink protection. If a malicious repository places those files as symlinks, local PDM operations can overwrite the symlink targets.\n\nThis creates an arbitrary file clobber primitive relative to the privileges of the invoking user.\n\n## Affected Behavior\n\n- Project-local config writes can affect files outside the repository\n- The most stable demonstrated sink is `pdm.toml`\n- Related sinks include `.pdm-python` and `.python-version`\n\n## Affected Code\n\n- `src/pdm/project/config.py:303-350`\n- `src/pdm/project/core.py:209-217`\n- `src/pdm/cli/commands/use.py:187-189`\n\n## Technical Details\n\n`Config.__init__()` resolves the project-local `pdm.toml` path and `_save_config()` writes to the resolved target. If `PROJECT_ROOT/pdm.toml` is a symlink to another file, `pdm config -l ...` updates the target file instead of refusing the write.\n\nThe same general problem exists for other project-local persistence paths that are written directly with no `lstat` / `O_NOFOLLOW` protection.\n\nFor the `pdm.toml` PoC specifically, the target file must already contain parseable TOML. Otherwise the load step fails before the write path is reached. That parser constraint does not apply to the `.pdm-python` or `.python-version` sinks.\n\n## Impact\n\n- Arbitrary file clobber as the invoking user\n- Destructive modification of local files outside the repository root\n- Useful primitive for privilege abuse when `pdm` is run in elevated contexts\n\n## Reproduction\n\nPoC:\n\n```bash\n# Replace this with a Python interpreter that can run `python -m pdm`.\nPDM_PY=/path/to/python-with-pdm\ntmpdir=$(mktemp -d)\ntarget=\"$tmpdir/clobbered-target.toml\"\n\ncat \u003e \"$target\" \u003c\u003c'EOF'\n[seed]\nvalue = 1\nEOF\n\nln -s \"$target\" \"$tmpdir/pdm.toml\"\n\ncat \u003e \"$tmpdir/pyproject.toml\" \u003c\u003c'EOF'\n[project]\nname = \"symlink-clobber-demo\"\nversion = \"0.0.1\"\nEOF\n\n(\n  cd \"$tmpdir\" &&\n  \"$PDM_PY\" -m pdm config -l venv.in_project false\n)\n\ncat \"$target\"\n```\n\nExpected result:\n\n- A temporary project is created\n- `pdm.toml` is a symlink to another TOML file\n- Running `pdm config -l venv.in_project false` modifies the symlink target\n\nObserved output from local validation:\n\n```text\n--- target ---\n[seed]\nvalue = 1\n\n[venv]\nin_project = false\n```\n\n## Severity\n\nMedium\n\n## CVSS v4.0\n\n- Base score: `6.8` (`Medium`)\n- Vector: `CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N`\n\nRationale:\n\n- `AV:L`: exploitation requires local execution of `pdm` against an attacker-prepared checkout\n- `AC:L`: there is no complex constraint once the symlink sink exists\n- `AT:N`: no extra prerequisite beyond the victim running the relevant command is required\n- `PR:N`: the attacker does not need prior privileges on the victim system\n- `UI:A`: the victim must actively run a command that writes project-local state or config\n- `VC:N`: the demonstrated issue is a write primitive, not a direct read primitive\n- `VI:H`: the attacker can cause unauthorized modification of files outside the repository root\n- `VA:L`: file clobber can disrupt local operation, but direct same-step availability impact is lower than a full RCE\n- `SC:N/SI:N/SA:N`: the base score is limited to the directly affected system\n\n## Root Cause\n\nProject-local file sinks are treated as trusted regular files and are written without symlink checks or guarded atomic replacement.\n\n## Recommended Remediation\n\n- Refuse to write project-local config/state files when the destination is a symlink\n- Use `lstat` and `O_NOFOLLOW` where available\n- Avoid resolving attacker-controlled project-local paths before writing\n- Use atomic temp-file replacement only after confirming the destination is a regular file\n\n## Disclosure Notes\n\nThis issue is independent from the code-execution issues above. It is best tracked as a separate CVE candidate because the root cause and remediation are different.","aliases":["CVE-2026-47763","GHSA-ghq2-5c67-fprm"],"modified":"2026-07-13T16:32:55.793163978Z","published":"2026-07-13T15:46:16.027754Z","references":[{"type":"WEB","url":"https://github.com/pdm-project/pdm/security/advisories/GHSA-ghq2-5c67-fprm"},{"type":"PACKAGE","url":"https://github.com/pdm-project/pdm"},{"type":"WEB","url":"https://github.com/pdm-project/pdm/releases/tag/2.27.0"},{"type":"PACKAGE","url":"https://pypi.org/project/pdm"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-ghq2-5c67-fprm"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-47763"}],"affected":[{"package":{"name":"pdm","ecosystem":"PyPI","purl":"pkg:pypi/pdm"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.27.0"}]}],"versions":["0.0.0","0.0.1","0.0.3","0.0.4","0.0.5","0.0.6","0.1.0","0.1.1","0.1.2","0.10.0","0.10.1","0.10.2","0.11.0","0.12.0","0.12.1","0.12.2","0.12.3","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.3.0","0.3.1","0.3.2","0.4.0","0.4.1","0.4.2","0.5.0","0.6.0","0.6.1","0.6.2","0.6.3","0.6.4","0.6.5","0.7.0","0.7.1","0.8.0","0.8.1","0.8.2","0.8.3","0.8.4","0.8.5","0.8.6","0.8.7","0.9.0","0.9.1","0.9.2","1.0.0","1.0.0b0","1.0.0b2","1.1.0","1.10.0","1.10.1","1.10.2","1.10.3","1.11.0","1.11.1","1.11.2","1.11.3","1.12.0","1.12.1","1.12.2","1.12.3","1.12.4","1.12.5","1.12.6","1.12.7","1.12.8","1.13.0","1.13.0.post0","1.13.1","1.13.2","1.13.3","1.13.4","1.13.5","1.13.6","1.14.0","1.14.1","1.15.0","1.15.1","1.15.2","1.15.3","1.15.4","1.15.5","1.2.0","1.2.0.post1","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.4.0","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.5.0","1.5.0b0","1.5.0b1","1.5.1","1.5.2","1.5.3","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.7.0","1.7.1","1.7.2","1.8.0","1.8.1","1.8.2","1.8.3","1.8.4","1.8.5","1.9.0","2.0.0","2.0.0a1","2.0.0b1","2.0.0b2","2.0.1","2.0.2","2.0.3","2.1.0","2.1.1","2.1.2","2.1.3","2.1.4","2.1.5","2.10.0","2.10.1","2.10.2","2.10.3","2.10.4","2.11.0","2.11.1","2.11.2","2.12.0","2.12.1","2.12.2","2.12.3","2.12.4","2.13.0","2.13.1","2.13.2","2.13.3","2.14.0","2.15.0","2.15.1","2.15.2","2.15.3","2.15.4","2.16.0","2.16.1","2.17.0","2.17.1","2.17.2","2.17.3","2.18.0","2.18.1","2.18.2","2.19.0","2.19.0a0","2.19.1","2.19.2","2.19.3","2.2.0","2.2.1","2.20.0","2.20.0.post1","2.20.1","2.21.0","2.22.0","2.22.1","2.22.2","2.22.3","2.22.4","2.23.0","2.23.1","2.24.0","2.24.1","2.24.2","2.25.0","2.25.1","2.25.2","2.25.3","2.25.4","2.25.5","2.25.6","2.25.7","2.25.8","2.25.9","2.26.0","2.26.1","2.26.2","2.26.3","2.26.4","2.26.5","2.26.6","2.26.7","2.26.8","2.26.9","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.4.5","2.4.6","2.4.7","2.4.8","2.4.9","2.5.0","2.5.0b0","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.5.6","2.6.0","2.6.1","2.7.0","2.7.1","2.7.2","2.7.3","2.7.4","2.8.0","2.8.0a0","2.8.0a1","2.8.0a2","2.8.1","2.8.2","2.9.0","2.9.1","2.9.2","2.9.3"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/pdm/PYSEC-2026-2862.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N"}]}