From e264b2ea077795cd9fc2adf65352187f809a08dd Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 28 May 2026 18:44:11 +0100 Subject: [PATCH] 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 --- ext/standard/http_fopen_wrapper.c | 4 +-- ext/standard/tests/http/gh22171.phpt | 47 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/http/gh22171.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 8157f6a3cec0..22700cf904f1 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -758,14 +758,14 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, smart_str scratch = {0}; /* decode the strings first */ - php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user)); + ZSTR_LEN(resource->user) = php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user)); smart_str_append(&scratch, resource->user); smart_str_appendc(&scratch, ':'); /* Note: password is optional! */ if (resource->password) { - php_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password)); + ZSTR_LEN(resource->password) = php_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password)); smart_str_append(&scratch, resource->password); } diff --git a/ext/standard/tests/http/gh22171.phpt b/ext/standard/tests/http/gh22171.phpt new file mode 100644 index 000000000000..b256db67bc98 --- /dev/null +++ b/ext/standard/tests/http/gh22171.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-22171 (http(s) stream wrapper sends a corrupted Authorization header for percent-encoded userinfo) +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + + $url = preg_replace('#^http://#', 'http://' . $userinfo . '@', $uri); + file_get_contents($url); + + http_server_kill($pid); + + fseek($output, 0, SEEK_SET); + $output = stream_get_contents($output); + + if (preg_match('/^Authorization:\s*Basic\s+(\S+)/mi', $output, $m)) { + $decoded = base64_decode($m[1]); + } else { + $decoded = ''; + } + + echo "=== {$label} ===", PHP_EOL; + echo " decoded : ", addcslashes($decoded, "\0..\37"), PHP_EOL; + echo " result : ", ($decoded === $expected ? "OK" : "CORRUPT"), PHP_EOL; +} + +probe('user only', '%76%6f%72%74%66%75', 'vortfu:'); +probe('user + password', '%76%6f%72%74%66%75:%70%61%73%73%77%6f%72%64', 'vortfu:password'); +?> +--EXPECT-- +=== user only === + decoded : vortfu: + result : OK +=== user + password === + decoded : vortfu:password + result : OK