From ba22647dbb33db294ec5bb7a153d7e2863290913 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 15 Jun 2026 20:34:20 -0400 Subject: [PATCH] ext/ftp: fix out-of-bounds read in ftp_get() ASCII CRLF translation In ASCII mode ftp_get() scans each received block for '\r' and peeks at the next byte to collapse a CRLF pair to '\n'. When the '\r' is the last byte of a full FTP_BUFSIZE block, the *(s + 1) lookahead reads one byte past the data buffer; a server placing '\r' at offset 4095 of a 4096-byte read triggers it. Bound the lookahead to the received data, matching the guard ftp_readline() already uses. ftp_nb_continue_read() carries the trailing '\r' across reads via ftp->lastch and is unaffected. Closes GH-22328 --- ext/ftp/ftp.c | 2 +- .../tests/ftp_get_ascii_crlf_boundary.phpt | 25 +++++++++++++++++++ ext/ftp/tests/server.inc | 7 ++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index 36a0d1697eca..ca5e05ead81b 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -946,7 +946,7 @@ ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_t pat #else while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) { php_stream_write(outstream, ptr, (s - ptr)); - if (*(s + 1) == '\n') { + if (s + 1 < e && *(s + 1) == '\n') { s++; php_stream_putc(outstream, '\n'); } diff --git a/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt b/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt new file mode 100644 index 000000000000..4c3a70b647c6 --- /dev/null +++ b/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt @@ -0,0 +1,25 @@ +--TEST-- +ftp_get() ASCII mode: CRLF straddling the FTP_BUFSIZE read boundary +--EXTENSIONS-- +ftp +pcntl +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index c2c8449a0686..4c9d2a754bff 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -391,6 +391,13 @@ if ($pid) { // Just a side channel for getting the received file size. fputs($s, "425 Can't open data connection (".$GLOBALS['rest_pos'].").\r\n"); break; + case "crlf_boundary": + // A CRLF whose CR lands on the final byte of the first + // FTP_BUFSIZE (4096) read, so the LF arrives in the next read. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10)); + fputs($s, "226 Closing data Connection.\r\n"); + break; default: fputs($s, "550 {$matches[1]}: No such file or directory \r\n");