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.
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://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()
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
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).
Variant Analysis & Alternative Triggers for CVE-2026-35025
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()(DELEcommand), which uses the samedir_canonical_path()+dir_check()pattern.- Other commands in
modules/mod_core.candmodules/mod_facts.cthat calldir_canonical_path()followed bydir_check()(e.g.,MDTM,SIZE,MFMT,MLSTfact handlers).
Variant / Alternate Trigger
Entry point: authenticated FTP
DELEcommand.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/RNTOto rename and retrieve a protected file; this variant usesDELEto delete a protected file, and it bypasses the proposedRNFR-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 proposedRNFR-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 theDELEcommand with a/proc/self/root-prefixed path. The same variant also bypasses the proposedRNFR-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
- Run
bundle/vuln_variant/reproduction_steps.sh. - 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
RNFRfix) on port 2122.
- For the vulnerable server, it confirms both the original
RNFRexploit and theDELEvariant succeed. - For the patched server, it confirms the
RNFRexploit is blocked but theDELEvariant still succeeds. - Exit code is
0when the bypass is confirmed.
Expected evidence:
bundle/logs/variant_reproduction_steps.logshows the sequence of server starts and client results.bundle/logs/proftpd_vuln_variant.logandbundle/logs/proftpd_patched_variant.logcontain ProFTPD debug output.- The vulnerable client output shows
vuln_rnfr: ALLOWEDandvuln_dele: ALLOWED. - The patched client output shows
patched_rnfr: DENIEDandpatched_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 theDELEvariant on the vulnerable binary.bundle/vuln_variant/test_dele_patched.sh— early standalone test of theDELEvariant against the patched binary.- Local patched source worktree:
/data/pruva/project-cache/e16fa440-7670-4503-8601-378cf2096f7e/repo-patchedwith the diff applied tomodules/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()incore_rnfr()
Recommendations / Next Steps
- Extend the fix to
DELE: changecore_dele()inmodules/mod_core.cto usedir_check_canon()afterdir_canonical_path(). - Audit all
dir_canonical_path()+dir_check()sites: specifically reviewmodules/mod_core.c(other symlink-aware commands) andmodules/mod_facts.c(MDTM,SIZE,MFMT,MLST) to ensure none are left vulnerable to the same/proc/self/rootprefix trick. - Add regression tests: test
RNFR,DELE, and any other affected commands with/proc/self/root-prefixed paths on both non-chrooted and chrooted configurations. - Consider a central fix: either make
dir_canonical_path()symlink-aware for ACL purposes, or consistently usedir_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.
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.