Skip to content

Commit e264b2e

Browse files
committed
ext/standard: http(s) wrapper corrupts the basic auth header on percent-encoded userinfo.
php_url_decode() returns the shorter decoded length but ZSTR_LEN() is left untouched, so smart_str_append() carries the stale [decoded][NUL][undecoded tail] bytes into the base64 credentials. Fix #22171
1 parent 7aa91a8 commit e264b2e

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

ext/standard/http_fopen_wrapper.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,14 +758,14 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
758758
smart_str scratch = {0};
759759

760760
/* decode the strings first */
761-
php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user));
761+
ZSTR_LEN(resource->user) = php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user));
762762

763763
smart_str_append(&scratch, resource->user);
764764
smart_str_appendc(&scratch, ':');
765765

766766
/* Note: password is optional! */
767767
if (resource->password) {
768-
php_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password));
768+
ZSTR_LEN(resource->password) = php_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password));
769769
smart_str_append(&scratch, resource->password);
770770
}
771771

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
GH-22171 (http(s) stream wrapper sends a corrupted Authorization header for percent-encoded userinfo)
3+
--SKIPIF--
4+
<?php require 'server.inc'; http_server_skipif(); ?>
5+
--INI--
6+
allow_url_fopen=1
7+
--FILE--
8+
<?php
9+
require 'server.inc';
10+
11+
function probe(string $label, string $userinfo, string $expected): void
12+
{
13+
$responses = array(
14+
"data://text/plain,HTTP/1.0 200 Ok\r\n\r\n",
15+
);
16+
17+
['pid' => $pid, 'uri' => $uri] = http_server($responses, $output);
18+
19+
$url = preg_replace('#^http://#', 'http://' . $userinfo . '@', $uri);
20+
file_get_contents($url);
21+
22+
http_server_kill($pid);
23+
24+
fseek($output, 0, SEEK_SET);
25+
$output = stream_get_contents($output);
26+
27+
if (preg_match('/^Authorization:\s*Basic\s+(\S+)/mi', $output, $m)) {
28+
$decoded = base64_decode($m[1]);
29+
} else {
30+
$decoded = '<no Authorization header>';
31+
}
32+
33+
echo "=== {$label} ===", PHP_EOL;
34+
echo " decoded : ", addcslashes($decoded, "\0..\37"), PHP_EOL;
35+
echo " result : ", ($decoded === $expected ? "OK" : "CORRUPT"), PHP_EOL;
36+
}
37+
38+
probe('user only', '%76%6f%72%74%66%75', 'vortfu:');
39+
probe('user + password', '%76%6f%72%74%66%75:%70%61%73%73%77%6f%72%64', 'vortfu:password');
40+
?>
41+
--EXPECT--
42+
=== user only ===
43+
decoded : vortfu:
44+
result : OK
45+
=== user + password ===
46+
decoded : vortfu:password
47+
result : OK

0 commit comments

Comments
 (0)