Skip to content

Commit b967f41

Browse files
committed
ext/standard: check the from address for control characters before PASS
GH-17976 sanitized from and user_agent where the HTTP wrapper emits headers, and left the FTP wrapper for a follow-up. php_ftp_fopen_connect() sends FG(from_address) as the anonymous password without a check, so an ini_set() value carrying CRLF puts extra commands on the control channel. Apply PHP_FTP_CNTRL_CHK, as the branch that sends a URL-supplied password already does.
1 parent 524ae41 commit b967f41

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

ext/standard/ftp_fopen_wrapper.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char
269269
/* if the user has configured who they are,
270270
send that as the password */
271271
if (FG(from_address)) {
272+
PHP_FTP_CNTRL_CHK(ZSTR_VAL(FG(from_address)), ZSTR_LEN(FG(from_address)), "Invalid password %s")
273+
272274
php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(FG(from_address)));
273275
} else {
274276
php_stream_write_string(stream, "PASS anonymous\r\n");
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
ftp:// wrapper must reject control characters in the from address sent as the anonymous password
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+
$log = tmpfile();
12+
13+
$socket = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
14+
if (!$socket) {
15+
die("server failed\n");
16+
}
17+
$name = stream_socket_get_name($socket, false);
18+
$port = (int) substr($name, strrpos($name, ':') + 1);
19+
20+
$pid = pcntl_fork();
21+
if ($pid === 0) {
22+
pcntl_alarm(60);
23+
$s = stream_socket_accept($socket, 10);
24+
if (!$s) {
25+
exit(1);
26+
}
27+
fputs($s, "220 ready\r\n");
28+
stream_set_timeout($s, 1);
29+
$seen = '';
30+
while (($buf = fgets($s)) !== false) {
31+
$seen .= $buf;
32+
if (str_starts_with($buf, 'USER')) {
33+
fputs($s, "331 need pass\r\n");
34+
} else {
35+
fputs($s, "500 no\r\n");
36+
}
37+
}
38+
fwrite($log, $seen);
39+
exit(0);
40+
}
41+
42+
ini_set('from', "evil\r\nSITE INJECT");
43+
var_dump(@fopen("ftp://127.0.0.1:{$port}/x", "r"));
44+
pcntl_waitpid($pid, $status);
45+
46+
rewind($log);
47+
$seen = stream_get_contents($log);
48+
var_dump(str_contains($seen, 'SITE INJECT'));
49+
var_dump(str_contains($seen, 'PASS'));
50+
?>
51+
--EXPECT--
52+
bool(false)
53+
bool(false)
54+
bool(false)

0 commit comments

Comments
 (0)