Skip to content

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.

REPRO-2026-00193 proftpd/proftpd · github 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://www.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()
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).

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.

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.