Skip to content

ftp: reject PWD responses containing control characters#20949

Closed
flightlesstux wants to merge 10 commits intocurl:masterfrom
flightlesstux:fix-ftp-pwd-control-chars
Closed

ftp: reject PWD responses containing control characters#20949
flightlesstux wants to merge 10 commits intocurl:masterfrom
flightlesstux:fix-ftp-pwd-control-chars

Conversation

@flightlesstux
Copy link
Copy Markdown
Contributor

@flightlesstux flightlesstux commented Mar 17, 2026

What

The ftp_pwd_resp() function in lib/ftp.c parses the quoted directory name out of a server's 257 PWD response and stores it as ftpc->entrypath. On connection reuse that string is passed directly to Curl_pp_sendf() as:

result = Curl_pp_sendf(data, &ftpc->pp, "CWD %s", ftpc->entrypath);

Curl_pp_vsendf() performs no sanitization - it formats the string, appends \r\n, and sends it. Because the extraction loop accepted any byte except \0 and ", a server could embed bare \r or other control characters inside the quoted path. Those bytes survive into the CWD command sent on the next connection reuse.

Example malicious 257 response:

257 "/legitimate/path\rSTOR evil.txt" is current directory

Some FTP server implementations treat a bare \r as a command separator, which turns this into a second injected command.

Fix

Reject the entire path during extraction if any byte with value < 0x20 or == 0x7f is encountered. This is consistent with how lib/cookie.c handles control bytes in cookie values.

Impact

Requires connecting to a malicious or MITM'd FTP server. The injected command is limited to what the server's own credentials allow. Severity is moderate but the fix is a one-liner guard with no effect on legitimate servers.

A malicious or compromised FTP server could include control characters
(e.g. bare \r, or bytes 0x01-0x1f/0x7f) inside the quoted directory
path of its 257 PWD response. That string is stored verbatim as
ftpc->entrypath and later sent unescaped in a CWD command on connection
reuse via Curl_pp_sendf(), which performs no sanitization before
appending \r\n.

Reject the entire path if any control character is encountered during
extraction so that tainted data never reaches a subsequent FTP command.
@github-actions github-actions bot added the FTP label Mar 17, 2026
@bagder
Copy link
Copy Markdown
Member

bagder commented Mar 17, 2026

It would also be cool to have a test case verifying this.

Something like this:

<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
FTP
PASV
RETR
</keywords>
</info>
# Server-side
<reply>
<data>
we can still send data even if pwd fails!
</data>
<servercmd>
REPLY PWD 257 %hex["/%03"]hex%
</servercmd>
</reply>

# Client-side
<client>
<server>
ftp
</server>
<name>
FTP with control characters in PWD response
</name>
<command>
ftp://%HOSTIP:%FTPPORT/%TESTNUMBER
</command>
</client>

# Verify data after the test has been "shot"
<verify>
<protocol crlf="yes">
USER anonymous
PASS ftp@example.com
PWD
EPSV
TYPE I
SIZE %TESTNUMBER
RETR %TESTNUMBER
QUIT
</protocol>
</verify>
</testcase>

Verify that curl rejects a 257 PWD response that contains a control
character inside the quoted path, returning CURLE_WEIRD_SERVER_REPLY.
@github-actions github-actions bot added the tests label Mar 17, 2026
@flightlesstux
Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion! I've added test3217 that uses the hex encoding to inject a control character (0x03) into the PWD response and verifies curl bails out with error code 8. Pushed it in the second commit. Let me know if the test format needs any tweaking or if you'd like a different test number range.

@bagder
Copy link
Copy Markdown
Member

bagder commented Mar 17, 2026

You did not incorporate my other proposals though, one of which causes the build error. Which indicates you did not compile this locally?

Instead of using a server-supplied entry path that contains control
characters (bytes < 0x20 or 0x7f), discard it silently and continue.

On connection reuse curl sends 'CWD <entrypath>' to return to the
starting directory. Accepting control characters in that path would let
a malicious server inject bytes into a subsequent FTP command.

Adjust test3217 accordingly: the transfer still succeeds, curl just
has no stored entry path for that connection.
Use curl's own ISCNTRL() macro from curl_ctype.h instead of an open
coded check, and return CURLE_WEIRD_SERVER_REPLY when control characters
are found in the server-supplied PWD path.

Update test3217 to expect error code 8 and the truncated protocol
exchange that results from rejecting the bad path.
@flightlesstux
Copy link
Copy Markdown
Contributor Author

Thanks for catching that. I've applied both inline suggestions - switched to ISCNTRL() with the curl_ctype.h include, and changed it back to returning CURLE_WEIRD_SERVER_REPLY with proper cleanup before the return. Updated the test to expect error code 8 and the shortened protocol exchange. Should be good now.

\r and \n signal end-of-line in unclosed-quote PWD responses and must
only stop path extraction (break), not abort the connection. Other
control characters (SOH, STX, BEL, etc.) have no legitimate place in
a file path and trigger CURLE_WEIRD_SERVER_REPLY.

Also add test3217 to tests/data/Makefile.am so the test runner picks
it up without a warning.
flightlesstux and others added 5 commits March 17, 2026 15:21
Remove the special leniency for \r and \n inside quoted PWD paths.
All control characters now uniformly return CURLE_WEIRD_SERVER_REPLY.
Add test3218 to verify CR in PWD path is rejected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a server sends a PWD response with an unclosed quote followed by
the normal CRLF line terminator (e.g. 257 "just one\r\n), the \r was
being treated as an injected control character and caused
CURLE_WEIRD_SERVER_REPLY. This broke test 1152.

Distinguish between a bare CR/LF that ends the response line (unclosed
quote case -- just break and ignore the path) and a CR or other control
character embedded inside a properly-quoted path (injected -- error).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the special leniency for CR/LF in PWD path parsing. Any control
character inside a quoted PWD path, including CR and LF, is treated as
a protocol error and returns CURLE_WEIRD_SERVER_REPLY.

Update test1152 (unclosed quote in PWD response) to expect error code 8
since the CR line terminator inside the unclosed quote now triggers the
same control-character error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the <data> block and update keywords since curl now errors out
at the PWD stage (CURLE_WEIRD_SERVER_REPLY) before any data transfer
occurs when the server sends an unclosed quote in the PWD response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When curlx_dyn_addn fails inside the PWD path parsing loop, the dynbuf
must be freed before returning. The missing curlx_dyn_free caused a
memory leak detected by the debug build leak tracker, which aborted the
process in torture tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bagder
Copy link
Copy Markdown
Member

bagder commented Mar 18, 2026

augment review

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Mar 18, 2026

🤖 Augment PR Summary

Summary: This PR hardens FTP handling by rejecting malicious/invalid PWD (257) responses that contain ASCII control characters, preventing them from being persisted and later reused in commands.

Changes:

  • Updates ftp_pwd_resp() in lib/ftp.c to abort extraction if any control byte is seen in the quoted path (via ISCNTRL()), returning CURLE_WEIRD_SERVER_REPLY.
  • Adds proper dynbuf cleanup on early error returns during path extraction.
  • Adjusts existing FTP test test1152 to expect failure on an invalid/unclosed-quote PWD response.
  • Adds new regression tests (test3217, test3218) that inject control bytes into the PWD path using hex encoding.
  • Registers the new test cases in tests/data/Makefile.am.

Technical Notes: The new guard prevents control characters (e.g., CR/LF) from reaching later CWD formatting/sending logic, mitigating potential command-injection behavior on some FTP servers.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@bagder
Copy link
Copy Markdown
Member

bagder commented Mar 18, 2026

Thanks!

@bagder bagder closed this in c3f04e7 Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants