Skip to content

CVE-2026-35025: Verified Repro With Script Download

CVE-2026-35025: ProFTPD ACL bypass via /proc/self/root path prefix in RNFR

CVE-2026-35025 is verified against proftpd/proftpd · github. Affected versions: ProFTPD through 1.3.9b and through 1.3.10rc2. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00193.

REPRO-2026-00193 proftpd/proftpd · github Variant found Jul 1, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
8.1
Confidence
HIGH
Reproduced in
88m 18s
Tool calls
258
Spend
$3.79
01 · Overview

What Is CVE-2026-35025?

CVE-2026-35025 is a high-severity ACL bypass in ProFTPD, categorized as CWE-59 (Improper Link Resolution Before File Access / Link Following), triggered via a /proc/self/root path prefix used with the RNFR (rename-from) FTP command. Pruva reproduced it (reproduction REPRO-2026-00193).

02 · Severity & CVSS

CVE-2026-35025 Severity & CVSS Score

CVE-2026-35025 is rated high severity, with a CVSS base score of 8.1 out of 10.

HIGH threat level
8.1 / 10 CVSS base
Weakness CWE-59 Improper Link Resolution Before File Access ('Link Following')

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected proftpd/proftpd Versions

proftpd/proftpd · github versions ProFTPD through 1.3.9b and through 1.3.10rc2 are affected.

How to Reproduce CVE-2026-35025

$ pruva-verify REPRO-2026-00193
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00193/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-35025

Authorization bypass — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

RNFR path argument prefixed with /proc/self/root

Attack chain
  1. ProFTPD RNFR command handler
  2. dir_canonical_path()
  3. dir_check()
Variants tested

Bypass of the proposed CVE-2026-35025 RNFR-only fix using the DELE command, which shares the same dir_canonical_path()+dir_check() root cause.

How the agent worked 673 events · 258 tool calls · 1h 28m
1h 28mDuration
258Tool calls
202Reasoning steps
673Events
4Dead-ends
Agent activity over 1h 28m
Support
31
Hypothesis
2
Repro
187
Judge
23
Variant
255
Coding
170
0:0088:18

Root Cause and Exploit Chain for CVE-2026-35025

Versions: ProFTPD through 1.3.9b and 1.3.10rc2 (issue #2170).

CVE-2026-35025 is a post-authentication access-control bypass in ProFTPD. An authenticated FTP user can evade a <Directory> block configured with DenyAll by prefixing the absolute path in an RNFR command with /proc/self/root. Because ProFTPD's dir_canonical_path() cleans the path lexically without resolving the /proc/self/root symlink, the resulting canonical path still contains the prefix. dir_check() then performs a lexical match against configured <Directory> blocks; the /proc/self/root-prefixed path does not match the intended protected-directory block, so the DenyAll policy is skipped. The attacker can then RNTO the protected file into an allowed directory and RETR it. Configurations using DefaultRoot/chroot are not affected because /proc/self/root resolves relative to the chroot.

  • Package/component affected: ProFTPD FTP server (modules/mod_core.c RNFR handler, src/support.c dir_canonical_path(), src/dirtree.c dir_check()/dir_match_path()).
  • Affected versions: ProFTPD through 1.3.9b and 1.3.10rc2 (issue #2170).
  • Risk level: High (CVSS 3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N, score 8.1; CVSS 4.0 8.6).
  • Consequences: Authenticated users can bypass directory-level ACLs to rename and read files located in paths that should be protected by DenyAll.

Impact Parity

  • Disclosed/claimed maximum impact: Access-control bypass allowing file rename and retrieval from DenyAll-protected directories.
  • Reproduced impact from this run: Confirmed authenticated FTP access-control bypass: direct RETR and RNFR to protected/secret.txt are denied with 550 Operation not permitted, but RNFR /proc/self/root/<absolute>/protected/secret.txt returns 350, RNTO to the public directory succeeds with 250 Rename successful, and the secret content is subsequently retrieved via RETR public/leaked.txt.
  • Parity: full.
  • Not demonstrated: No remote code execution or crash was claimed; the claim was limited to ACL bypass and file leakage, which was fully demonstrated.

Root Cause

In the RNFR command handler (modules/mod_core.c around core_rnto/RNFR logic), the supplied path is cleaned with dir_canonical_path():

abs_path = dir_abs_path(cmd->tmp_pool, path, FALSE);
path = dir_canonical_path(cmd->tmp_pool, path);

dir_canonical_path() (src/support.c) concatenates the path with the current working directory and then calls pr_fs_clean_path2(), which performs only lexical normalization (removing ., .., duplicate /, etc.). It does not call readlink() or otherwise resolve symlink components. Therefore a path such as:

/proc/self/root/data/.../protected/secret.txt

remains literally that string after canonicalization. On a non-chrooted Linux server, /proc/self/root is a symlink to the real root, so the path ultimately resolves to the same file as /data/.../protected/secret.txt, but the canonical string retains the prefix.

Next, the handler calls dir_check() (src/dirtree.c) to enforce <Directory>/Limit policies. dir_check() relies on dir_match_path() to locate the matching <Directory> block. dir_match_path() does a prefix/string comparison of the canonical path against configured directory names. The configured protected block (e.g., /data/.../protected) does not match the /proc/self/root/.../protected/secret.txt string, so no DenyAll block is found and the operation is allowed. The file is then accepted as a valid rename source and stored in session.xfer.path, after which RNTO and RETR operate on the file normally.

  • Fix commit: Not yet available in the upstream repository at the time of this run (issue #2170 remains open, CVE published 2026-06-24). A robust fix would resolve symlink components before the directory-policy check, or teach dir_check()/dir_match_path() to compare the real filesystem path rather than the lexical canonical string.

Reproduction Steps

  1. Run bundle/repro/reproduction_steps.sh.
  2. The script checks out ProFTPD v1.3.9b from the project cache, builds it with minimal modules, and starts the server on localhost:2121 as the current user.
  3. It creates an AuthUserFile virtual user testuser/testpass and a directory layout with protected/secret.txt (DenyAll) and public/ (AllowAll). No DefaultRoot/chroot is configured.
  4. The Python FTP client connects, logs in, and shows that direct RETR protected/secret.txt and RNFR protected/secret.txt are denied with 550 Operation not permitted.
  5. The client then sends RNFR /proc/self/root/<absolute>/protected/secret.txt, receives 350 File or directory exists, ready for destination name, and RNTO the file into the public directory, receiving 250 Rename successful.
  6. Finally, the client RETR public/leaked.txt and the original secret content is returned.

Expected evidence:

  • bundle/logs/reproduction_steps.log shows the full build and exploit output.
  • bundle/logs/proftpd.log shows the server starting and the configured DenyAll/AllowAll directory blocks.
  • bundle/repro/artifacts/ftp_exploit_output.txt contains the FTP command responses, including direct_retr: DENIED, direct_rnfr: DENIED, bypass_rnfr: ALLOWED, bypass_rnto: ALLOWED, retr_public: ALLOWED, and SUCCESS: Secret content was leaked....
  • bundle/repro/ftp-root/public/leaked.txt contains the secret content that was originally in bundle/repro/ftp-root/protected/secret.txt.

Evidence

  • bundle/logs/reproduction_steps.log: full build and exploit trace.
  • bundle/logs/proftpd.log: ProFTPD daemon logs confirming the vulnerable configuration, e.g.:
    <Directory /.../protected>: adding section for resolved path '/.../protected'
    Limit DenyAll
    
    and the absence of a match for the /proc/self/root-prefixed path.
  • bundle/repro/artifacts/ftp_exploit_output.txt:
    Direct RETR denied: 550 protected/secret.txt: Operation not permitted
    Direct RNFR denied: 550 protected/secret.txt: Operation not permitted
    RNFR response: 350 File or directory exists, ready for destination name
    RNTO response: 250 Rename successful
    RETR line: This is the secret content that should not be accessible via normal FTP.
    SUCCESS: Secret content was leaked via RNFR /proc/self/root bypass!
    
  • Environment: Ubuntu container, ProFTPD v1.3.9b (390b21555), no DefaultRoot/chroot.

Recommendations / Next Steps

  • Fix approach: In dir_canonical_path() or in the RNFR handler, resolve symlink components via realpath()/readlink() before the directory ACL check, so that /proc/self/root/... collapses to the real absolute path and matches the configured <Directory> block. Alternatively, make dir_match_path()/dir_check() compare the physical filesystem path rather than the lexical string. Care must be taken to preserve ProFTPD's chroot-aware semantics.
  • Upgrade guidance: Apply the upstream fix once released; until then, avoid relying solely on <Directory> DenyAll for sensitive paths, and consider using DefaultRoot/chroot, which the advisory notes is not affected.
  • Testing recommendations: Add regression tests that send RNFR with /proc/self/root, /proc/self/cwd, and other procfs symlink prefixes against protected directories, and verify they are denied.

Additional Notes

  • Idempotency: The reproduction script was executed twice consecutively and produced the same successful result both times.
  • Edge cases/limitations: The proof intentionally disables DefaultRoot/chroot because the vulnerability is documented as not affecting chrooted sessions. The reproduction is a non-privileged server running on port 2121; it demonstrates the same code path as a production server. The exploit requires valid FTP credentials (post-authentication).

Variant Analysis & Alternative Triggers for CVE-2026-35025

Versions: versions (as tested): ProFTPD v1.3.9b (commit 390b21555) and the same commit with the proposed RNFR-only patch applied.

The disclosed CVE-2026-35025 vulnerability is an authenticated FTP access-control bypass in ProFTPD: an attacker can prefix an absolute path with /proc/self/root in the RNFR command so that dir_canonical_path() leaves the symlink prefix in place, and the subsequent lexical dir_check() fails to match the configured DenyAll <Directory> block. This variant shows that the same root cause is reachable through a different FTP command: DELE. The core_dele() handler uses the same dir_canonical_path() + dir_check() pattern, so a /proc/self/root-prefixed path bypasses the directory ACL and deletes a protected file. We also tested the upstream/proposed one-line fix (changing core_rnfr() to use dir_check_canon()) and confirmed that DELE still bypasses the patch.

Fix Coverage / Assumptions

The fix proposed in the upstream issue discussion (proftpd/proftpd#2170) changes only one line in modules/mod_core.c:

-      !dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||
+      !dir_check_canon(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||

inside core_rnfr(). It relies on the assumption that RNFR is the only command that feeds the output of dir_canonical_path() into dir_check(). It explicitly covers the RNFR command path: core_rnfr() -> dir_canonical_path() -> dir_check_canon() -> dir_best_path() -> dir_check().

What the fix does not cover:

  • core_dele() (DELE command), which uses the same dir_canonical_path() + dir_check() pattern.
  • Other commands in modules/mod_core.c and modules/mod_facts.c that call dir_canonical_path() followed by dir_check() (e.g., MDTM, SIZE, MFMT, MLST fact handlers).

Variant / Alternate Trigger

  • Entry point: authenticated FTP DELE command.

  • Attacker-controlled input: absolute path argument prefixed with /proc/self/root, e.g., DELE /proc/self/root/ftp-root/protected/secret.txt.

  • Code path:

    • modules/mod_core.c:core_dele()
    • src/support.c:dir_canonical_path() (lexical canonicalization, leaves /proc/self/root)
    • src/dirtree.c:dir_check() (lexical <Directory> match misses the protected block)
    • src/fsio.c:pr_fsio_unlink() (deletes the file at the resolved real path)
  • Difference from parent claim: the parent claim uses RNFR/RNTO to rename and retrieve a protected file; this variant uses DELE to delete a protected file, and it bypasses the proposed RNFR-only fix.

  • Package/component affected: ProFTPD FTP server (modules/mod_core.c:core_dele(), src/support.c:dir_canonical_path(), src/dirtree.c:dir_check()).

  • Affected versions (as tested): ProFTPD v1.3.9b (commit 390b21555) and the same commit with the proposed RNFR-only patch applied.

  • Risk level: High (same CVSS family as the parent CVE; post-authentication ACL bypass with integrity impact on protected files).

  • Consequences: An authenticated FTP user can delete files inside directories protected by DenyAll <Directory>/<Limit> blocks.

Impact Parity

  • Disclosed/claimed maximum impact: Access-control bypass allowing file rename and retrieval from DenyAll-protected directories.
  • Reproduced impact from this variant run: Access-control bypass allowing deletion of a file inside a DenyAll-protected directory using the DELE command with a /proc/self/root-prefixed path. The same variant also bypasses the proposed RNFR-only fix.
  • Parity: partial — the root cause and bypass mechanism are the same, but the demonstrated impact is file deletion rather than rename/retrieval.
  • Not demonstrated: reading the contents of a protected file via DELE (deletion does not provide read access), remote code execution, or privilege escalation beyond the authenticated user.

Root Cause

The root cause is identical to CVE-2026-35025: dir_canonical_path() in src/support.c performs only lexical path cleaning (collapsing . and .., removing duplicate slashes) without resolving symbolic links. When a path such as /proc/self/root/ftp-root/protected/secret.txt is passed to dir_canonical_path(), the result still contains the /proc/self/root prefix. dir_check() then matches this string against <Directory> configuration blocks; because the configured block is /ftp-root/protected and not /proc/self/root/ftp-root/protected, the DenyAll policy is skipped. The filesystem operation is performed on the real path because /proc/self/root resolves to the process root at the kernel level.

The proposed fix resolves symlinks for RNFR by calling dir_check_canon(), which internally uses dir_best_path() (a realpath()-style resolver). But DELE was left unchanged, so the same bypass is still possible after the patch.

Reproduction Steps

  1. Run bundle/vuln_variant/reproduction_steps.sh.
  2. The script builds the test environment and starts two ProFTPD instances:
    • Vulnerable ProFTPD v1.3.9b on port 2121.
    • Patched ProFTPD v1.3.9b (with the proposed RNFR fix) on port 2122.
  3. For the vulnerable server, it confirms both the original RNFR exploit and the DELE variant succeed.
  4. For the patched server, it confirms the RNFR exploit is blocked but the DELE variant still succeeds.
  5. Exit code is 0 when the bypass is confirmed.

Expected evidence:

  • bundle/logs/variant_reproduction_steps.log shows the sequence of server starts and client results.
  • bundle/logs/proftpd_vuln_variant.log and bundle/logs/proftpd_patched_variant.log contain ProFTPD debug output.
  • The vulnerable client output shows vuln_rnfr: ALLOWED and vuln_dele: ALLOWED.
  • The patched client output shows patched_rnfr: DENIED and patched_dele: ALLOWED.

Evidence

  • bundle/logs/variant_reproduction_steps.log — full console output from both reproduction runs.
  • bundle/logs/proftpd_vuln_variant.log — vulnerable server logs.
  • bundle/logs/proftpd_patched_variant.log — patched server logs.
  • bundle/vuln_variant/test_dele_variant.sh — early standalone test of the DELE variant on the vulnerable binary.
  • bundle/vuln_variant/test_dele_patched.sh — early standalone test of the DELE variant against the patched binary.
  • Local patched source worktree: /data/pruva/project-cache/e16fa440-7670-4503-8601-378cf2096f7e/repo-patched with the diff applied to modules/mod_core.c.

Key excerpt from the final reproduction:

[ PATCHED ] RNFR exploit (should be blocked)
RNFR denied: 550 /proc/self/root/.../protected/secret.txt: Operation not permitted
[ PATCHED ] DELE variant (should still bypass)
DELE allowed
BYPASS CONFIRMED: proposed RNFR fix leaves DELE path open

Environment details:

  • Host: Linux container, x86_64
  • User: vscode (uid 1000, gid 1000)
  • ProFTPD built from source at v1.3.9b (commit 390b21555)
  • Patched build: same commit, plus one-line change dir_check() -> dir_check_canon() in core_rnfr()

Recommendations / Next Steps

  1. Extend the fix to DELE: change core_dele() in modules/mod_core.c to use dir_check_canon() after dir_canonical_path().
  2. Audit all dir_canonical_path() + dir_check() sites: specifically review modules/mod_core.c (other symlink-aware commands) and modules/mod_facts.c (MDTM, SIZE, MFMT, MLST) to ensure none are left vulnerable to the same /proc/self/root prefix trick.
  3. Add regression tests: test RNFR, DELE, and any other affected commands with /proc/self/root-prefixed paths on both non-chrooted and chrooted configurations.
  4. Consider a central fix: either make dir_canonical_path() symlink-aware for ACL purposes, or consistently use dir_check_canon() whenever the canonicalized path is used for permission enforcement.

Additional Notes

  • The script was run twice successfully and produced the same results, confirming idempotency.
  • The vulnerable repository checkout and the patched worktree were left in their original states; no mutation was performed on the source used by the repro stage (/data/pruva/project-cache/e16fa440-7670-4503-8601-378cf2096f7e/repo).
  • Upstream has not yet released a fix; the patched version tested here is the likely minimal fix discussed in the public issue.

CVE-2026-35025 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:004:11
0:00
session startedaccounts/fireworks/models/kimi-k2p7-code · CVE-2026-35025 · REPRO-20
0:04
0:06
web search
0:10
0:11
0:13
web search
0:16
0:17
web search
0:22
0:23
web search
0:28
0:29
web search
0:35
0:37
0:47
0:49
web search
0:56
0:57
2:02
2:02
extract_facts
no facts extracted
2:07
2:07
2:07
supportrepro
4:03
4:06
4:06
4:06
4:11

Artifacts and Evidence for CVE-2026-35025

Scripts, logs, diffs, and output captured during the reproduction.

bundle/repro/ftp-root/public/leaked.txt0.1 KB
bundle/repro/proftpd.group0.0 KB
bundle/repro/artifacts/ftp_exploit_output.txt4.7 KB
bundle/repro/proftpd.conf1.0 KB
bundle/repro/runtime_manifest.json0.7 KB
bundle/repro/ftp_exploit.py3.7 KB
bundle/repro/proftpd.passwd0.2 KB
bundle/repro/validation_verdict.json0.7 KB
bundle/ticket.json1.7 KB
bundle/ticket.md0.9 KB
bundle/logs/proftpd.log3.6 KB
bundle/logs/reproduction_steps.log532.9 KB
bundle/logs/dele_variant_patched_test.log0.5 KB
bundle/logs/proftpd_patched_variant.log3.7 KB
bundle/logs/proftpd_dele_patched.log3.7 KB
bundle/logs/proftpd_dele.log3.6 KB
bundle/logs/proftpd_patched_configure.log21.0 KB
bundle/logs/proftpd_vuln_variant.log3.7 KB
bundle/logs/proftpd_patched_build.log26.0 KB
bundle/logs/variant_reproduction_steps.log1.9 KB
bundle/logs/dele_variant_test.log3.6 KB
bundle/repro/reproduction_steps.sh12.9 KB
bundle/repro/rca_report.md7.7 KB
bundle/coding/logs/proftpd_build.log4.5 KB
bundle/coding/logs/proftpd_verify.log3.7 KB
bundle/coding/proposed_fix.diff0.7 KB
bundle/coding/summary_report.md4.7 KB
bundle/coding/verify_env/ftp-root/protected/secret.txt0.0 KB
bundle/coding/verify_env/ftp-root/protected/secret2.txt0.0 KB
bundle/coding/verify_env/ftp-root/public/file_renamed.txt0.0 KB
bundle/coding/verify_env/proftpd.conf1.1 KB
bundle/coding/verify_env/proftpd.group0.0 KB
bundle/coding/verify_env/proftpd.passwd0.2 KB
bundle/coding/verify_fix.sh6.7 KB
bundle/vuln_variant/patch_analysis.md5.9 KB
bundle/vuln_variant/rca_report.md8.1 KB
bundle/vuln_variant/reproduction_steps.sh9.5 KB
bundle/vuln_variant/root_cause_equivalence.json1.6 KB
bundle/vuln_variant/runtime_manifest.json0.9 KB
bundle/vuln_variant/source_identity.json1.0 KB
bundle/vuln_variant/test_dele_patched.sh5.1 KB
bundle/vuln_variant/test_dele_variant.sh4.5 KB
bundle/vuln_variant/validation_verdict.json1.0 KB
bundle/vuln_variant/variant_manifest.json2.7 KB
08 · How to Fix

How to Fix CVE-2026-35025

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-35025 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-35025

Which ProFTPD versions are affected by CVE-2026-35025?

ProFTPD through 1.3.9b and through 1.3.10rc2 are affected. This record does not specify a fixed version.

How severe is CVE-2026-35025?

It is rated high severity. No CVSS score is recorded for this reproduction.

How can I reproduce CVE-2026-35025?

Download the verified script from this page and run it in an isolated environment against ProFTPD through 1.3.9b or 1.3.10rc2. It issues an RNFR command using a /proc/self/root path prefix to demonstrate the ACL bypass.
11 · References

References for CVE-2026-35025

Authoritative sources for CVE-2026-35025 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.