CVE-2026-35025: Verified Reproduction
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.
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).
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 — serious impact or readily exploitable. Prioritize remediation.
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 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00193/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-35025
- 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
RNFR path argument prefixed with /proc/self/root
- ProFTPD RNFR command handler
- dir_canonical_path()
- dir_check()
How the agent worked
Root Cause and Exploit Chain for CVE-2026-35025
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.cRNFR handler,src/support.cdir_canonical_path(),src/dirtree.cdir_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
RETRandRNFRtoprotected/secret.txtare denied with550 Operation not permitted, butRNFR /proc/self/root/<absolute>/protected/secret.txtreturns350,RNTOto the public directory succeeds with250 Rename successful, and the secret content is subsequently retrieved viaRETR 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
- Run
bundle/repro/reproduction_steps.sh. - The script checks out ProFTPD
v1.3.9bfrom the project cache, builds it with minimal modules, and starts the server onlocalhost:2121as the current user. - It creates an
AuthUserFilevirtual usertestuser/testpassand a directory layout withprotected/secret.txt(DenyAll) andpublic/(AllowAll). NoDefaultRoot/chrootis configured. - The Python FTP client connects, logs in, and shows that direct
RETR protected/secret.txtandRNFR protected/secret.txtare denied with550 Operation not permitted. - The client then sends
RNFR /proc/self/root/<absolute>/protected/secret.txt, receives350 File or directory exists, ready for destination name, andRNTOthe file into the public directory, receiving250 Rename successful. - Finally, the client
RETR public/leaked.txtand the original secret content is returned.
Expected evidence:
bundle/logs/reproduction_steps.logshows the full build and exploit output.bundle/logs/proftpd.logshows the server starting and the configured DenyAll/AllowAll directory blocks.bundle/repro/artifacts/ftp_exploit_output.txtcontains the FTP command responses, includingdirect_retr: DENIED,direct_rnfr: DENIED,bypass_rnfr: ALLOWED,bypass_rnto: ALLOWED,retr_public: ALLOWED, andSUCCESS: Secret content was leaked....bundle/repro/ftp-root/public/leaked.txtcontains the secret content that was originally inbundle/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.:
and the absence of a match for the<Directory /.../protected>: adding section for resolved path '/.../protected' Limit DenyAll/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), noDefaultRoot/chroot.
Recommendations / Next Steps
- Fix approach: In
dir_canonical_path()or in the RNFR handler, resolve symlink components viarealpath()/readlink()before the directory ACL check, so that/proc/self/root/...collapses to the real absolute path and matches the configured<Directory>block. Alternatively, makedir_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>DenyAllfor sensitive paths, and consider usingDefaultRoot/chroot, which the advisory notes is not affected. - Testing recommendations: Add regression tests that send
RNFRwith/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/chrootbecause 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).
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.
Artifacts and Evidence for CVE-2026-35025
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-35025
FAQ: CVE-2026-35025
Which ProFTPD versions are affected by CVE-2026-35025?
How severe is CVE-2026-35025?
How can I reproduce CVE-2026-35025?
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.