Skip to content

Commit 7774891

Browse files
alepachecondossche
authored andcommitted
Fix out-of-bounds reads parsing ftp:// wrapper server replies
The ftp:// wrapper parses replies in a 512-byte buffer at fixed offsets and sizes that are not tied to the line actually received: - get_ftp_result() returns the code of the last line read when the stream ends, even if it is not a complete "ddd " status line, while the callers parse the reply text from tmp_line + 4. - php_fopen_do_pasv() copies a fixed ip_size bytes from the dotted address inside the buffer, reading past its end when the address is near the end of the line, and silently truncating an over-long one. - The MDTM parsing in php_stream_ftp_url_stat() skips non-digits past the terminating NUL, and its bound check allows sscanf() to start one past the end of the buffer. Only return a code from get_ftp_result() for a complete status line, copy exactly the parsed address and reject one that does not fit, and stop the MDTM scan at the end of the line. Closes GH-24340.
1 parent fd99679 commit 7774891

5 files changed

Lines changed: 73 additions & 8 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ PHP NEWS
343343
parsing fails after Transfer-Encoding). (bercedev)
344344
. Fixed bug GH-24509 (HTTP wrapper leaks Location buffer on empty
345345
Location header). (bercedev)
346+
. Fix out-of-bounds reads parsing ftp:// wrapper server replies.
347+
(Alejandro Pacheco)
346348

347349
- Tidy:
348350
. Fixed tidy_repair_string(), tidy_repair_file(), tidy::repairString() and

‎ext/ftp/tests/server.inc‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ if ($pid) {
412412
fclose($fs);
413413

414414

415+
}elseif (preg_match('/^PASV/', $buf, $matches) && isset($pasv_reply_at_buffer_end)) {
416+
// The address ends at the 510th byte and the port is cut off.
417+
fputs($s, "227 " . str_repeat("x", 496) . "127,0,0,1,4,210)\r\n");
418+
415419
}elseif (preg_match('/^PASV/', $buf, $matches)) {
416420
$pasv=true;
417421
$host = "127.0.0.1";
@@ -542,6 +546,13 @@ if ($pid) {
542546
fclose($fs);
543547
}elseif (preg_match('/^SIZE \/bug73457/', $buf)) {
544548
fputs($s, "213 10\r\n");
549+
}elseif (preg_match('/^SIZE \/pasvbufferend/', $buf)) {
550+
fputs($s, "213 10\r\n");
551+
}elseif (preg_match('/^SIZE \/mdtmnodigits/', $buf)) {
552+
// A longer line with digits, read into the same buffer before MDTM.
553+
fputs($s, "213 10 bytes (19991005213102)\r\n");
554+
}elseif (preg_match('/^MDTM \/mdtmnodigits/', $buf)) {
555+
fputs($s, "213 unknown\r\n");
545556
}elseif (preg_match("/^SITE/", $buf)) {
546557
fputs($s, "500 Syntax error, command unrecognized.\r\n");
547558
}else {

‎ext/standard/ftp_fopen_wrapper.c‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,23 @@ typedef struct _php_ftp_dirstream_data {
7474
} php_ftp_dirstream_data;
7575

7676
/* {{{ get_ftp_result */
77+
/* Reads reply lines into buffer until a complete status line ("ddd ") and
78+
* returns its code, or 0 if the stream ends before one.
79+
* On return the buffer always holds a NUL-terminated string, which the
80+
* callers rely on when they parse or print the reply text: it is emptied
81+
* first in case nothing can be read, and php_stream_gets() terminates every
82+
* line it reads. */
7783
static inline int get_ftp_result(php_stream *stream, char *buffer, size_t buffer_size)
7884
{
7985
buffer[0] = '\0'; /* in case read fails to read anything */
80-
while (php_stream_gets(stream, buffer, buffer_size-1) &&
81-
!(isdigit((unsigned char)buffer[0]) && isdigit((unsigned char)buffer[1]) &&
82-
isdigit((unsigned char)buffer[2]) && buffer[3] == ' '));
83-
return strtol(buffer, NULL, 10);
86+
while (php_stream_gets(stream, buffer, buffer_size-1)) {
87+
if (isdigit((unsigned char)buffer[0]) && isdigit((unsigned char)buffer[1]) &&
88+
isdigit((unsigned char)buffer[2]) && buffer[3] == ' ') {
89+
return strtol(buffer, NULL, 10);
90+
}
91+
}
92+
/* no complete status line, callers parse the reply from buffer + 4 */
93+
return 0;
8494
}
8595
/* }}} */
8696

@@ -352,8 +362,10 @@ static unsigned short php_fopen_do_pasv(php_stream *stream, char *ip, size_t ip_
352362
tpath++;
353363
}
354364
tpath[-1] = '\0';
355-
memcpy(ip, hoststart, ip_size);
356-
ip[ip_size-1] = '\0';
365+
if ((size_t)(tpath - hoststart) > ip_size) {
366+
return 0;
367+
}
368+
memcpy(ip, hoststart, tpath - hoststart);
357369
hoststart = ip;
358370

359371
/* pull out the MSB of the port */
@@ -825,11 +837,11 @@ static int php_stream_ftp_url_stat(php_stream_wrapper *wrapper, const char *url,
825837
struct tm tm, tmbuf, *gmt;
826838
time_t stamp;
827839

828-
while ((size_t)(p - tmp_line) < sizeof(tmp_line) && !isdigit((unsigned char)*p)) {
840+
while (*p && !isdigit((unsigned char)*p)) {
829841
p++;
830842
}
831843

832-
if ((size_t)(p - tmp_line) > sizeof(tmp_line)) {
844+
if (!*p) {
833845
goto mdtm_error;
834846
}
835847

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
ftp:// wrapper rejects a PASV reply whose address ends at the reply buffer limit
3+
--SKIPIF--
4+
<?php
5+
if (array_search('ftp',stream_get_wrappers()) === FALSE) die("skip ftp wrapper not available.");
6+
if (!function_exists('pcntl_fork')) die("skip pcntl_fork() not available.");
7+
?>
8+
--FILE--
9+
<?php
10+
11+
$pasv_reply_at_buffer_end = true;
12+
require __DIR__ . "/../../../ftp/tests/server.inc";
13+
14+
$path = "ftp://localhost:" . $port . "/pasvbufferend";
15+
16+
var_dump(fopen($path, "r"));
17+
?>
18+
--EXPECTF--
19+
Warning: fopen(%s): Failed to open stream: FTP server reports %a in %s on line %d
20+
bool(false)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
stat() on ftp:// ignores an MDTM reply without digits
3+
--SKIPIF--
4+
<?php
5+
if (array_search('ftp',stream_get_wrappers()) === FALSE) die("skip ftp wrapper not available.");
6+
if (!function_exists('pcntl_fork')) die("skip pcntl_fork() not available.");
7+
?>
8+
--FILE--
9+
<?php
10+
11+
require __DIR__ . "/../../../ftp/tests/server.inc";
12+
13+
$path = "ftp://localhost:" . $port . "/mdtmnodigits";
14+
15+
$stat = stat($path);
16+
var_dump($stat['size'], $stat['mtime']);
17+
?>
18+
--EXPECT--
19+
int(10)
20+
int(-1)

0 commit comments

Comments
 (0)