# CVE-2026-35025 ProFTPD ACL Bypass – Fix Verification Report

## Fix Summary

The vulnerability is a post-authentication ACL bypass: an authenticated FTP user can prefix a path with `/proc/self/root` so that `dir_canonical_path()` performs only lexical cleaning and leaves the procfs symlink prefix intact. The subsequent `dir_check()` then does a lexical `<Directory>` match and misses the intended `DenyAll` block. The upstream/proposed one-line fix changed `core_rnfr()` to use `dir_check_canon()`, but the variant stage showed that the same weakness is reachable through `DELE`. This patch applies the same remedy to both `core_rnfr()` and `core_dele()` by replacing their `dir_check()` calls with `dir_check_canon()`, which resolves symlinks (via `dir_best_path()` / `realpath`-style logic) before the ACL comparison. This makes `/proc/self/root/.../protected/secret.txt` collapse to the real filesystem path and match the configured `DenyAll` block for the protected directory.

## Changes Made

- **`modules/mod_core.c`** (2 hunks)
  - `core_dele()` (line ~6099): changed `!dir_check(..., path, NULL)` to `!dir_check_canon(..., path, NULL)` after the `dir_canonical_path()` call.
  - `core_rnfr()` (line ~6479): changed `!dir_check(..., path, NULL)` to `!dir_check_canon(..., path, NULL)` in the `path == NULL || ... || !exists2(...)` check.

No other files are modified. The change is minimal and follows the existing pattern already used by `core_rnto()`, `core_mkd()`, and `core_rmd()` in the same file, which already call `dir_check_canon()` after canonicalization.

## Verification Steps

1. **Source preparation**
   - Checked out the vulnerable source at tag `v1.3.9b` (commit `390b21555`) from the project cache.
   - Applied `bundle/coding/proposed_fix.diff` with `git apply`.
2. **Build**
   - Re-used the existing `build-vuln` directory as a template for `build-fix` to avoid a full reconfigure, then ran `make` to recompile the changed `modules/mod_core.c` and relink `proftpd`.
3. **Runtime test setup**
   - Created a fresh FTP root with `protected/` (DenyAll) and `public/` (AllowAll) directories.
   - Generated an `AuthUserFile`/`AuthGroupFile` virtual user mapping to the current UID/GID.
   - Started the patched `proftpd` on `localhost:2123` with no `DefaultRoot`/`chroot` (the vulnerable configuration).
4. **FTP client tests** (Python `ftplib`)
   - Direct `RNFR protected/secret.txt` → expect 550 denial.
   - `RNFR /proc/self/root/<abs>/protected/secret.txt` → expect 550 denial.
   - Direct `DELE protected/secret2.txt` → expect 550 denial.
   - `DELE /proc/self/root/<abs>/protected/secret.txt` → expect 550 denial.
   - Normal `RNFR public/file.txt` + `RNTO public/file_renamed.txt` → expect success.
   - Normal `DELE public/file2.txt` → expect success.

## Test Results

```
ProFTPD ready on port 2123
=== Connecting to patched ProFTPD ===
Logged in
Direct RNFR protected: DENIED 550
/proc/self/root RNFR protected: DENIED 550
Direct DELE protected: DENIED 550
/proc/self/root DELE protected: DENIED 550

=== Regression checks for normal operations ===
Normal RNFR/RNTO: OK
Normal DELE: OK

SUCCESS: all bypass attempts are blocked and normal operations still work.
```

The patch successfully blocks both the originally disclosed `RNFR` bypass and the `DELE` variant, while leaving legitimate rename and delete operations on allowed directories functional.

## Remaining Concerns and Recommendations

- **Other commands:** A code audit shows that `modules/mod_facts.c` (`facts_mff()`, `facts_mfmt()`) also uses `dir_canonical_path()` followed by `dir_check()`. These were not part of the demonstrated exploit chain and are not addressed by this patch. A future hardening pass should either switch them to `dir_check_canon()` or centralize the ACL path resolution so that all path-based `<Directory>` checks are performed on real filesystem paths.
- **Non-chroot requirement:** The bypass only works when the server is not chrooted (`DefaultRoot`/`chroot`). The fix is still correct for chrooted configurations because `dir_check_canon()` resolves the path within the chroot, but the recommended deployment posture is to use chroot for sensitive environments.
- **Testing edge cases:** The verification used absolute `/proc/self/root` prefixes. Additional regression tests should cover relative paths, paths with multiple symlinks, and dangling symlinks to ensure `dir_check_canon()` does not introduce regressions for symlink rename/delete semantics.
- **Upstream coordination:** Once the upstream issue (`proftpd/proftpd#2170`) lands an official fix, this patch should be reviewed against it; the approach taken here is consistent with the maintainer’s `dir_check_canon()` direction but extends it to the `DELE` command as required by the variant analysis.
