From b4e3397ec8f4f647e6e019dbf79870fc2f14ba78 Mon Sep 17 00:00:00 2001 From: geeknik Date: Sat, 7 Feb 2026 17:07:22 -0600 Subject: [PATCH 01/25] Fix heap-buffer-overflow in convert stream filters with NUL in line-break-chars Use pestrndup() instead of pestrdup() when duplicating lbchars in the base64-encode, quoted-printable-encode, and quoted-printable-decode filter constructors. pestrdup() is strlen-based and truncates at the first NUL byte, but lbchars_len preserves the original length, causing an out-of-bounds read when the filter later copies lbchars_len bytes from the truncated allocation. Fixes GHSA-88hq-2827-7pg6. --- ext/standard/filters.c | 6 ++--- .../tests/filters/ghsa-88hq-2827-7pg6.phpt | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt diff --git a/ext/standard/filters.c b/ext/standard/filters.c index 08678f8d5d81..956bd86b880c 100644 --- a/ext/standard/filters.c +++ b/ext/standard/filters.c @@ -235,7 +235,7 @@ static php_conv_err_t php_conv_base64_encode_ctor(php_conv_base64_encode *inst, inst->line_ccnt = line_len; inst->line_len = line_len; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; @@ -844,7 +844,7 @@ static php_conv_err_t php_conv_qprint_encode_ctor(php_conv_qprint_encode *inst, inst->line_ccnt = line_len; inst->line_len = line_len; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; @@ -1057,7 +1057,7 @@ static php_conv_err_t php_conv_qprint_decode_ctor(php_conv_qprint_decode *inst, inst->next_char = 0; inst->lb_ptr = inst->lb_cnt = 0; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; diff --git a/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt b/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt new file mode 100644 index 000000000000..84a85daefdb9 --- /dev/null +++ b/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt @@ -0,0 +1,27 @@ +--TEST-- +GHSA-88hq-2827-7pg6: OOB read in convert stream filters when line-break-chars contains NUL +--FILE-- + 4, "line-break-chars" => "\0X"]; +$fp = fopen("php://temp", "r+"); +fwrite($fp, str_repeat("A", 6)); +rewind($fp); +stream_filter_append($fp, "convert.base64-encode", STREAM_FILTER_READ, $opts); +$out = stream_get_contents($fp); +fclose($fp); + +// base64("AAAAAA") = "QUFBQUFB" (8 chars) +// With line-length=4: "QUFB" + "\0X" + "QUFB" +// Hex: 5155464200585155 4642 +echo bin2hex($out), "\n"; + +echo "Done\n"; + +?> +--EXPECT-- +51554642005851554642 +Done From dcdfcf86fcf7f42fa98dd3d000f310ebdad41965 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 25 Aug 2026 11:23:52 +0200 Subject: [PATCH 02/25] Fix GHSA-62xp-839h-2637: FastCGI allowed_clients compared only 96 bits of IPv6 addresses --- main/fastcgi.c | 2 +- ...a-62xp-839h-2637-ipv6-allowed-clients.phpt | 52 +++++++++++++++++++ ...xp-839h-2637-v4mapped-allowed-clients.phpt | 52 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt create mode 100644 sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt diff --git a/main/fastcgi.c b/main/fastcgi.c index 18eb4b394bc2..606faeea43d8 100644 --- a/main/fastcgi.c +++ b/main/fastcgi.c @@ -1340,7 +1340,7 @@ static int fcgi_is_allowed(void) { if (client_sa.sa.sa_family == AF_INET6) { for (i = 0; allowed_clients[i].sa.sa_family ; i++) { if (allowed_clients[i].sa.sa_family == AF_INET6 - && !memcmp(&client_sa.sa_inet6.sin6_addr, &allowed_clients[i].sa_inet6.sin6_addr, 12)) { + && !memcmp(&client_sa.sa_inet6.sin6_addr, &allowed_clients[i].sa_inet6.sin6_addr, sizeof(client_sa.sa_inet6.sin6_addr))) { return 1; } #ifdef IN6_IS_ADDR_V4MAPPED diff --git a/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt b/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt new file mode 100644 index 000000000000..c72f9bcdcc5f --- /dev/null +++ b/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt @@ -0,0 +1,52 @@ +--TEST-- +GHSA-62xp-839h-2637: IPv6 allowed client compared on all 128 bits +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('{{ADDR:IPv6[exact]}}', 'exact: ok', 'exact: error'); +$tester->checkRequest('{{ADDR:IPv6[prefix]}}', 'prefix: ok', 'prefix: error'); +$tester->terminate(); +$tester->expectLogWarning( + 'child %d said into stderr: "ERROR: Connection disallowed: IP address \'::1\' has been dropped."', + 'prefix' +); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +exact: ok +prefix: error +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt b/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt new file mode 100644 index 000000000000..1bbb7c16585f --- /dev/null +++ b/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt @@ -0,0 +1,52 @@ +--TEST-- +GHSA-62xp-839h-2637: IPv4-mapped allowed client compared on all 128 bits +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('{{ADDR:IPv4[exact]}}', 'exact: ok', 'exact: error'); +$tester->checkRequest('{{ADDR:IPv4[prefix]}}', 'prefix: ok', 'prefix: error'); +$tester->terminate(); +$tester->expectLogWarning( + 'child %d said into stderr: "ERROR: Connection disallowed: IP address \'127.0.0.1\' has been dropped."', + 'prefix' +); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +exact: ok +prefix: error +Done +--CLEAN-- + From 0bb308eb4cf699e707efbcf97ffb70e46a176be7 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 10 Aug 2026 22:25:55 +0200 Subject: [PATCH 03/25] Fix GHSA-vvx9-73fr-5jjx: do not fall back to CN if the cert has a service ID RFC 6125 6.4.4 forbids matching CN-ID if the certificate presents a DNS-ID, SRV-ID or URI-ID. Falling back allowed a CA that validated only one of those identities to leave an unvalidated CN to be matched as a host name. IP-ID is left out as it is out of scope of RFC 6125 and suppressing CN there would break IP-only SAN certificates. --- ext/openssl/tests/san_absent_cn_fallback.phpt | 61 ++++++++++++++++++ ext/openssl/tests/san_no_cn_fallback.phpt | 64 +++++++++++++++++++ ext/openssl/tests/san_peer_matching.phpt | 2 +- ext/openssl/tests/san_srv_no_cn_fallback.phpt | 57 +++++++++++++++++ ext/openssl/tests/san_uri_no_cn_fallback.phpt | 57 +++++++++++++++++ ext/openssl/xp_ssl.c | 28 +++++++- 6 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 ext/openssl/tests/san_absent_cn_fallback.phpt create mode 100644 ext/openssl/tests/san_no_cn_fallback.phpt create mode 100644 ext/openssl/tests/san_srv_no_cn_fallback.phpt create mode 100644 ext/openssl/tests/san_uri_no_cn_fallback.phpt diff --git a/ext/openssl/tests/san_absent_cn_fallback.phpt b/ext/openssl/tests/san_absent_cn_fallback.phpt new file mode 100644 index 000000000000..30f4821b5fe5 --- /dev/null +++ b/ext/openssl/tests/san_absent_cn_fallback.phpt @@ -0,0 +1,61 @@ +--TEST-- +Peer verification falls back to CN when the certificate has no SAN +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'other.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('cn.example.org', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +resource(%d) of type (stream) + +Warning: stream_socket_client(): Peer certificate CN=`cn.example.org' did not match expected CN=`other.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_no_cn_fallback.phpt b/ext/openssl/tests/san_no_cn_fallback.phpt new file mode 100644 index 000000000000..ae19be414ee7 --- /dev/null +++ b/ext/openssl/tests/san_no_cn_fallback.phpt @@ -0,0 +1,64 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has a SAN +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The SAN entry is matched as usual. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'san.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + // The CN must not be considered because the certificate presents a SAN. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'DNS:san.example.org'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +resource(%d) of type (stream) + +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_peer_matching.phpt b/ext/openssl/tests/san_peer_matching.phpt index 0df18f4a9250..cca4d360d8bd 100644 --- a/ext/openssl/tests/san_peer_matching.phpt +++ b/ext/openssl/tests/san_peer_matching.phpt @@ -54,7 +54,7 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); --EXPECTF-- resource(%d) of type (stream) -Warning: stream_socket_client(): Unable to locate peer certificate CN in %s on line %d +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `moar.example.org' in %s on line %d Warning: stream_socket_client(): Failed to enable crypto in %s on line %d diff --git a/ext/openssl/tests/san_srv_no_cn_fallback.phpt b/ext/openssl/tests/san_srv_no_cn_fallback.phpt new file mode 100644 index 000000000000..9c130e757b78 --- /dev/null +++ b/ext/openssl/tests/san_srv_no_cn_fallback.phpt @@ -0,0 +1,57 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has an SRV-ID +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The CN must not be considered because the certificate presents an SRV-ID. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'otherName:1.3.6.1.5.5.7.8.7;IA5STRING:_https.san.example.org'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_uri_no_cn_fallback.phpt b/ext/openssl/tests/san_uri_no_cn_fallback.phpt new file mode 100644 index 000000000000..b621d52312bc --- /dev/null +++ b/ext/openssl/tests/san_uri_no_cn_fallback.phpt @@ -0,0 +1,57 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has a URI-ID +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The CN must not be considered because the certificate presents a URI-ID. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'URI:https://san.example.org/x'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 46b4de67ee7d..8060c698fe7b 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -450,7 +450,8 @@ static bool php_openssl_matches_wildcard_name(const char *subjectname, const cha } /* }}} */ -static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) /* {{{ */ +static bool php_openssl_matches_san_list( + X509 *peer, const char *subject_name, bool *has_service_id) { int i, len; unsigned char *cert_name = NULL; @@ -459,6 +460,8 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / GENERAL_NAMES *alt_names = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0); int alt_name_count = sk_GENERAL_NAME_num(alt_names); + *has_service_id = false; + #ifdef HAVE_IPV6_SAN /* detect if subject name is an IPv6 address and expand once if required */ char subject_name_ipv6_expanded[40]; @@ -476,6 +479,8 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / GENERAL_NAME *san = sk_GENERAL_NAME_value(alt_names, i); if (san->type == GEN_DNS) { + *has_service_id = true; + ASN1_STRING_to_UTF8(&cert_name, san->d.dNSName); if ((size_t)ASN1_STRING_length(san->d.dNSName) != strlen((const char*)cert_name)) { OPENSSL_free(cert_name); @@ -521,6 +526,16 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / } } #endif + } else if (san->type == GEN_URI) { + *has_service_id = true; + } else if (san->type == GEN_OTHERNAME) { + char oid[32]; + + /* SRV-ID, matched by OID because NID_SRVName needs OpenSSL 3.0 */ + if (OBJ_obj2txt(oid, sizeof(oid), san->d.otherName->type_id, 1) > 0 + && strcmp(oid, "1.3.6.1.5.5.7.8.7") == 0) { + *has_service_id = true; + } } } @@ -528,7 +543,6 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / return 0; } -/* }}} */ static bool php_openssl_matches_common_name(X509 *peer, const char *subject_name) /* {{{ */ { @@ -635,8 +649,16 @@ static int php_openssl_apply_peer_verification_policy(SSL *ssl, X509 *peer, php_ } if (peer_name) { - if (php_openssl_matches_san_list(peer, peer_name)) { + bool has_service_id = false; + + if (php_openssl_matches_san_list(peer, peer_name, &has_service_id)) { return SUCCESS; + } else if (has_service_id) { + /* CN must be ignored if the certificate presents a service identity. */ + php_error_docref(NULL, E_WARNING, + "Peer certificate subjectAltName did not match expected name `%s'", + peer_name); + return FAILURE; } else if (php_openssl_matches_common_name(peer, peer_name)) { return SUCCESS; } else { From 78cc82b37a340840c6fbff676f513eb4e3d96533 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 10 Aug 2026 14:15:35 +0200 Subject: [PATCH 04/25] Fix GHSA-xr7j-rvgx-xq5p: OOB read in php_openssl_matches_wildcard_name() The memchr() length underflowed when the wildcard prefix and suffix overlapped in the subject name (e.g. CN "a*aaaa" against peer name "aaaa"). --- ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt | 67 ++++++++++++++++++++++ ext/openssl/xp_ssl.c | 5 +- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt diff --git a/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt b/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt new file mode 100644 index 000000000000..daf5b878bbf8 --- /dev/null +++ b/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt @@ -0,0 +1,67 @@ +--TEST-- +GHSA-xr7j-rvgx-xq5p: OOB read in php_openssl_matches_wildcard_name() with overlapping wildcard prefix and suffix +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'aaaa'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'www.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('a*aaaa', $certFile, null, $san); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `aaaa' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) + +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `www.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 8060c698fe7b..cc0f53898abc 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -418,8 +418,7 @@ static bool php_openssl_x509_fingerprint_match(X509 *peer, zval *val) static bool php_openssl_matches_wildcard_name(const char *subjectname, const char *certname) /* {{{ */ { char *wildcard = NULL; - ptrdiff_t prefix_len; - size_t suffix_len, subject_len; + size_t prefix_len, suffix_len, subject_len; if (strcasecmp(subjectname, certname) == 0) { return 1; @@ -438,7 +437,7 @@ static bool php_openssl_matches_wildcard_name(const char *subjectname, const cha suffix_len = strlen(wildcard + 1); subject_len = strlen(subjectname); - if (suffix_len <= subject_len) { + if (suffix_len + prefix_len <= subject_len) { /* 2) suffix must match * 3) no . between prefix and suffix **/ From 114dbb74368e6a6cd6e48ddb4a76dbe8728b84e2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 12 Jul 2026 23:37:54 +0200 Subject: [PATCH 05/25] Fix GHSA-r6x9-5r99-36j7: Various packet overreads in mysqlnd wireprotocol Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com> --- ext/mysqli/tests/fake_server.inc | 597 +++++++++++++++++- ...6j7-auth-packet-affected-rows-2-bytes.phpt | 32 + ...6j7-auth-packet-affected-rows-8-bytes.phpt | 32 + ...auth-packet-affected-rows-null-length.phpt | 32 + ...9-5r99-36j7-auth-packet-affected-rows.phpt | 37 ++ ...7-auth-packet-last-insert-id-no-space.phpt | 37 ++ ...-5r99-36j7-auth-packet-last-insert-id.phpt | 37 ++ ...9-5r99-36j7-auth-packet-warning-count.phpt | 37 ++ ...x9-5r99-36j7-auth-switch-unterminated.phpt | 37 ++ ...ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt | 45 ++ ...-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt | 41 ++ .../ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt | 45 ++ ...-36j7-cached-sha2-switch-unterminated.phpt | 40 ++ .../ghsa-r6x9-5r99-36j7-chg-user-empty.phpt | 43 ++ .../ghsa-r6x9-5r99-36j7-chg-user-len.phpt | 43 ++ ...r99-36j7-chg-user-switch-unterminated.phpt | 43 ++ ...ghsa-r6x9-5r99-36j7-eof-warning-count.phpt | 44 ++ ...sa-r6x9-5r99-36j7-greet-packet-string.phpt | 37 ++ .../ghsa-r6x9-5r99-36j7-greet-packet.phpt | 37 ++ ...hsa-r6x9-5r99-36j7-greet-scramble-len.phpt | 37 ++ ...6x9-5r99-36j7-ok-packet-affected-rows.phpt | 42 ++ ...x9-5r99-36j7-ok-packet-last-insert-id.phpt | 42 ++ ...-r6x9-5r99-36j7-ok-packet-message-len.phpt | 42 ++ ...6x9-5r99-36j7-ok-packet-warning-count.phpt | 42 ++ ...sa-r6x9-5r99-36j7-query-row-eof-short.phpt | 47 ++ ...5r99-36j7-query-row-field-len-2-bytes.phpt | 49 ++ ...sa-r6x9-5r99-36j7-query-row-field-len.phpt | 47 ++ ...a-r6x9-5r99-36j7-query-row-field-null.phpt | 49 ++ ...-r6x9-5r99-36j7-rset-header-field-len.phpt | 50 ++ ...36j7-rset-header-upsert-affected-rows.phpt | 50 ++ ...r6x9-5r99-36j7-rset-header-upsert-len.phpt | 48 ++ .../ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt | 47 ++ .../ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt | 47 ++ ...9-36j7-stmt-response-field-len-faulty.phpt | 38 ++ ...6j7-stmt-response-field-len-premature.phpt | 42 ++ ...x9-5r99-36j7-stmt-response-row-status.phpt | 40 ++ ...-5r99-36j7-stmt-row-short-length-date.phpt | 45 ++ ...9-36j7-stmt-row-short-length-datetime.phpt | 45 ++ ...-5r99-36j7-stmt-row-short-length-time.phpt | 45 ++ ext/mysqlnd/mysqlnd_ps.c | 3 + ext/mysqlnd/mysqlnd_ps_codec.c | 48 +- ext/mysqlnd/mysqlnd_wireprotocol.c | 396 +++++++----- ext/mysqlnd/mysqlnd_wireprotocol.h | 12 +- 43 files changed, 2475 insertions(+), 174 deletions(-) create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt create mode 100644 ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index d6afbb049773..ff49ae896066 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -168,11 +168,11 @@ class my_mysqli_fake_packet_generator return new my_mysqli_fake_packet_item(null, $packed_value, $is_hex); } - public function server_ok(): my_mysqli_fake_packet + public function server_ok($number = "02"): my_mysqli_fake_packet { $packet = new my_mysqli_fake_packet(); $packet->packet_length = "070000"; - $packet->packet_number = "02"; + $packet->packet_number = $number; $packet->header = "00"; // OK $packet->affected_rows = "00"; $packet->last_insert_id = "00"; @@ -181,6 +181,17 @@ class my_mysqli_fake_packet_generator return $packet; } + public function server_eof(): my_mysqli_fake_packet + { + $packet = new my_mysqli_fake_packet(); + $packet->packet_length = "050000"; + $packet->packet_number = "01"; + $packet->header = "fe"; // EOF + $packet->warning_count = "0000"; + $packet->server_status = "0200"; + return $packet; + } + public function server_greetings(): my_mysqli_fake_packet { $packet = new my_mysqli_fake_packet(); @@ -204,6 +215,25 @@ class my_mysqli_fake_packet_generator return $packet; } + public function server_greetings_sha256(): my_mysqli_fake_packet + { + $packet = $this->server_greetings(); + // 6 bytes shorter than the default mysql_native_password greeting + $packet->packet_length = "520000"; + $packet->mariadb_extended_server_capabilities_auth_plugin = + self::create_packet_item('sha256_password'); + + return $packet; + } + public function server_greetings_caching_sha2(): my_mysqli_fake_packet + { + $packet = $this->server_greetings(); + // same length as the default mysql_native_password so the length stays + $packet->mariadb_extended_server_capabilities_auth_plugin = + self::create_packet_item('caching_sha2_password'); + + return $packet; + } public function server_tabular_query_response(): array { $qr1 = new my_mysqli_fake_packet(); @@ -246,7 +276,6 @@ class my_mysqli_fake_packet_generator $qr1->packet_number = "01"; $qr1->field_count = "00"; // UPSERT $qr1->affected_rows = "00"; - $qr1->affected_rows = "00"; $qr1->last_insert_id = "00"; $qr1->server_status = "0000"; $qr1->warning_count = "0000"; @@ -257,6 +286,21 @@ class my_mysqli_fake_packet_generator return [$qr1]; } + public function server_upsert_result_response(): my_mysqli_fake_packet + { + $ur = new my_mysqli_fake_packet(); + $ur->packet_length = "300000"; + $ur->packet_number = "01"; + $ur->field_count = "00"; // UPSERT + $ur->affected_rows = "00"; + $ur->last_insert_id = "00"; + $ur->server_status = "0002"; + $ur->warning_count = "0000"; + $ur->payload = "28526f7773206d6174636865643a203120204368616e6765643a203020205761726e696e67733a2030"; + + return $ur; + } + public function server_stmt_prepare_response_start($num_field): my_mysqli_fake_packet { $pr1 = new my_mysqli_fake_packet(); @@ -712,6 +756,44 @@ function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server $conn->read(65536); } +function my_mysqli_test_stmt_response_row_short_length( + my_mysqli_fake_server_conn $conn, + string $field_name +): void { + $rh = $conn->packet_generator->server_stmt_execute_data_response($field_name); + + // The length does not cover the fixed offsets that the field type reads + $rh[4]->packet_length = '090000'; + $rh[4]->row_field2 = '01de'; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send_server_stmt_prepare_data_response($field_name); + $conn->read(); + $conn->send( + $conn->packets_to_bytes($rh), + "Malicious Stmt Response for data $field_name [length too short]" + ); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_row_short_length_date(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'datval'); +} + +function my_mysqli_test_stmt_response_row_short_length_time(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'timval'); +} + +function my_mysqli_test_stmt_response_row_short_length_datetime(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'dtival'); +} + function my_mysqli_test_stmt_response_row_over_read_two_fields( my_mysqli_fake_server_conn $conn, string $field_name, @@ -805,6 +887,114 @@ function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server $conn->read(65536); } +function my_mysqli_test_query_response_row_field_len(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + + // 2-byte length prefix (0xfc) with the packet ending right after it, so + // decoding the length itself would read past the end of the packet + $rh[4]->row_field2 = 'fc'; + $rh[4]->packet_length = '060000'; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [field length over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row(my_mysqli_fake_server_conn $conn, string $packet_length, + string $row_field2, string $message): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + $rh[4]->packet_length = $packet_length; + $rh[4]->row_field2 = $row_field2; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), $message); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row_eof_short(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + // EOF row packet that has no space for the warnings and the status + $rh[5]->packet_length = '030000'; + $rh[5]->server_status = ''; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response [short EOF row packet]"); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row_field_null(my_mysqli_fake_server_conn $conn): void +{ + // NULL field, which is encoded as a single 0xfb byte + my_mysqli_test_query_response_row($conn, "060000", "fb", "Query Response for data strval field [NULL]"); +} + +function my_mysqli_test_query_response_row_field_len_2_bytes(my_mysqli_fake_server_conn $conn): void +{ + // field length encoded on 2 bytes + my_mysqli_test_query_response_row($conn, "0c0000", "fc040074657374", + "Query Response for data strval field [length on 2 bytes]"); +} + +function my_mysqli_test_query_rset_header_field_over_read(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + + $rh[0]->num_fields = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [length overflow]"); + $conn->read(65536); +} + +function my_mysqli_test_upsert_rset_header_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $ur = $conn->packet_generator->server_upsert_result_response(); + + $ur->packet_length = "020000"; + $ur->affected_rows = "fd"; + $ur->last_insert_id = ""; + $ur->server_status = ""; + $ur->warning_count = ""; + $ur->payload = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($ur->to_bytes(), "Malicious Upsert Response [affected rows overflow]"); + $conn->read(65536); +} + +function my_mysqli_test_upsert_rset_header_packet_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $ur = $conn->packet_generator->server_upsert_result_response(); + + $ur->affected_rows = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($ur->to_bytes(), "Malicious Upsert Response [affected rows overflow]"); + $conn->read(65536); +} + + function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void { $conn->send_server_greetings(); @@ -818,7 +1008,401 @@ function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server } } -function run_fake_server(string $test_function, int|string $port = 0): int +function my_mysqli_test_auth_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "020000"; + $p->affected_rows = "fe"; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_auth_last_insert_id_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "030000"; + $p->last_insert_id = "fd"; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_auth_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "060000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_cached_sha2_result(my_mysqli_fake_server_conn $conn, string $packet_length, string $payload, + string $message): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = $packet_length; + $p->packet_number = "02"; + if ($payload !== '') { + $p->payload = $payload; + } + + $conn->send($conn->packet_generator->server_greetings_caching_sha2()->to_bytes(), "Server Greeting"); + $conn->read(65536); + // sent in one go so that the client cannot interleave its output in between + $conn->send($p->to_bytes() . $conn->packet_generator->server_ok("03")->to_bytes(), $message); + $conn->read(65536); +} + +function my_mysqli_test_cached_sha2_result_fast_auth(my_mysqli_fake_server_conn $conn): void +{ + // What a real server sends for a successful fast auth: CR_OK and the status byte + my_mysqli_test_cached_sha2_result($conn, "020000", "0103", "Cached SHA2 Result [fast auth]"); +} + +function my_mysqli_test_cached_sha2_result_empty(my_mysqli_fake_server_conn $conn): void +{ + // Empty packet, so reading the response code reads past the packet + my_mysqli_test_cached_sha2_result($conn, "000000", "", "Malicious Cached SHA2 Result [empty packet]"); +} + +function my_mysqli_test_cached_sha2_result_len(my_mysqli_fake_server_conn $conn): void +{ + // Only the response code is left, so there is no space for the status byte + my_mysqli_test_cached_sha2_result($conn, "010000", "01", "Malicious Cached SHA2 Result [packet too short]"); +} + +function my_mysqli_test_sha256_pk_response_empty(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + // Empty packet, so reading the response code reads past the packet + $p->packet_length = "000000"; + $p->packet_number = "02"; + + $conn->send($conn->packet_generator->server_greetings_sha256()->to_bytes(), "Server Greeting"); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious SHA256 PK Response [empty packet]"); + $conn->read(65536); + $conn->send($conn->packet_generator->server_ok("04")->to_bytes(), "Server OK"); + $conn->read(65536); +} + +function my_mysqli_test_sha256_pk_response_len(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + // Only the response code is left, so there is no space for the public key + $p->packet_length = "010000"; + $p->packet_number = "02"; + $p->response_code = "00"; + + $conn->send($conn->packet_generator->server_greetings_sha256()->to_bytes(), "Server Greeting"); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious SHA256 PK Response [packet too short]"); + $conn->read(65536); + $conn->send($conn->packet_generator->server_ok("04")->to_bytes(), "Server OK"); + $conn->read(65536); +} + +/* An auth switch packet whose plugin name is not NUL terminated. */ +function my_mysqli_auth_switch_packet(string $packet_number): my_mysqli_fake_packet +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "160000"; + $p->packet_number = $packet_number; + $p->response_code = "fe"; + // 21 bytes of plugin name and no terminator + $p->auth_plugin_name = bin2hex('mysql_native_password'); + + return $p; +} + +function my_mysqli_test_auth_response_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send(my_mysqli_auth_switch_packet("02")->to_bytes(), + "Malicious Auth Switch Response [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send(my_mysqli_auth_switch_packet("01")->to_bytes(), + "Malicious Change User Response [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_cached_sha2_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send($conn->packet_generator->server_greetings_caching_sha2()->to_bytes(), "Server Greeting"); + $conn->read(65536); + // sent in one go so that the client cannot interleave its output in between + $conn->send(my_mysqli_auth_switch_packet("02")->to_bytes() + . $conn->packet_generator->server_ok("03")->to_bytes(), + "Malicious Cached SHA2 Result [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_response_empty(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + // Empty packet, so reading the response code reads past the packet + $p->packet_length = "000000"; + $p->header = ""; + $p->affected_rows = ""; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious Change User Response [empty packet]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_response_len(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + // Only the response code is left, so there is no space for the rest + $p->packet_length = "010000"; + $p->affected_rows = ""; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious Change User Response [packet too short]"); + $conn->read(65536); +} + +function my_mysqli_test_auth_ok(my_mysqli_fake_server_conn $conn, string $packet_length, string $affected_rows, + string $last_insert_id, string $server_status, string $warning_count, + string $message): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = $packet_length; + $p->affected_rows = $affected_rows; + $p->last_insert_id = $last_insert_id; + $p->server_status = $server_status; + $p->warning_count = $warning_count; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), $message); + $conn->read(); +} + +function my_mysqli_test_auth_last_insert_id_no_space(my_mysqli_fake_server_conn $conn): void +{ + // affected rows takes the last byte, so there is nothing left for the last insert id + my_mysqli_test_auth_ok($conn, "020000", "00", "", "", "", + "Malicious OK Auth Response [no space for last insert id]"); +} + +function my_mysqli_test_auth_affected_rows_null_length(my_mysqli_fake_server_conn $conn): void +{ + // affected rows sent as the NULL length marker + my_mysqli_test_auth_ok($conn, "070000", "fb", "00", "0200", "0000", + "OK Auth Response [affected rows as NULL length]"); +} + +function my_mysqli_test_auth_affected_rows_2_bytes(my_mysqli_fake_server_conn $conn): void +{ + // affected rows encoded on 2 bytes + my_mysqli_test_auth_ok($conn, "090000", "fc3412", "00", "0200", "0000", + "OK Auth Response [affected rows on 2 bytes]"); +} + +function my_mysqli_test_auth_affected_rows_8_bytes(my_mysqli_fake_server_conn $conn): void +{ + // affected rows encoded on 8 bytes + my_mysqli_test_auth_ok($conn, "0f0000", "fe0100000000000000", "00", "0200", "0000", + "OK Auth Response [affected rows on 8 bytes]"); +} + +function my_mysqli_test_greet_scramble_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_greetings(); + // the scramble length claims more data than the packet holds + $p->auth_plugin = "ff"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting [scramble length over-read]"); + $conn->read(); +} + +function my_mysqli_test_greet_over_read_string(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "080000"; + $p->packet_number = "00"; + $p->proto_version = "0a"; + $p->version = my_mysqli_fake_packet_generator::create_packet_item('5.5'); + $p->thread_id = "03030303"; + $p->salt = "473e3f6047257c67"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting"); + $conn->read(); +} + +function my_mysqli_test_greet_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "110000"; + $p->packet_number = "00"; + $p->proto_version = "0a"; + $p->version = my_mysqli_fake_packet_generator::create_packet_item('8.0' . chr(0)); + $p->thread_id = "03000000"; + $p->salt = "473e3f6047257c67"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting"); + $conn->read(); +} + +function my_mysqli_test_ok_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "020000"; + $p->affected_rows = "fe"; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_last_insert_id_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "030000"; + $p->last_insert_id = "fd"; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_message_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "080000"; + $p->message_len = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "060000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_eof_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_eof(); + $p->packet_length = "040000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious EOF Response"); + $conn->read(); +} + +function my_mysqli_test_stmt_response_row_over_read_status(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_stmt_execute_items_response(); + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + $packets[2]->server_status = '02'; + $packets[2]->packet_length = '040000'; + $conn->send($conn->packets_to_bytes($packets), "Stmt prepare items"); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for items [Extract heap through buffer over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_field_len_faulty(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_stmt_execute_items_response(); + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + // org_name length exceeds the whole remaining packet + $packets[1]->orig_name_len = 'fe'; + $packets[2]->packet_length = '1f0000'; + $conn->send($conn->packets_to_bytes($packets), "Stmt prepare items"); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for fields [Extract heap through buffer over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_field_len_premature(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + // org_name length over-reads into the trailing fixed-length metadata block + $packets[1]->orig_name_len = '0c'; + $conn->send($conn->packets_to_bytes($packets), "Malicious Stmt Prepare items [Field length over-read]"); + $conn->read(65536); +} + +function run_fake_server(string $test_function, int|string $port = 0): void { $host = '127.0.0.1'; @@ -847,9 +1431,12 @@ function run_fake_server(string $test_function, int|string $port = 0): int echo "[*] Server finished\n"; } - function run_fake_server_in_background($test_function, $port = 0): my_mysqli_fake_server_process { + if ($port == 0) { + $port = (int) getenv('MYSQLI_TEST_FAKE_SERVER_PORT'); + } + $command = [PHP_BINARY, '-n', __FILE__, 'mysqli_fake_server', $test_function, $port]; $descriptorspec = array( diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt new file mode 100644 index 000000000000..45d33cdf7fc1 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows on 2 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows on 2 bytes]: 0900000200fc34120002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt new file mode 100644 index 000000000000..63233ad8291e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows on 8 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows on 8 bytes]: 0f00000200fe01000000000000000002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt new file mode 100644 index 000000000000..6a18f36d37c6 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows as NULL length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows as NULL length]: 0700000200fb0002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt new file mode 100644 index 000000000000..13856f058f4e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in affected rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 0200000200fe + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt new file mode 100644 index 000000000000..3a163ef63a5b --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with no space for last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response [no space for last insert id]: 020000020000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt new file mode 100644 index 000000000000..9d30f04dfc98 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 030000020000fd + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt new file mode 100644 index 000000000000..a4f5ca20a0c2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 06000002000000020000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt new file mode 100644 index 000000000000..8c1415a7bd0b --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth switch response with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 7d00000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400140c7b6398a9794c7dc95737fa731fe62e95fe56626d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Auth Switch Response [plugin name not terminated]: 16000002fe6d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt new file mode 100644 index 000000000000..9a7c96116e78 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result with empty packet) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [empty packet]: 000000020700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt new file mode 100644 index 000000000000..568f1f0b5dbb --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt @@ -0,0 +1,41 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result for a successful fast auth) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Cached SHA2 Result [fast auth]: 0200000201030700000300000002000000 +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt new file mode 100644 index 000000000000..77b19832cc02 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result shorter than expected) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [packet too short]: 01000002010700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt new file mode 100644 index 000000000000..2554a46c5a00 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt @@ -0,0 +1,40 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [plugin name not terminated]: 16000002fe6d7973716c5f6e61746976655f70617373776f72640700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt new file mode 100644 index 000000000000..66bde99bef01 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response with empty packet) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Changing user on the fake server...\n"; +var_dump($conn->change_user("root2", "", "")); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [empty packet]: 00000001 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +bool(false) +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt new file mode 100644 index 000000000000..5a2b2c6b1baa --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response shorter than expected) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Changing user on the fake server...\n"; +var_dump($conn->change_user("root2", "", "")); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [packet too short]: 0100000100 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +bool(false) +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt new file mode 100644 index 000000000000..e6403f5f89a8 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, "", "", $process->getPort() ); + +echo "[*] Changing user on the fake server...\n"; +try { + var_dump($conn->change_user("root2", "", "")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [plugin name not terminated]: 16000001fe6d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt new file mode 100644 index 000000000000..2f1f0f054d92 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt @@ -0,0 +1,44 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - eof packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + $query = "SELECT 1; SELECT 2;"; + $conn->multi_query($query); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 030000001b0000 +[*] Sending - Malicious EOF Response: 04000001fe000200 + +Warning: mysqli::multi_query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::multi_query(): EOF packet shorter than expected in %s on line %d + +Warning: mysqli::multi_query(): Error while reading SET_OPTION's response packet. PID=%d in %s on line %d +[*] Received: 140000000353454c45435420313b2053454c45435420323b +MySQL server has gone away +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt new file mode 100644 index 000000000000..210ffb8f8702 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet over-read string) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting: 080000000a352e3503030303473e3f6047257c67 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt new file mode 100644 index 000000000000..7bcbd264dec0 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet over-read) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting: 110000000a382e300003000000473e3f6047257c67 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt new file mode 100644 index 000000000000..c85401d9a4b2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet scramble length over-read) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting [scramble length over-read]: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81ff0000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt new file mode 100644 index 000000000000..953abde7d6e6 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in affected rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 0200000100fe + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt new file mode 100644 index 000000000000..fe2f0c7757bf --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 030000010000fd + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt new file mode 100644 index 000000000000..ed332988994a --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in message length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 0800000100000002000000fd + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt new file mode 100644 index 000000000000..70ce5b8debb2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 06000001000000020000 + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt new file mode 100644 index 000000000000..685af9948ba3 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - row packet with an EOF shorter than the status it announces) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response [short EOF row packet]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374047465737403000006fe0000 +array(2) { + [0]=> + string(4) "test" + [1]=> + string(4) "test" +} +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt new file mode 100644 index 000000000000..dd7a496b26ea --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt @@ -0,0 +1,49 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row field length on 2 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Query Response for data strval field [length on 2 bytes]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000c0000050474657374fc04007465737405000006fe00002200 +array(2) { + [0]=> + string(4) "test" + [1]=> + string(4) "test" +} +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt new file mode 100644 index 000000000000..1b1d80c400e2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row field length prefix) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$sql = "SELECT strval, strval FROM data"; + +$result = $conn->query($sql); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + var_dump($row['strval']); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response for data strval field [field length over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00002200060000050474657374fc05000006fe00002200 + +Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after end of packet in %s on line %d +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt new file mode 100644 index 000000000000..dc6e6703374d --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt @@ -0,0 +1,49 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row with a NULL field) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Query Response for data strval field [NULL]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00002200060000050474657374fb05000006fe00002200 +array(2) { + [0]=> + string(4) "test" + [1]=> + NULL +} +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt new file mode 100644 index 000000000000..066844195144 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header read field length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$sql = "SELECT strval, strval FROM data"; + +$result = $conn->query($sql); + +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + var_dump($row['strval']); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response for data strval field [length overflow]: 01000001fd3200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374047465737405000006fe00002200 + +Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::query(): RSET_HEADER packet shorter than expected in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt new file mode 100644 index 000000000000..7e91c0db5f53 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header upsert affected_rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +// Construct the SQL query directly +$sql = "UPDATE data SET strval = 'test' WHERE id = 1"; + +// Execute the query +$result = $conn->query($sql); +if ($result) { + echo "Affected rows: " . $conn->affected_rows . "\n"; + echo "Info: " . $conn->info . "\n"; +} + +// Close the connection +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 2d000000035550444154452064617461205345542073747276616c203d20277465737427205748455245206964203d2031 +[*] Sending - Malicious Upsert Response [affected rows overflow]: 0200000100fd + +Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::query(): RSET_HEADER packet shorter than expected in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt new file mode 100644 index 000000000000..13193abaab08 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt @@ -0,0 +1,48 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header upsert packet length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +// Construct the SQL query directly +$sql = "UPDATE data SET strval = 'test' WHERE id = 1"; + +// Execute the query +$result = $conn->query($sql); +if ($result) { + echo "Affected rows: " . $conn->affected_rows . "\n"; + echo "Info: " . $conn->info . "\n"; +} + +// Close the connection +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 2d000000035550444154452064617461205345542073747276616c203d20277465737427205748455245206964203d2031 +[*] Sending - Malicious Upsert Response [affected rows overflow]: 3000000100fd000002000028526f7773206d6174636865643a203120204368616e6765643a203020205761726e696e67733a2030 + +Warning: mysqli::query(): RSET_HEADER packet additional data length is past 82 bytes the packet size in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt new file mode 100644 index 000000000000..14357a39613c --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - sha256 public key response with empty packet) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 520000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431007368613235365f70617373776f7264 +[*] Received: 0100000101 +[*] Sending - Malicious SHA256 PK Response [empty packet]: 00000002 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d + +Warning: Error while receiving public key. PID=%d in %s on line %d +[*] Received: 6300000385a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400007368613235365f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000400000002000000 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt new file mode 100644 index 000000000000..d56e69b816c5 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - sha256 public key response shorter than expected) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 520000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431007368613235365f70617373776f7264 +[*] Received: 0100000101 +[*] Sending - Malicious SHA256 PK Response [packet too short]: 0100000200 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d + +Warning: Error while receiving public key. PID=%d in %s on line %d +[*] Received: 6300000385a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400007368613235365f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000400000002000000 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt new file mode 100644 index 000000000000..44d61c244149 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt @@ -0,0 +1,38 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt field length exceeding the packet) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Stmt prepare items: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656dfe6974656d0ce000c8000000fd01100000001f000003fe00000200 + +Warning: mysqli::prepare(): Protocol error. Server sent NULL_LENGTH. The server is faulty in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt new file mode 100644 index 000000000000..69756273b9a8 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt field length over-reading into fixed block) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Malicious Stmt Prepare items [Field length over-read]: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656d0c6974656d0ce000c8000000fd011000000005000003fe00000200 + +Warning: mysqli::prepare(): Protocol error. Server sent false length. Expected 12 in %s on line %d + +Warning: mysqli::prepare(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::prepare(): Result set field packet shorter than expected in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt new file mode 100644 index 000000000000..1c71da1cbbc4 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt @@ -0,0 +1,40 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt response) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Stmt prepare items: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656d046974656d0ce000c8000000fd011000000004000003fe000002 + +Warning: mysqli::prepare(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::prepare(): EOF packet shorter than expected in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt new file mode 100644 index 000000000000..e01dc3ff5c66 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row date field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, datval FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2064617476616c2046524f4d2064617461 +[*] Sending - Stmt prepare data datval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data datval [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt new file mode 100644 index 000000000000..8e86d4039879 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row datetime field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, dtival FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2064746976616c2046524f4d2064617461 +[*] Sending - Stmt prepare data dtival: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data dtival [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt new file mode 100644 index 000000000000..0b9335c42f4e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row time field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, timval FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2074696d76616c2046524f4d2064617461 +[*] Sending - Stmt prepare data timval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data timval [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index c564fca3097e..0b6de80ce682 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -348,6 +348,9 @@ mysqlnd_stmt_prepare_read_eof(MYSQLND_STMT * s) if (FAIL == (ret = PACKET_READ(conn, &fields_eof))) { if (stmt->result) { stmt->result->m.free_result_contents(stmt->result); + /* The memset() below resets the statement, so release what it still owns first. */ + conn->m->free_reference(conn); + mnd_efree(stmt->execute_cmd_buffer.buffer); /* XXX: This will crash, because we will null also the methods. But seems it happens in extreme cases or doesn't. Should be fixed by exporting a function (from mysqlnd_driver.c?) to do the reset. diff --git a/ext/mysqlnd/mysqlnd_ps_codec.c b/ext/mysqlnd/mysqlnd_ps_codec.c index 796516b31028..fbef5f22e117 100644 --- a/ext/mysqlnd/mysqlnd_ps_codec.c +++ b/ext/mysqlnd/mysqlnd_ps_codec.c @@ -63,7 +63,9 @@ static inline bool ps_fetch_is_packet_over_read_with_variable_length(const unsig return false; } size_t length_len = *row - p; - if (length_len > pack_len || length > pack_len - length_len) { + /* This assert should never fire, otherwise we invoked UB earlier */ + ZEND_ASSERT(length_len <= pack_len); + if (length > pack_len - length_len) { ps_fetch_over_read_error(row); return true; } @@ -80,6 +82,18 @@ static inline bool ps_fetch_is_packet_over_read_with_static_length(const unsigne return false; } +/* The declared length has to cover the fixed offsets that the field type reads. */ +static inline bool ps_fetch_is_length_too_short(const zend_uchar ** row, const zend_ulong length, + const unsigned int min_length) +{ + if (UNEXPECTED(length < min_length)) { + php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length is too short for the field type"); + *row = NULL; + return true; + } + return false; +} + /* {{{ ps_fetch_from_1_to_8_bytes */ void @@ -259,11 +273,15 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p const zend_uchar *p = *row; DBG_ENTER("ps_fetch_time"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 8))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_TIME; @@ -273,7 +291,7 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.hour = (unsigned int) to[5]; t.minute = (unsigned int) to[6]; t.second = (unsigned int) to[7]; - t.second_part = (length > 8) ? (zend_ulong) sint4korr(to+8) : 0; + t.second_part = (length >= 12) ? (zend_ulong) sint4korr(to+8) : 0; t.year = t.month= 0; if (t.day) { /* Convert days to hours at once */ @@ -309,11 +327,15 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p const zend_uchar *p = *row; DBG_ENTER("ps_fetch_date"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -325,7 +347,7 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -346,11 +368,15 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i const zend_uchar *p = *row; DBG_ENTER("ps_fetch_datetime"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -360,16 +386,16 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - if (length > 4) { + if (length >= 7) { t.hour = (unsigned int) to[4]; t.minute = (unsigned int) to[5]; t.second = (unsigned int) to[6]; } else { t.hour = t.minute = t.second= 0; } - t.second_part = (length > 7) ? (zend_ulong) sint4korr(to+7) : 0; + t.second_part = (length >= 11) ? (zend_ulong) sint4korr(to+7) : 0; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -393,7 +419,7 @@ static void ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) { const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } @@ -413,7 +439,7 @@ static void ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) { const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index 19debe98089d..8f526798e8e6 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -24,11 +24,18 @@ #include "mysqlnd_statistics.h" #include "mysqlnd_debug.h" -#define BAIL_IF_NO_MORE_DATA \ - if (UNEXPECTED((size_t)(p - begin) > packet->header.size)) { \ +#define BAIL_PREMATURE_END do { \ php_error_docref(NULL, E_WARNING, "Premature end of data (mysqlnd_wireprotocol.c:%u)", __LINE__); \ goto premature_end; \ - } \ + } while (0) + +/* Bail out unless the packet still has the required number of bytes left to read. */ +#define BAIL_IF_NOT_ENOUGH_DATA_EX(min_required_bytes) \ + if (UNEXPECTED((size_t)(p - begin) + (min_required_bytes) > packet->header.size)) { \ + BAIL_PREMATURE_END; \ + } + +#define BAIL_IF_NOT_ENOUGH_DATA BAIL_IF_NOT_ENOUGH_DATA_EX(1) static const char *unknown_sqlstate= "HY000"; @@ -91,10 +98,14 @@ static enum_mysqlnd_collected_stats packet_type_to_statistic_packet_count[PROT_L /* {{{ php_mysqlnd_net_field_length Get next field's length */ zend_ulong -php_mysqlnd_net_field_length(const zend_uchar **packet) +php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size) { const zend_uchar *p= (const zend_uchar *)*packet; + if (UNEXPECTED(remaining_size == 0)) { + return MYSQLND_INVALID_NET_FIELD_LENGTH; + } + if (*p < 251) { (*packet)++; return (zend_ulong) *p; @@ -105,14 +116,23 @@ php_mysqlnd_net_field_length(const zend_uchar **packet) (*packet)++; return MYSQLND_NULL_LENGTH; case 252: - (*packet) += 3; - return (zend_ulong) uint2korr(p+1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + return (zend_ulong) uint2korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; case 253: - (*packet) += 4; - return (zend_ulong) uint3korr(p+1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + return (zend_ulong) uint3korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; default: - (*packet) += 9; - return (zend_ulong) uint4korr(p+1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + return (zend_ulong) uint4korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; } } /* }}} */ @@ -120,29 +140,53 @@ php_mysqlnd_net_field_length(const zend_uchar **packet) /* {{{ php_mysqlnd_net_field_length_ll Get next field's length */ -uint64_t -php_mysqlnd_net_field_length_ll(const zend_uchar **packet) +MYSQLND_OPTIONAL_UINT64_T +php_mysqlnd_net_field_length_ll(const zend_uchar **packet, size_t remaining_size) { const zend_uchar *p = (zend_uchar *)*packet; + MYSQLND_OPTIONAL_UINT64_T result; + result.has_value = false; + result.value = 0; + + if (UNEXPECTED(remaining_size == 0)) { + return result; + } + if (*p < 251) { (*packet)++; - return (uint64_t) *p; + result.value = (uint64_t) *p; + result.has_value = true; + return result; } switch (*p) { case 251: (*packet)++; - return (uint64_t) MYSQLND_NULL_LENGTH; + result.value = MYSQLND_NULL_LENGTH; + result.has_value = true; + return result; case 252: - (*packet) += 3; - return (uint64_t) uint2korr(p + 1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + result.value = (uint64_t) uint2korr(p+1); + result.has_value = true; + } + return result; case 253: - (*packet) += 4; - return (uint64_t) uint3korr(p + 1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + result.value = (uint64_t) uint3korr(p+1); + result.has_value = true; + } + return result; default: - (*packet) += 9; - return (uint64_t) uint8korr(p + 1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + result.value = uint8korr(p+1); + result.has_value = true; + } + return result; } } /* }}} */ @@ -339,22 +383,14 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "greeting", PROT_GREET_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->authentication_plugin_data.s = packet->intern_auth_plugin_data; packet->authentication_plugin_data.l = sizeof(packet->intern_auth_plugin_data); - if (packet->header.size < sizeof(buf)) { - /* - Null-terminate the string, so strdup can work even if the packets have a string at the end, - which is not ASCIIZ - */ - buf[packet->header.size] = '\0'; - } - packet->protocol_version = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->protocol_version) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -374,43 +410,42 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) /* MariaDB always sends 5.5.5 before version string: 5.5.5 was never released, so just ignore it */ - if (!strncmp((char *) p, MARIADB_RPL_VERSION_HACK, sizeof(MARIADB_RPL_VERSION_HACK) - 1)) { + if (packet->header.size - (p - begin) >= sizeof(MARIADB_RPL_VERSION_HACK) - 1 && !strncmp((char *) p, MARIADB_RPL_VERSION_HACK, sizeof(MARIADB_RPL_VERSION_HACK) - 1)) { p += sizeof(MARIADB_RPL_VERSION_HACK) - 1; } - packet->server_version = estrdup((char *)p); - p+= strlen(packet->server_version) + 1; /* eat the '\0' */ - BAIL_IF_NO_MORE_DATA; + /* This server version string MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *version_nul_byte = memchr(p, '\0', packet->header.size - (p - begin)); + if (!version_nul_byte) { + BAIL_PREMATURE_END; + } + packet->server_version = estrndup((char *) p, version_nul_byte - (const char *) p); + p = (unsigned char *) version_nul_byte + 1; /* eat the '\0' */ + + BAIL_IF_NOT_ENOUGH_DATA_EX(4 + SCRAMBLE_LENGTH_323 + 1 + 2 + 1 + 2 + 13); packet->thread_id = uint4korr(p); p+=4; - BAIL_IF_NO_MORE_DATA; memcpy(packet->authentication_plugin_data.s, p, SCRAMBLE_LENGTH_323); p+= SCRAMBLE_LENGTH_323; - BAIL_IF_NO_MORE_DATA; /* pad1 */ p++; - BAIL_IF_NO_MORE_DATA; packet->server_capabilities = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; DBG_INF_FMT("4.1 server_caps=%u\n", (uint32_t) packet->server_capabilities); packet->charset_no = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* pad2 */ pad_start = p; p+= 13; - BAIL_IF_NO_MORE_DATA; if ((size_t) (p - buf) < packet->header.size) { /* auth_plugin_data is split into two parts */ @@ -432,6 +467,9 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) /* And a length of the server scramble in one byte */ packet->authentication_plugin_data.l = uint1korr(pad_start + 2); if (packet->authentication_plugin_data.l > SCRAMBLE_LENGTH) { + /* the rest of the scramble has to be in the packet */ + BAIL_IF_NOT_ENOUGH_DATA_EX(packet->authentication_plugin_data.l - SCRAMBLE_LENGTH); + /* more data*/ char * new_auth_plugin_data = emalloc(packet->authentication_plugin_data.l); @@ -445,11 +483,14 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) } if (packet->server_capabilities & CLIENT_PLUGIN_AUTH) { - BAIL_IF_NO_MORE_DATA; + /* This is actually checked above and it needs to pass to set extended server capabilities + * so it should never bail. */ + BAIL_IF_NOT_ENOUGH_DATA; /* The server is 5.5.x and supports authentication plugins */ size_t remaining_size = packet->header.size - (size_t)(p - buf); if (remaining_size == 0) { - /* Might be better to fail but this will fail anyway */ + /* This should never happen as the size should be at least 1 but it is kept just in + * case something above changes. */ packet->auth_protocol = estrdup(""); } else { /* Check if NUL present */ @@ -483,9 +524,8 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("GREET packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "GREET packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("GREET packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "GREET packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -690,7 +730,7 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* zero-terminate the buffer for safety. We are sure there is place for the \0 @@ -701,7 +741,7 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) /* Should be always 0x0 or ERROR_MARKER for error */ packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -713,9 +753,14 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -726,27 +771,37 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_INF_FMT("Server salt : [%zu][%.*s]", packet->new_auth_protocol_data_len, (int) packet->new_auth_protocol_data_len, packet->new_auth_protocol_data); } } else { - zend_ulong net_len; /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong net_len; + MYSQLND_OPTIONAL_UINT64_T len_ll; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the net_len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + /* In older versions of mysqlnd we had to perform an extra check because reading the field length + * could overread the OK packet. Now reading the length won't go out of bounds and we only have + * to check whether the message length is in bounds. */ + ZEND_ASSERT((p - buf) <= packet->header.size); + if (packet->header.size - (p - buf) < net_len) { DBG_ERR_FMT("OK packet message length is past the packet size"); php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size"); DBG_RETURN(FAIL); @@ -765,9 +820,8 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -860,12 +914,12 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always 0x0 or ERROR_MARKER for error */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -875,22 +929,33 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + MYSQLND_OPTIONAL_UINT64_T len_ll; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + if (UNEXPECTED(net_len == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } packet->message_len = MIN(net_len, buf_len - (p - begin)); packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE); } else { @@ -902,13 +967,10 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) packet->affected_rows, packet->last_insert_id, packet->server_status, packet->warning_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "OK packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "OK packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -953,12 +1015,12 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "EOF", PROT_EOF_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always EODATA_MARKER */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -974,28 +1036,25 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * conn, void * _packet) according to the Docs@Forge!!! */ if (packet->header.size > 1) { + BAIL_IF_NOT_ENOUGH_DATA_EX(4); + packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; } else { packet->warning_count = 0; packet->server_status = 0; } - BAIL_IF_NO_MORE_DATA; - DBG_INF_FMT("EOF packet: fields=%u status=%u warnings=%u", packet->field_count, packet->server_status, packet->warning_count); DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("EOF packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "EOF packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("EOF packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "EOF packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1076,13 +1135,14 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) const zend_uchar * p = buf; const zend_uchar * const begin = buf; size_t len; + MYSQLND_OPTIONAL_UINT64_T len_ll; DBG_ENTER("php_mysqlnd_rset_header_read"); if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "resultset header", PROT_RSET_HEADER_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -1091,7 +1151,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1099,8 +1159,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } - packet->field_count = php_mysqlnd_net_field_length(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong field_count = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(field_count == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } + packet->field_count = field_count; switch (packet->field_count) { case MYSQLND_NULL_LENGTH: @@ -1108,7 +1171,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) /* First byte in the packet is the field count. Thus, the name is size - 1. And we add 1 for a trailing \0. - Because we have BAIL_IF_NO_MORE_DATA before the switch, we are guaranteed + Because we have BAIL_IF_NOT_ENOUGH_DATA before the switch, we are guaranteed that packet->header.size is > 0. Which means that len can't underflow, that would lead to 0 byte allocation but 2^32 or 2^64 bytes copied. */ @@ -1120,24 +1183,34 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) break; case 0x00: DBG_INF("UPSERT"); - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; + + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + /* Check for additional textual data */ - if (packet->header.size > (size_t) (p - buf) && (len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < len) { + if (len && len != MYSQLND_INVALID_NET_FIELD_LENGTH) { + /* This checks both whether reading the len was successful + * and that the len is not greater than the packet size */ + if (packet->header.size - (p - buf) < len) { size_t local_file_name_over_read = ((p - buf) - packet->header.size) + len; DBG_ERR_FMT("RSET_HEADER packet additional data length is past %zu bytes the packet size", local_file_name_over_read); @@ -1160,13 +1233,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) /* Result set */ break; } - BAIL_IF_NO_MORE_DATA; DBG_RETURN(ret); premature_end: - DBG_ERR_FMT("RSET_HEADER packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "RSET_HEADER packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("RSET_HEADER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "RSET_HEADER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1184,8 +1255,8 @@ void php_mysqlnd_rset_header_free_mem(void * _packet) /* }}} */ #define READ_RSET_FIELD(field_name) do { \ - len = php_mysqlnd_net_field_length(&p); \ - if (UNEXPECTED(len == MYSQLND_NULL_LENGTH)) { \ + zend_ulong len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); \ + if (UNEXPECTED(len > packet->header.size - (p - begin))) { \ goto faulty_or_fake; \ } else if (len != 0) { \ meta->field_name = (const char *)p; \ @@ -1229,11 +1300,11 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1297,9 +1368,8 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) } /* COM_FIELD_LIST is no longer supported so def should not be present */ - if (packet->header.size > (size_t) (p - buf) && - (len = php_mysqlnd_net_field_length(&p)) && - len != MYSQLND_NULL_LENGTH) + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (len < MYSQLND_INVALID_NET_FIELD_LENGTH) { DBG_ERR_FMT("Protocol error. Server sent default for unsupported field list"); php_error_docref(NULL, E_WARNING, @@ -1367,9 +1437,8 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) " The server is faulty"); DBG_RETURN(FAIL); premature_end: - DBG_ERR_FMT("RSET field packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "Result set field packet %zu bytes " - "shorter than expected", p - begin - packet->header.size); + DBG_ERR_FMT("RSET field packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "Result set field packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1591,12 +1660,12 @@ php_mysqlnd_rowp_read_text_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fiel for (i = 0, current_field = start_field; current_field < end_field; current_field++, i++) { /* php_mysqlnd_net_field_length() call should be after *this_field_len_pos = p; */ - const zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p); + zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p, packet_end - p); /* NULL or NOT NULL, this is the question! */ if (len == MYSQLND_NULL_LENGTH) { ZVAL_NULL(current_field); - } else if (p > packet_end || len > packet_end - p) { + } else if (len > packet_end - p) { php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after end of packet"); for (j = 0, current_field = start_field; j < i; current_field++, j++) { zval_ptr_dtor(current_field); @@ -1769,7 +1838,8 @@ php_mysqlnd_rowp_read(MYSQLND_CONN_DATA * conn, void * _packet) } else if (EODATA_MARKER == *p && data_size < 8) { /* EOF */ packet->eof = TRUE; p++; - if (data_size > 1) { + /* the marker is followed by 2 bytes of warnings and 2 bytes of status */ + if (data_size >= 5) { packet->warning_count = uint2korr(p); p += 2; packet->server_status = uint2korr(p); @@ -1854,12 +1924,12 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "prepare", PROT_PREPARE_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; data_size = packet->header.size; packet->error_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->error_code) { php_mysqlnd_read_error_from_line(p, data_size - 1, @@ -1879,23 +1949,23 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(FAIL); } + BAIL_IF_NOT_ENOUGH_DATA_EX(8); + packet->stmt_id = uint4korr(p); p += 4; - BAIL_IF_NO_MORE_DATA; /* Number of columns in result set */ packet->field_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; packet->param_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; if (data_size > 9) { + BAIL_IF_NOT_ENOUGH_DATA_EX(3); + /* 0x0 filler sent by the server for 5.0+ clients */ p++; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); } @@ -1903,13 +1973,10 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_INF_FMT("Prepare packet read: stmt_id=" ZEND_ULONG_FMT " fields=%u params=%u", packet->stmt_id, packet->field_count, packet->param_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("PREPARE packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "PREPARE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("PREPARE packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "PREPARE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1936,7 +2003,7 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "change user response", PROT_CHG_USER_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -1961,11 +2028,16 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) packet->error_info.sqlstate ); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (packet->response_code == 0xFE && packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { packet->new_auth_protocol_data = mnd_emalloc(packet->new_auth_protocol_data_len); @@ -1977,9 +2049,8 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("CHANGE_USER packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "CHANGE_USER packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("CHANGE_USER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "CHANGE_USER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2049,10 +2120,10 @@ php_mysqlnd_sha256_pk_request_response_read(MYSQLND_CONN_DATA * conn, void * _pa if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "SHA256_PK_REQUEST_RESPONSE", PROT_SHA256_PK_REQUEST_RESPONSE_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->public_key_len = packet->header.size - (p - buf); packet->public_key = mnd_emalloc(packet->public_key_len + 1); @@ -2062,9 +2133,8 @@ php_mysqlnd_sha256_pk_request_response_read(MYSQLND_CONN_DATA * conn, void * _pa DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2128,11 +2198,11 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "PROT_CACHED_SHA2_RESULT_PACKET", PROT_CACHED_SHA2_RESULT_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -2144,9 +2214,14 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -2163,20 +2238,27 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_ERR_FMT("Unexpected response code %d", packet->response_code); } - /* This is not really the response code, but we reuse the field. */ + BAIL_IF_NOT_ENOUGH_DATA; + + /* This is not really the response code, but we reuse the field. This should be either 0x03 if + * fast auth is used or 0x04 if full auth should be done. */ packet->response_code = uint1korr(p); - p++; - BAIL_IF_NO_MORE_DATA; - packet->result = uint1korr(p); - BAIL_IF_NO_MORE_DATA; + /* This should be removed in master and it is really kept for just in case scenario that should + * never happen because mysql-server sends only byte that is stored above. The result seems not + * to be used so there is not much point to set it in any case. */ + if (UNEXPECTED((size_t)(p - begin) + 2 <= packet->header.size)) { + p++; + packet->result = uint1korr(p); + } else { + packet->result = 0; + } DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.h b/ext/mysqlnd/mysqlnd_wireprotocol.h index f24eb70f4a79..33c90f315a50 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.h +++ b/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -295,9 +295,19 @@ typedef struct st_mysqlnd_packet_cached_sha2_result { unsigned int error_no; } MYSQLND_PACKET_CACHED_SHA2_RESULT; +/* The following structure implements an optional value, + * which forces the caller to check if the value was correctly read, for safety reasons. */ +typedef struct st_mysqlnd_optional_uint64_t { + uint64_t value; + bool has_value; +} MYSQLND_OPTIONAL_UINT64_T; -zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet); +/* must not be equal to MYSQLND_NULL_LENGTH, but larger than 2**24 */ +#define MYSQLND_INVALID_NET_FIELD_LENGTH ((zend_ulong) -2) + +/* Returns MYSQLND_INVALID_NET_FIELD_LENGTH on error */ +zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size); zend_uchar * php_mysqlnd_net_store_length(zend_uchar *packet, const uint64_t length); size_t php_mysqlnd_net_store_length_size(uint64_t length); From 5af9465ca890be679033d5fb0dc89b6ea071326b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 14 Sep 2026 09:12:03 +0200 Subject: [PATCH 06/25] Fix GHSA-fpwc-w8rq-cr92: strip credentials from user headers on cross-origin redirects Authorization, Cookie and Proxy-Authorization set through the http context were forwarded verbatim when follow_location sent the request to another origin. They are now stripped from the user header bag whenever the redirect target differs in scheme, host or port, including when the header name is repeated or the bag uses malformed line endings. Co-authored-by: Jakub Zelenka --- ext/standard/http_fopen_wrapper.c | 109 +++++++++--- ext/standard/tests/http/bug61548.phpt | 2 - .../tests/http/ghsa-fpwc-w8rq-cr92.phpt | 159 ++++++++++++++++++ 3 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index ca0fc0d6ee17..b946684f3710 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -81,27 +81,70 @@ #define HTTP_WRAPPER_HEADER_INIT 1 #define HTTP_WRAPPER_REDIRECTED 2 #define HTTP_WRAPPER_KEEP_METHOD 4 +#define HTTP_WRAPPER_STRIP_AUTH 8 +static char *next_header_line(char *line) +{ + while (*line != '\0' && *line != '\r' && *line != '\n') { + line++; + } + if (*line == '\r') { + line++; + } + if (*line == '\n') { + line++; + } + + return line; +} + +/* Removes every line whose header name matches, along with the folded + * continuation lines carrying the rest of its value. Neither a repeated header + * nor an occurrence of the name inside another header's value may leave the real + * header behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */ static inline void strip_header(char *header_bag, char *lc_header_bag, const char *lc_header_name) { - char *lc_header_start = strstr(lc_header_bag, lc_header_name); - if (lc_header_start - && (lc_header_start == lc_header_bag || *(lc_header_start-1) == '\n') - ) { - char *header_start = header_bag + (lc_header_start - lc_header_bag); - char *lc_eol = strchr(lc_header_start, '\n'); + size_t name_len = strlen(lc_header_name); + char *lc_line = lc_header_bag; - if (lc_eol) { - char *eol = header_start + (lc_eol - lc_header_start); - size_t eollen = strlen(lc_eol); + while (*lc_line != '\0') { + if (strncmp(lc_line, lc_header_name, name_len) != 0) { + lc_line = next_header_line(lc_line); + continue; + } - memmove(lc_header_start, lc_eol+1, eollen); - memmove(header_start, eol+1, eollen); - } else { - *lc_header_start = '\0'; - *header_start = '\0'; + /* the whitespace RFC 7230 forbids before the colon is tolerated by some + * servers, so it must not hide the header from us either */ + const char *lc_colon = lc_line + name_len; + while (*lc_colon == ' ' || *lc_colon == '\t') { + lc_colon++; + } + + if (*lc_colon != ':') { + lc_line = next_header_line(lc_line); + continue; } + + char *lc_next = next_header_line(lc_line); + while (*lc_next == ' ' || *lc_next == '\t') { + lc_next = next_header_line(lc_next); + } + + if (*lc_next == '\0') { + /* drop the preceding line break too, or the one appended after the bag + * would close the header block early */ + while (lc_line > lc_header_bag + && (*(lc_line - 1) == '\r' || *(lc_line - 1) == '\n')) { + --lc_line; + } + } + + size_t tail_len = strlen(lc_next) + 1; + char *line = header_bag + (lc_line - lc_header_bag); + + memmove(line, header_bag + (lc_next - lc_header_bag), tail_len); + memmove(lc_line, lc_next, tail_len); } } @@ -678,8 +721,23 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, if (!header_init && !redirect_keep_method) { /* strip POST headers on redirect */ - strip_header(user_headers, t, "content-length:"); - strip_header(user_headers, t, "content-type:"); + strip_header(user_headers, t, "content-length"); + strip_header(user_headers, t, "content-type"); + } + + if (flags & HTTP_WRAPPER_STRIP_AUTH) { + strip_header(user_headers, t, "authorization"); + strip_header(user_headers, t, "cookie"); + if (!use_proxy) { + strip_header(user_headers, t, "proxy-authorization"); + } + } + + if (*user_headers == '\0') { + /* everything got stripped, keeping the empty bag would append a + * stray CRLF and end the header block early */ + efree(user_headers); + user_headers = NULL; } if (check_has_header(t, "user-agent:")) { @@ -1081,14 +1139,22 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, header_info.location = NULL; } - php_url_free(resource); - /* check for invalid redirection URLs */ - if ((resource = php_url_parse(new_path)) == NULL) { + php_url *new_resource = php_url_parse(new_path); + if (new_resource == NULL) { php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); efree(new_path); goto out; } + int default_port = use_ssl ? 443 : 80; + bool same_origin = zend_string_equals_ci(resource->scheme, new_resource->scheme) + && zend_string_equals_ci(resource->host, new_resource->host) + && (resource->port ? resource->port : default_port) + == (new_resource->port ? new_resource->port : default_port); + + php_url_free(resource); + resource = new_resource; + #define CHECK_FOR_CNTRL_CHARS(val) { \ if (val) { \ unsigned char *s, *e; \ @@ -1110,7 +1176,10 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, CHECK_FOR_CNTRL_CHARS(resource->pass); CHECK_FOR_CNTRL_CHARS(resource->path); } - int new_flags = HTTP_WRAPPER_REDIRECTED; + int new_flags = HTTP_WRAPPER_REDIRECTED | (flags & HTTP_WRAPPER_STRIP_AUTH); + if (!same_origin) { + new_flags |= HTTP_WRAPPER_STRIP_AUTH; + } if (response_code == 307 || response_code == 308) { /* RFC 7538 specifies that status code 308 does not allow changing the request method from POST to GET. * RFC 7231 does the same for status code 307. diff --git a/ext/standard/tests/http/bug61548.phpt b/ext/standard/tests/http/bug61548.phpt index 5f21b3769dd8..ba46e65704f3 100644 --- a/ext/standard/tests/http/bug61548.phpt +++ b/ext/standard/tests/http/bug61548.phpt @@ -55,7 +55,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close @@ -69,7 +68,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close diff --git a/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt new file mode 100644 index 000000000000..5ccc88b3cc4d --- /dev/null +++ b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt @@ -0,0 +1,159 @@ +--TEST-- +GHSA-fpwc-w8rq-cr92: strip credentials from user headers on cross-origin redirect +--INI-- +allow_url_fopen=1 +--SKIPIF-- + +--FILE-- + [ + 'header' => "Authorization: Bearer SECRET\r\n" + . "Cookie: sid=abc\r\n" + . "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n" + . "X-Custom: keep-me", + 'follow_location' => 1, +]]); + +/* server B listens on a different port than server A, so the hop from A to B is + * cross-origin; B then redirects to itself: credentials must stay withheld for + * that same-origin hop too */ +$captureB = null; +['pid' => $pidB, 'uri' => $uriB] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /second\r\nContent-Length: 0\r\n\r\n", + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureB); + +$captureA = null; +['pid' => $pidA, 'uri' => $uriA] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriB/first\r\nContent-Length: 0\r\n\r\n", +], $captureA); + +var_dump(file_get_contents($uriA . '/src', false, $ctx)); + +http_server_kill($pidA); +http_server_kill($pidB); + +rewind($captureA); +rewind($captureB); +report('--- origin A (1 request) ---', stream_get_contents($captureA)); +report('--- origin B (2 requests) ---', stream_get_contents($captureB)); + +/* same origin throughout: credentials must be sent on both hops */ +$captureC = null; +['pid' => $pidC, 'uri' => $uriC] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n", + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureC); + +var_dump(file_get_contents($uriC . '/first', false, $ctx)); + +http_server_kill($pidC); + +rewind($captureC); +report('--- origin C (2 requests) ---', stream_get_contents($captureC)); + +/* a stripped header that was last in the bag must not leave a trailing line break + * behind, or the request body would be pushed out of the request */ +$ctx = stream_context_create(['http' => [ + 'method' => 'POST', + 'content' => 'hello=world', + 'header' => "X-Custom: keep-me\r\nAuthorization: Bearer SECRET", + 'follow_location' => 1, +]]); + +$captureH = null; +['pid' => $pidH, 'uri' => $uriH] = http_server([ + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureH); + +$captureG = null; +['pid' => $pidG, 'uri' => $uriG] = http_server([ + "data://text/plain,HTTP/1.1 307 Temporary Redirect\r\nLocation: $uriH/second\r\nContent-Length: 0\r\n\r\n", +], $captureG); + +var_dump(@file_get_contents($uriG . '/first', false, $ctx)); + +http_server_kill($pidG); +http_server_kill($pidH); + +rewind($captureH); +echo "--- credential header last in the bag (307) ---\n"; +echo preg_replace('/^Host:.*$/m', 'Host: ...', stream_get_contents($captureH)); + +echo "--- malformed header bags ---\n"; +foreach ([ + 'folded value ' => "Authorization:\r\n Bearer SECRET\r\nX-Custom: keep-me", + 'folded value (tab)' => "Authorization:\r\n\tBearer SECRET\r\nX-Custom: keep-me", + 'lone CR ' => "X-Custom: keep-me\rAuthorization: Bearer SECRET", + 'space before colon' => "Authorization : Bearer SECRET\r\nX-Custom: keep-me", + 'tab before colon ' => "Authorization\t: Bearer SECRET\r\nX-Custom: keep-me", +] as $label => $header) { + $ctx = stream_context_create(['http' => ['header' => $header, 'follow_location' => 1]]); + + $captureF = null; + ['pid' => $pidF, 'uri' => $uriF] = http_server([ + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", + ], $captureF); + + $captureE = null; + ['pid' => $pidE, 'uri' => $uriE] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriF/second\r\nContent-Length: 0\r\n\r\n", + ], $captureE); + + file_get_contents($uriE . '/first', false, $ctx); + + http_server_kill($pidE); + http_server_kill($pidF); + + rewind($captureF); + $request = stream_get_contents($captureF); + printf(" %s SECRET leaked: %d, X-Custom kept: %d\n", $label, + str_contains($request, 'SECRET'), str_contains($request, 'X-Custom')); +} +?> +--EXPECT-- +string(2) "OK" +--- origin A (1 request) --- + Authorization: 1 + Cookie: 1 + Proxy-Authorization: 1 + X-Custom: 1 +--- origin B (2 requests) --- + Authorization: 0 + Cookie: 0 + Proxy-Authorization: 0 + X-Custom: 2 +string(2) "OK" +--- origin C (2 requests) --- + Authorization: 2 + Cookie: 2 + Proxy-Authorization: 2 + X-Custom: 2 +string(2) "OK" +--- credential header last in the bag (307) --- +POST /second HTTP/1.1 +Host: ... +Connection: close +Content-Length: 11 +X-Custom: keep-me +Content-Type: application/x-www-form-urlencoded + +hello=world--- malformed header bags --- + folded value SECRET leaked: 0, X-Custom kept: 1 + folded value (tab) SECRET leaked: 0, X-Custom kept: 1 + lone CR SECRET leaked: 0, X-Custom kept: 1 + space before colon SECRET leaked: 0, X-Custom kept: 1 + tab before colon SECRET leaked: 0, X-Custom kept: 1 From 3655b79c7bfa90db004d75a3b282ed0f538f4836 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 17 Sep 2026 08:09:08 +0200 Subject: [PATCH 07/25] Fix GHSA-rgrp-mwpx-f6rm: unbounded recursion in ext/soap XML parsing and decoding --- ext/soap/php_encoding.c | 7 ++ ext/soap/php_soap.h | 3 + ext/soap/php_xml.c | 102 ++++++++++++++---- ext/soap/soap.c | 1 + .../tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt | 47 ++++++++ .../tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt | 43 ++++++++ ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt | 89 +++++++++++++++ 7 files changed, 272 insertions(+), 20 deletions(-) create mode 100644 ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt create mode 100644 ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt create mode 100644 ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c index 0865726ceb9b..553ab7cc3ae4 100644 --- a/ext/soap/php_encoding.c +++ b/ext/soap/php_encoding.c @@ -552,7 +552,12 @@ static zval *master_to_zval_int(zval *ret, encodePtr encode, xmlNodePtr data) } } if (encode->to_zval) { + if (SOAP_GLOBAL(decode_depth) >= SOAP_MAX_DECODE_DEPTH) { + soap_error0(E_ERROR, "Encoding: Nesting level too deep"); + } + SOAP_GLOBAL(decode_depth)++; ret = encode->to_zval(ret, &encode->details, data); + SOAP_GLOBAL(decode_depth)--; } return ret; } @@ -3444,6 +3449,7 @@ void encode_reset_ns() { SOAP_GLOBAL(cur_uniq_ns) = 0; SOAP_GLOBAL(cur_uniq_ref) = 0; + SOAP_GLOBAL(decode_depth) = 0; if (SOAP_GLOBAL(ref_map)) { zend_hash_destroy(SOAP_GLOBAL(ref_map)); } else { @@ -3456,6 +3462,7 @@ void encode_finish() { SOAP_GLOBAL(cur_uniq_ns) = 0; SOAP_GLOBAL(cur_uniq_ref) = 0; + SOAP_GLOBAL(decode_depth) = 0; if (SOAP_GLOBAL(ref_map)) { zend_hash_destroy(SOAP_GLOBAL(ref_map)); efree(SOAP_GLOBAL(ref_map)); diff --git a/ext/soap/php_soap.h b/ext/soap/php_soap.h index 33a071ed4181..e14916c038ea 100644 --- a/ext/soap/php_soap.h +++ b/ext/soap/php_soap.h @@ -150,6 +150,8 @@ struct _soapService { #define SOAP_SSL_METHOD_SSLv3 2 #define SOAP_SSL_METHOD_SSLv23 3 +#define SOAP_MAX_XML_DEPTH 2048 +#define SOAP_MAX_DECODE_DEPTH (SOAP_MAX_XML_DEPTH * 2) ZEND_BEGIN_MODULE_GLOBALS(soap) HashTable defEncNs; /* mapping of default namespaces to prefixes */ @@ -175,6 +177,7 @@ ZEND_BEGIN_MODULE_GLOBALS(soap) HashTable wsdl_cache; int cur_uniq_ref; HashTable *ref_map; + unsigned int decode_depth; ZEND_END_MODULE_GLOBALS(soap) #ifdef ZTS diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c index 20fd91ac4b49..340dbd8ea4ae 100644 --- a/ext/soap/php_xml.c +++ b/ext/soap/php_xml.c @@ -35,36 +35,72 @@ static int is_blank(const xmlChar* str) return 1; } -/* removes all empty text, comments and other insignoficant nodes */ +/* removes all empty text, comments and other insignoficant nodes. + * Iterative because recursion overflows the stack on a deep document. */ static void cleanup_xml_node(xmlNodePtr node) { - xmlNodePtr trav; - xmlNodePtr del = NULL; + xmlNodePtr parent = node; + xmlNodePtr trav = node->children; - trav = node->children; while (trav != NULL) { - if (del != NULL) { - xmlUnlinkNode(del); - xmlFreeNode(del); - del = NULL; - } + xmlNodePtr next = trav->next; + if (trav->type == XML_TEXT_NODE) { if (is_blank(trav->content)) { - del = trav; + xmlUnlinkNode(trav); + xmlFreeNode(trav); } } else if ((trav->type != XML_ELEMENT_NODE) && (trav->type != XML_CDATA_SECTION_NODE)) { - del = trav; + xmlUnlinkNode(trav); + xmlFreeNode(trav); } else if (trav->children != NULL) { - cleanup_xml_node(trav); + parent = trav; + trav = trav->children; + continue; } - trav = trav->next; + + while (next == NULL) { + if (parent == node) { + return; + } + next = parent->next; + parent = parent->parent; + } + trav = next; } - if (del != NULL) { - xmlUnlinkNode(del); - xmlFreeNode(del); +} + +#if LIBXML_VERSION < 21300 +static int is_nesting_too_deep(xmlNodePtr node) +{ + xmlNodePtr trav = node->children; + unsigned int depth = 0; + + while (trav != NULL) { + /* An entity reference borrows its declaration as child list, and that + * declaration hangs off the DTD, so descending leaves the document. */ + if (trav->children != NULL && + trav->type != XML_ENTITY_REF_NODE && + trav->type != XML_DTD_NODE) { + if (++depth > SOAP_MAX_XML_DEPTH) { + return TRUE; + } + trav = trav->children; + continue; + } + while (trav->next == NULL) { + trav = trav->parent; + if (trav == node) { + return FALSE; + } + depth--; + } + trav = trav->next; } + return FALSE; } +#endif static void soap_ignorableWhitespace(void *ctx, const xmlChar *ch, int len) { @@ -125,6 +161,15 @@ xmlDocPtr soap_xmlParseFile(const char *filename) */ if (ret) { +#if LIBXML_VERSION < 21300 + if (is_nesting_too_deep((xmlNodePtr)ret)) { + /* php_sdl.c reports xmlGetLastError() as the reason, and libxml2 did + * not fail here, so drop the error an earlier parse left behind. */ + xmlResetLastError(); + xmlFreeDoc(ret); + return NULL; + } +#endif cleanup_xml_node((xmlNodePtr)ret); } return ret; @@ -175,6 +220,13 @@ xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size) xmlCleanupParser(); */ +#if LIBXML_VERSION < 21300 + if (ret && is_nesting_too_deep((xmlNodePtr)ret)) { + xmlFreeDoc(ret); + ret = NULL; + } +#endif + /* if (ret) { cleanup_xml_node((xmlNodePtr)ret); @@ -297,6 +349,8 @@ xmlNodePtr get_node_with_attribute_ex(xmlNodePtr node, char *name, char *name_ns xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, char *name, char *name_ns, char *attribute, char *value, char *attr_ns) { + unsigned int depth = 0; + while (node != NULL) { if (node_is_equal_ex(node, name, name_ns)) { xmlAttrPtr attr = get_attribute_ex(node->properties, attribute, attr_ns); @@ -304,11 +358,19 @@ xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, char *name, cha return node; } } - if (node->children != NULL) { - xmlNodePtr tmp = get_node_with_attribute_recursive_ex(node->children, name, name_ns, attribute, value, attr_ns); - if (tmp) { - return tmp; + if (node->children != NULL && + node->type != XML_ENTITY_REF_NODE && + node->type != XML_DTD_NODE) { + node = node->children; + depth++; + continue; + } + while (node->next == NULL) { + if (depth == 0) { + return NULL; } + node = node->parent; + depth--; } node = node->next; } diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 02501b8d73ff..8850895ed593 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -346,6 +346,7 @@ static void php_soap_init_globals(zend_soap_globals *soap_globals) soap_globals->soap_version = SOAP_1_1; soap_globals->mem_cache = NULL; soap_globals->ref_map = NULL; + soap_globals->decode_depth = 0; } PHP_MSHUTDOWN_FUNCTION(soap) diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt new file mode 100644 index 000000000000..e07f78d816f3 --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on a chain of href references +--EXTENSIONS-- +soap +--FILE-- +' . ($i < $n ? '' : 'leaf') . ''; + } + + return '

' + . $links . ''; +} + +function test($arg) +{ + $GLOBALS['decoded'] = $arg; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(chain(3)); +var_dump($GLOBALS['decoded']); + +$server->handle(chain(12000)); + +?> +--EXPECTF-- + +ok +object(stdClass)#%d (1) { + ["q"]=> + object(stdClass)#%d (1) { + ["q"]=> + string(4) "leaf" + } +} + +SOAP-ENV:ServerSOAP-ERROR: Encoding: Nesting level too deep diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt new file mode 100644 index 000000000000..5058ba109bf8 --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on an array element referencing its own array +--EXTENSIONS-- +soap +--FILE-- +' + . '' . $param . ''; +} + +function test($arg) +{ + $GLOBALS['decoded'] = $arg; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(envelope('ab')); +var_dump($GLOBALS['decoded']); + +$server->handle(envelope('')); + +?> +--EXPECTF-- + +ok +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} + +SOAP-ENV:ServerSOAP-ERROR: Encoding: Nesting level too deep diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt new file mode 100644 index 000000000000..f3213962b47f --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt @@ -0,0 +1,89 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on deeply nested XML +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--FILE-- +' + . str_repeat('', $depth) . 'leaf' . str_repeat('', $depth) + . ''; +} + +function test($arg) +{ + $depth = 0; + while (is_object($arg)) { + $depth++; + $arg = $arg->a; + } + $GLOBALS['decoded'] = [$depth, $arg]; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(envelope(200)); +var_dump($GLOBALS['decoded']); + +/* libxml2 2.13 rejects past 2048 despite XML_PARSE_HUGE, older ones do not. */ +$wsdl = __DIR__ . '/GHSA-rgrp-mwpx-f6rm.wsdl'; +foreach ([1000, 2100] as $depth) { + file_put_contents($wsdl, '' . str_repeat('', $depth) . 'leaf' . str_repeat('', $depth) . ''); + try { + new SoapClient($wsdl); + } catch (SoapFault $e) { + echo rtrim($e->getMessage()), "\n"; + } +} + +/* A WSDL reached through an entity reference still loads. Its replacement tree cannot + be built deeper than the 2048 libxml2 2.13 enforces, so this pins the entity skip + rather than the limit. */ +$entity = __DIR__ . '/GHSA-rgrp-mwpx-f6rm-entity.wsdl'; +$desc = str_repeat('<p>', 300) . 'A test service.' . str_repeat('</p>', 300); +file_put_contents($entity, ' +]> + + &desc; + + + + + + + +'); +var_dump((new SoapClient($entity))->__getFunctions()); + +$server->handle(envelope(3000)); + +?> +--CLEAN-- + +--EXPECTF-- + +ok +array(2) { + [0]=> + int(199) + [1]=> + string(4) "leaf" +} +SOAP-ERROR: Parsing WSDL: Couldn't find in '%sGHSA-rgrp-mwpx-f6rm.wsdl' +SOAP-ERROR: Parsing WSDL: Couldn't load from '%sGHSA-rgrp-mwpx-f6rm.wsdl'%S +array(1) { + [0]=> + string(9) "void op()" +} + +SOAP-ENV:ClientBad Request From b11bd1d9390bb203684e3d4aa5e1104ea842de6f Mon Sep 17 00:00:00 2001 From: Nora Dossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:30:55 +0200 Subject: [PATCH 08/25] Fix GHSA-cj93-vc83-wgqv Co-authored-by: Jakub Zelenka --- ext/soap/php_http.c | 19 ++-- ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt | 99 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 00aa54c83efd..ad2a3a8b06fb 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -1144,7 +1144,7 @@ int make_http_soap_request(zval *this_ptr, char *t = ZSTR_VAL(phpurl->path); char *p = strrchr(t, '/'); if (p) { - zend_string *s = zend_string_alloc((p - t) + ZSTR_LEN(new_url->path) + 2, 0); + zend_string *s = zend_string_safe_alloc(1, p - t, ZSTR_LEN(new_url->path) + 2, 0); strncpy(ZSTR_VAL(s), t, (p - t) + 1); ZSTR_VAL(s)[(p - t) + 1] = 0; strcat(ZSTR_VAL(s), ZSTR_VAL(new_url->path)); @@ -1435,7 +1435,8 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers) { zend_string *http_buf = NULL; char *header; - int header_close = close, header_chunked = 0, header_length = 0, http_buf_size = 0; + int header_close = close, header_chunked = 0, header_length = 0; + size_t http_buf_size = 0; if (!close) { header = get_http_header_value(headers, "Connection: "); @@ -1465,14 +1466,14 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers) done = FALSE; while (!done) { - int buf_size = 0; + unsigned int buf_size = 0; php_stream_gets(stream, headerbuf, sizeof(headerbuf)); if (sscanf(headerbuf, "%x", &buf_size) > 0 ) { if (buf_size > 0) { size_t len_size = 0; - if (http_buf_size + buf_size + 1 < 0) { + if (buf_size >= ZSTR_MAX_LEN - http_buf_size) { if (http_buf) { zend_string_release_ex(http_buf, 0); } @@ -1480,7 +1481,7 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers) } if (http_buf) { - http_buf = zend_string_realloc(http_buf, http_buf_size + buf_size, 0); + http_buf = zend_string_safe_realloc(http_buf, 1, http_buf_size, buf_size, false); } else { http_buf = zend_string_alloc(buf_size, 0); } @@ -1539,7 +1540,7 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers) } } else if (header_length) { - if (header_length < 0 || header_length >= INT_MAX) { + if (header_length < 0 || header_length >= ZSTR_MAX_LEN) { return NULL; } http_buf = zend_string_alloc(header_length, 0); @@ -1554,7 +1555,11 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers) do { ssize_t len_read; if (http_buf) { - http_buf = zend_string_realloc(http_buf, http_buf_size + 4096, 0); + if (UNEXPECTED(http_buf_size >= ZSTR_MAX_LEN - 4096)) { + zend_string_efree(http_buf); + return NULL; + } + http_buf = zend_string_realloc(http_buf, http_buf_size + 4096, false); } else { http_buf = zend_string_alloc(4096, 0); } diff --git a/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt new file mode 100644 index 000000000000..8813d956ec5a --- /dev/null +++ b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt @@ -0,0 +1,99 @@ +--TEST-- +GHSA-cj93-vc83-wgqv +--INI-- +soap.wsdl_cache_enabled=0 +memory_limit=-1 +--EXTENSIONS-- +soap +--CONFLICTS-- +all +--SKIPIF-- + 4) { + $enough_free_ram = true; + } + } + } +} + +if (empty($enough_free_ram)) { + die(sprintf("skip need +4G free RAM, but only %01.2f available", $free_ram)); +} +--FILE-- + $v) { + $chunks[$k] = sprintf("%08x\r\n%s\r\n", strlen($v), $v); + } + + return join('', $chunks); +} + +$wsdl = file_get_contents(__DIR__.'/../server030.wsdl'); + +$soap = << +text0text1text2text3text4text5text6text7text8text9 +EOF; + +$responses = [ + "data://text/plain,HTTP/1.1 200 OK\r\n". + "Content-Type: text/xml;charset=utf-8\r\n". + "Transfer-Encoding: \t chunked\t \r\n". + "Connection: close\r\n". + "\r\n". + chunk_body($wsdl, 64), + "data://text/plain,HTTP/1.1 200 OK\r\n". + "Content-Type: text/xml;charset=utf-8\r\n". + "Transfer-Encoding: \t chunked\t \r\n". + "Connection: close\r\n". + "\r\n". + /* The second chunk only needs its size header: the reallocation for it + * happens before its body is read, so the overflow triggers on the first + * read into the undersized buffer. */ + sprintf("%08x\r\n", 0x7fffffff).str_repeat('x', 0x7fffffff)."\r\n" . + sprintf("%08x\r\n", 0x7fffffff)."xxxx", +]; + + +['pid' => $pid, 'uri' => $uri] = http_server($responses); + +$options = [ + 'trace' => false, + 'location' => $uri, +]; + +$client = new SoapClient($uri, $options); + +$client->getItems(); + +http_server_kill($pid); + +--EXPECTF-- +Fatal error: Uncaught SoapFault exception: [HTTP] Error Fetching http body, No Content-Length, connection closed or chunked data in %s:%d +Stack trace: +#0 [internal function]: SoapClient->__doRequest('__call('getItems', Array) +#2 {main} + thrown in %s on line %d From 0994e2e887cd993866b5e4d7dba2fade5caeefec Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 13 Sep 2026 22:37:00 +0200 Subject: [PATCH 09/25] Fix GHSA-j3wh-g957-2m85: phar tar entry injection Reject entry sizes that cannot be parsed or represented, and skip the data blocks of every entry type that carries data. Both left the stream on attacker controlled data that was then parsed as a tar header. --- ext/phar/phar_internal.h | 5 + ext/phar/tar.c | 80 +++++++++++++++- .../tar/GHSA-j3wh-g957-2m85-longlink.phpt | 50 ++++++++++ .../tests/tar/GHSA-j3wh-g957-2m85-size.phpt | 92 +++++++++++++++++++ .../tar/GHSA-j3wh-g957-2m85-typeflag.phpt | 79 ++++++++++++++++ 5 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt create mode 100644 ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt create mode 100644 ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index 30b408a8c446..81245a3e9b15 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -115,8 +115,13 @@ #define TAR_FILE '0' #define TAR_LINK '1' #define TAR_SYMLINK '2' +#define TAR_CHAR '3' +#define TAR_BLOCK '4' #define TAR_DIR '5' +#define TAR_FIFO '6' #define TAR_NEW '8' +#define TAR_LONGLINK 'K' +#define TAR_LONGNAME 'L' #define TAR_GLOBAL_HDR 'g' #define TAR_FILE_HDR 'x' diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 652062679d70..345569d4719f 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -37,6 +37,52 @@ static uint32_t phar_tar_number(char *buf, size_t len) /* {{{ */ } /* }}} */ +static bool phar_tar_type_has_data(char typeflag) +{ + switch (typeflag) { + case TAR_LINK: + case TAR_SYMLINK: + case TAR_CHAR: + case TAR_BLOCK: + case TAR_DIR: + case TAR_FIFO: + return false; + default: + return true; + } +} + +static bool phar_tar_size(const char *buf, size_t len, uint32_t *result) +{ + uint64_t num = 0; + size_t i = 0; + + while (i < len && buf[i] == ' ') { + ++i; + } + + /* GNU base-256 encoding is only used for sizes that do not fit the octal field */ + if (i < len && (((unsigned char) buf[i]) & 0x80)) { + return false; + } + + while (i < len && buf[i] >= '0' && buf[i] <= '7') { + num = num * 8 + (buf[i] - '0'); + ++i; + } + + while (i < len && (buf[i] == ' ' || buf[i] == '\0')) { + ++i; + } + + if (i != len || num > UINT32_MAX - 511) { + return false; + } + + *result = (uint32_t) num; + return true; +} + /* adapted from format_octal() in libarchive * * Copyright (c) 2003-2009 Tim Kientzle @@ -276,8 +322,32 @@ int phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, char *alia } } - size = entry.uncompressed_filesize = entry.compressed_filesize = - phar_tar_number(hdr->size, sizeof(hdr->size)); + if (!phar_tar_size(hdr->size, sizeof(hdr->size), &size)) { + if (error) { + spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); + } + if (last_was_longlink) { + pefree(entry.filename, myphar->is_persistent); + } + php_stream_close(fp); + phar_destroy_phar_data(myphar); + return FAILURE; + } + entry.uncompressed_filesize = entry.compressed_filesize = size; + + /* GNU long link names are not supported, so refuse the record instead of + * registering it as an entry and dropping the link target of the entry that follows */ + if (hdr->typeflag == TAR_LONGLINK) { + if (error) { + spprintf(error, 4096, "phar error: \"%s\" is a tar file with an unsupported GNU long link entry", fname); + } + if (last_was_longlink) { + pefree(entry.filename, myphar->is_persistent); + } + php_stream_close(fp); + phar_destroy_phar_data(myphar); + return FAILURE; + } /* skip global/file headers (pax) */ if (!old && (hdr->typeflag == TAR_GLOBAL_HDR || hdr->typeflag == TAR_FILE_HDR)) { @@ -365,13 +435,13 @@ int phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, char *alia goto bail; } - if (!last_was_longlink && hdr->typeflag == 'L') { + if (!last_was_longlink && hdr->typeflag == TAR_LONGNAME) { last_was_longlink = 1; /* support the ././@LongLink system for storing long filenames */ entry.filename_len = entry.uncompressed_filesize; /* Check for overflow - bug 61065 */ - if (entry.filename_len == UINT_MAX || entry.filename_len == 0) { + if (entry.filename_len == 0 || entry.filename_len > totalsize) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); } @@ -582,7 +652,7 @@ int phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, char *alia size = (size+511)&~511; - if (((hdr->typeflag == '\0') || (hdr->typeflag == TAR_FILE)) && size > 0) { + if (phar_tar_type_has_data(hdr->typeflag) && size > 0) { next: /* this is not good enough - seek succeeds even on truncated tars */ php_stream_seek(fp, size, SEEK_CUR); diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt new file mode 100644 index 000000000000..5d05ac311ef0 --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (././@LongLink name longer than the archive) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +$fname = __DIR__ . '/' . basename(__FILE__, '.php') . '.tar'; + +/* 2 GB of file name announced by a 2 KB archive */ +$tar = tar_header('././@LongLink', "20000000000\0", 'L'); +$tar .= str_pad('long.txt', 512, "\0"); +$tar .= tar_header('short.txt', sprintf("%011o\0", 3)) . str_pad('abc', 512, "\0"); +$tar .= str_repeat("\0", 1024); +file_put_contents($fname, $tar); + +try { + new PharData($fname); +} catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECTF-- +phar error: "%s" is a corrupted tar file (invalid entry size) diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt new file mode 100644 index 000000000000..731422a0250b --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt @@ -0,0 +1,92 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (tar entry injection via invalid entry size) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +function tar_dump(string $fname): void +{ + try { + $phar = new PharData($fname); + $names = []; + foreach (new RecursiveIteratorIterator($phar) as $file) { + $names[] = $file->getFilename(); + } + sort($names); + echo implode(', ', $names), "\n"; + } catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; + } +} + +$base = __DIR__ . '/' . basename(__FILE__, '.php'); + +/* the entry after the carrier is never reached by a conforming tar reader */ +$sizes = [ + 'octal overflow to 0' => "40000000000\0", + 'octal overflow to 1' => "40000000001\0", + 'octal padding overflow' => "37777777400\0", + 'GNU base-256' => "\x80" . str_repeat("\0", 9) . "\x02\x00", + 'non-octal digits' => "99999999999\0", + 'trailing garbage' => "0000000001XX", +]; + +$i = 0; +foreach ($sizes as $label => $size) { + $fname = $base . '.' . $i++ . '.tar'; + $tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); + $tar .= tar_header('carrier.bin', $size); + $tar .= tar_header('injected.txt', sprintf("%011o\0", 8)) . str_pad('INJECTED', 512, "\0"); + $tar .= str_repeat("\0", 1024); + file_put_contents($fname, $tar); + + echo $label, ': '; + tar_dump($fname); +} + +$fname = $base . '.valid.tar'; +$tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); +$tar .= tar_header('second.txt', sprintf("%011o\0", 8)) . str_pad('SECOND__', 512, "\0"); +$tar .= str_repeat("\0", 1024); +file_put_contents($fname, $tar); + +echo 'valid archive: '; +tar_dump($fname); +?> +--CLEAN-- + +--EXPECTF-- +octal overflow to 0: phar error: "%s" is a corrupted tar file (invalid entry size) +octal overflow to 1: phar error: "%s" is a corrupted tar file (invalid entry size) +octal padding overflow: phar error: "%s" is a corrupted tar file (invalid entry size) +GNU base-256: phar error: "%s" is a corrupted tar file (invalid entry size) +non-octal digits: phar error: "%s" is a corrupted tar file (invalid entry size) +trailing garbage: phar error: "%s" is a corrupted tar file (invalid entry size) +valid archive: normal.txt, second.txt diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt new file mode 100644 index 000000000000..227d33a4823b --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt @@ -0,0 +1,79 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (tar entry injection via entry types that carry data) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +$base = __DIR__ . '/' . basename(__FILE__, '.php'); + +/* '5' and the other types below carry no data in any tar reader, so the + * injected header stays visible to all of them and is not hidden from + * anything that inspects the archive with a different implementation. + * 'K' is a GNU metadata record that is not supported and is refused. */ +$typeflags = [ + 'contiguous file' => '7', + 'unknown type' => 'Z', + 'GNU long link' => 'K', + 'directory' => '5', + 'fifo' => '6', +]; + +$i = 0; +foreach ($typeflags as $label => $typeflag) { + $fname = $base . '.' . $i++ . '.tar'; + $tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); + $tar .= tar_header('carrier.bin', sprintf("%011o\0", 512), $typeflag); + /* the carrier's only data block is a tar header a conforming reader skips */ + $tar .= tar_header('injected.txt', sprintf("%011o\0", 0)); + $tar .= str_repeat("\0", 1024); + file_put_contents($fname, $tar); + + echo $label, ': '; + try { + $phar = new PharData($fname); + $names = []; + foreach (new RecursiveIteratorIterator($phar) as $file) { + $names[] = $file->getFilename(); + } + sort($names); + echo implode(', ', $names), "\n"; + } catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--CLEAN-- + +--EXPECTF-- +contiguous file: carrier.bin, normal.txt +unknown type: carrier.bin, normal.txt +GNU long link: phar error: "%s" is a tar file with an unsupported GNU long link entry +directory: injected.txt, normal.txt +fifo: carrier.bin, injected.txt, normal.txt From f785c3f6ce2917c20529c12da80e20f9e1c5e493 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Fri, 18 Sep 2026 16:32:20 +0200 Subject: [PATCH 10/25] Fix GHSA-9f67-6fw4-hpfp Reject Windows reserved device names (CON, NUL, COM1, ...) used as a path component in filesystem operations. Bare device names and DOS device paths keep working. Co-authored-by: Jakub Zelenka --- Zend/zend_virtual_cwd.c | 13 ++ .../tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt | 86 ++++++++++ win32/ioutil.c | 162 ++++++++++++++++++ win32/ioutil.h | 12 +- 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt diff --git a/Zend/zend_virtual_cwd.c b/Zend/zend_virtual_cwd.c index a2ea4de165f4..6cbaeb248be3 100644 --- a/Zend/zend_virtual_cwd.c +++ b/Zend/zend_virtual_cwd.c @@ -1030,6 +1030,19 @@ CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func fprintf(stderr,"cwd = %s path = %s\n", state->cwd, path); #endif +#ifdef ZEND_WIN32 + switch (php_win32_ioutil_path_kind_a(path, path_length)) { + case PHP_WIN32_IOUTIL_PATH_RESERVED: + SET_ERRNO_FROM_WIN32_CODE(ERROR_INVALID_NAME); + return 1; + case PHP_WIN32_IOUTIL_PATH_DEVICE: + memcpy(resolved_path, path, path_length + 1); + goto verify; + default: + break; + } +#endif + /* cwd_length can be 0 when getcwd() fails. * This can happen under solaris when a dir does not have read permissions * but *does* have execute permissions */ diff --git a/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt b/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt new file mode 100644 index 000000000000..d83260e5507e --- /dev/null +++ b/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt @@ -0,0 +1,86 @@ +--TEST-- +GHSA-9f67-6fw4-hpfp: Windows reserved device names are rejected in filesystem paths +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Bare device names keep working: +bool(true) +bool(true) +bool(true) +Reserved names in paths are rejected: +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Similar names are fine: +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/win32/ioutil.c b/win32/ioutil.c index a6f2fe2ae2fc..40e0ce5a1f48 100644 --- a/win32/ioutil.c +++ b/win32/ioutil.c @@ -71,6 +71,159 @@ typedef HRESULT (__stdcall *MyPathCchCanonicalizeEx)(wchar_t *pszPathOut, size_t static MyPathCchCanonicalizeEx canonicalize_path_w = NULL; +typedef ULONG (WINAPI *php_win32_ioutil_rtl_is_dos_device_name_u_t)(PCWSTR); + +static php_win32_ioutil_rtl_is_dos_device_name_u_t rtl_is_dos_device_name_u = NULL; + +static BOOL php_win32_ioutil_is_reserved_name_w(const wchar_t *name, size_t len) +{/*{{{*/ + if (len == 3) { + return _wcsnicmp(name, L"CON", 3) == 0 + || _wcsnicmp(name, L"PRN", 3) == 0 + || _wcsnicmp(name, L"AUX", 3) == 0 + || _wcsnicmp(name, L"NUL", 3) == 0; + } + + if (len == 4 && (_wcsnicmp(name, L"COM", 3) == 0 || _wcsnicmp(name, L"LPT", 3) == 0)) { + return (name[3] >= L'1' && name[3] <= L'9') + || name[3] == L'\u00B2' + || name[3] == L'\u00B3' + || name[3] == L'\u00B9'; + } + + return (len == 6 && _wcsnicmp(name, L"CONIN$", 6) == 0) + || (len == 7 && _wcsnicmp(name, L"CONOUT$", 7) == 0); +}/*}}}*/ + +/* Also catches variants like NUL.txt, NUL:stream or "NUL ", as far as the OS treats them as devices. */ +static BOOL php_win32_ioutil_is_reserved_component_w(const wchar_t *name, size_t len) +{/*{{{*/ + size_t base_len = len; + wchar_t *tmp; + BOOL ret; + ALLOCA_FLAG(use_heap) + + if (php_win32_ioutil_is_reserved_name_w(name, len)) { + return TRUE; + } + + for (size_t i = 0; i < len; i++) { + if (name[i] == L'.' || name[i] == L':') { + base_len = i; + break; + } + } + while (base_len > 0 && name[base_len - 1] == L' ') { + base_len--; + } + + if (base_len == len || !php_win32_ioutil_is_reserved_name_w(name, base_len)) { + return FALSE; + } + + if (!rtl_is_dos_device_name_u) { + return TRUE; + } + + tmp = do_alloca((len + 1) * sizeof(wchar_t), use_heap); + memcpy(tmp, name, len * sizeof(wchar_t)); + tmp[len] = L'\0'; + ret = rtl_is_dos_device_name_u(tmp) > 0; + free_alloca(tmp, use_heap); + + return ret; +}/*}}}*/ + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_w(const wchar_t *path, size_t path_len) +{/*{{{*/ + size_t i = 0; + + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + + if (i == path_len && !(path_len > 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':')) { + if (path_len == 0) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + /* Bare device names like NUL or NUL: are kept working for BC. */ + if (php_win32_ioutil_is_reserved_name_w(path, path_len - (path[path_len - 1] == L':'))) { + return PHP_WIN32_IOUTIL_PATH_DEVICE; + } + return php_win32_ioutil_is_reserved_component_w(path, path_len) + ? PHP_WIN32_IOUTIL_PATH_RESERVED : PHP_WIN32_IOUTIL_PATH_OK; + } + + /* Windows does not map device names within DOS device paths (\\.\, \\?\ and \??\). */ + if (path_len >= 4 && PHP_WIN32_IOUTIL_IS_SLASHW(path[3]) + && ((PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1]) && (path[2] == L'.' || path[2] == L'?')) + || (path[0] == L'\\' && path[1] == L'?' && path[2] == L'?'))) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + + i = 0; + if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1])) { + /* UNC, skip server and share. */ + for (int n = 0; n < 2; n++) { + while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + } + } else if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':') { + i = 2; + } + + while (i < path_len) { + size_t start; + + while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + start = i; + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + if (i > start && php_win32_ioutil_is_reserved_component_w(path + start, i - start)) { + return PHP_WIN32_IOUTIL_PATH_RESERVED; + } + } + + return PHP_WIN32_IOUTIL_PATH_OK; +}/*}}}*/ + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_a(const char *path, size_t path_len) +{/*{{{*/ + wchar_t *pathw; + size_t i, pathw_len; + php_win32_ioutil_path_kind ret; + ALLOCA_FLAG(use_heap) + + for (i = 0; i < path_len && !(path[i] & 0x80); i++); + + if (i < path_len) { + pathw = php_win32_cp_conv_any_to_w(path, path_len, &pathw_len); + if (!pathw) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + ret = php_win32_ioutil_path_kind_w(pathw, pathw_len); + free(pathw); + return ret; + } + + /* ASCII only, widen on the stack instead of a full conversion. */ + pathw = do_alloca((path_len + 1) * sizeof(wchar_t), use_heap); + for (i = 0; i < path_len; i++) { + pathw[i] = (wchar_t) path[i]; + } + ret = php_win32_ioutil_path_kind_w(pathw, path_len); + free_alloca(pathw, use_heap); + + return ret; +}/*}}}*/ + PW32IO BOOL php_win32_ioutil_posix_to_open_opts(int flags, mode_t mode, php_ioutil_open_opts *opts) {/*{{{*/ int current_umask; @@ -661,6 +814,11 @@ BOOL php_win32_ioutil_init(void) canonicalize_path_w = (MyPathCchCanonicalizeEx)MyPathCchCanonicalizeExFallback; } + hMod = GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + rtl_is_dos_device_name_u = (php_win32_ioutil_rtl_is_dos_device_name_u_t)GetProcAddress(hMod, "RtlIsDosDeviceName_U"); + } + return TRUE; }/*}}}*/ @@ -668,6 +826,8 @@ PW32IO int php_win32_ioutil_access_w(const wchar_t *path, mode_t mode) {/*{{{*/ DWORD attr; + PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0) + if ((mode & X_OK) == X_OK) { DWORD type; return GetBinaryTypeW(path, &type) ? 0 : -1; @@ -977,6 +1137,8 @@ PW32IO int php_win32_ioutil_stat_ex_w(const wchar_t *path, size_t path_len, php_ int ret; ALLOCA_FLAG(use_heap_large) + PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0) + hLink = CreateFileW(path, FILE_READ_ATTRIBUTES, PHP_WIN32_IOUTIL_DEFAULT_SHARE_MODE, diff --git a/win32/ioutil.h b/win32/ioutil.h index d92e3fb8f512..c3d168011028 100644 --- a/win32/ioutil.h +++ b/win32/ioutil.h @@ -110,6 +110,15 @@ typedef enum { PHP_WIN32_IOUTIL_NORM_FAIL, } php_win32_ioutil_normalization_result; +typedef enum { + PHP_WIN32_IOUTIL_PATH_OK, + PHP_WIN32_IOUTIL_PATH_DEVICE, + PHP_WIN32_IOUTIL_PATH_RESERVED, +} php_win32_ioutil_path_kind; + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_w(const wchar_t *path, size_t path_len); +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_a(const char *path, size_t path_len); + #define PHP_WIN32_IOUTIL_FW_SLASHW L'/' #define PHP_WIN32_IOUTIL_FW_SLASH '/' #define PHP_WIN32_IOUTIL_BW_SLASHW L'\\' @@ -155,7 +164,8 @@ typedef enum { #define PHP_WIN32_IOUTIL_PATH_IS_OK_W(pathw, len) \ (!((len) >= 1 && L' ' == pathw[(len)-1] || \ - (len) > 1 && !PHP_WIN32_IOUTIL_IS_SLASHW(pathw[(len)-2]) && L'.' != pathw[(len)-2] && L'.' == pathw[(len)-1])) + (len) > 1 && !PHP_WIN32_IOUTIL_IS_SLASHW(pathw[(len)-2]) && L'.' != pathw[(len)-2] && L'.' == pathw[(len)-1]) \ + && PHP_WIN32_IOUTIL_PATH_RESERVED != php_win32_ioutil_path_kind_w(pathw, len)) #define PHP_WIN32_IOUTIL_CHECK_PATH_W(pathw, ret, dealloc) do { \ size_t _len = wcslen(pathw); \ From 7ac9700d0d46a7b96e72e330efd295a3332124b0 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 29 Jul 2026 11:21:50 -0400 Subject: [PATCH 11/25] Fix GHSA-ch8v-r6jh-4vvr: encode 0xFF in FILTER_SANITIZE_ENCODED php_filter_encode_url() initialized its 256-byte "must encode" table with memset(tmp, 1, sizeof(tmp) - 1), leaving tmp[255] uninitialized. Whether 0xFF got percent-encoded then depended on stack garbage; valgrind reports the read as a conditional jump on an uninitialised value. Initialize the whole table. Backport of 56149186d81469fd7fb2f8444359ee81dd4a70f4 from PHP-8.4. --- ext/filter/sanitizing_filters.c | 2 +- ext/filter/tests/filter_sanitize_encoded_0xff.phpt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 ext/filter/tests/filter_sanitize_encoded_0xff.phpt diff --git a/ext/filter/sanitizing_filters.c b/ext/filter/sanitizing_filters.c index eac384172a62..585277c26642 100644 --- a/ext/filter/sanitizing_filters.c +++ b/ext/filter/sanitizing_filters.c @@ -66,7 +66,7 @@ static void php_filter_encode_url(zval *value, const unsigned char* chars, const unsigned char *e = s + char_len; zend_string *str; - memset(tmp, 1, sizeof(tmp)-1); + memset(tmp, 1, sizeof(tmp)); while (s < e) { tmp[*s++] = '\0'; diff --git a/ext/filter/tests/filter_sanitize_encoded_0xff.phpt b/ext/filter/tests/filter_sanitize_encoded_0xff.phpt new file mode 100644 index 000000000000..1ee61aba7b70 --- /dev/null +++ b/ext/filter/tests/filter_sanitize_encoded_0xff.phpt @@ -0,0 +1,12 @@ +--TEST-- +FILTER_SANITIZE_ENCODED percent-encodes 0xFF +--EXTENSIONS-- +filter +--FILE-- + +--EXPECT-- +string(3) "%FF" +string(10) "%FE%FF%00A" From 547566f17a12b4900efb69a7cf8cd325d00613a2 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 12:05:47 -0400 Subject: [PATCH 12/25] [http] Fix out-of-bounds read on empty Location header An empty Location header allocates a single byte for the NUL terminator, so reading location[1] in the relative-redirect branch over-reads heap memory and could append a garbage-derived path to the redirect target instead of the correct host root. Use location_len instead of strlen, and skip the relative join when location_len is 0, so the second byte is never read. Closes GH-23467 --- ext/standard/http_fopen_wrapper.c | 4 +-- .../http/http_empty_location_redirect.phpt | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/http/http_empty_location_redirect.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index b946684f3710..8249fe195b57 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1091,7 +1091,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, char *new_path = NULL; - if (strlen(header_info.location) < 8 || + if (header_info.location_len < 8 || (strncasecmp(header_info.location, "http://", sizeof("http://")-1) && strncasecmp(header_info.location, "https://", sizeof("https://")-1) && strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && @@ -1099,7 +1099,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, { char *loc_path = NULL; if (*header_info.location != '/') { - if (*(header_info.location+1) != '\0' && resource->path) { + if (header_info.location_len > 0 && resource->path) { char *s = strrchr(ZSTR_VAL(resource->path), '/'); if (!s) { s = ZSTR_VAL(resource->path); diff --git a/ext/standard/tests/http/http_empty_location_redirect.phpt b/ext/standard/tests/http/http_empty_location_redirect.phpt new file mode 100644 index 000000000000..a7f99bf1e249 --- /dev/null +++ b/ext/standard/tests/http/http_empty_location_redirect.phpt @@ -0,0 +1,36 @@ +--TEST-- +Empty Location header must not over-read when building the redirect target +--FILE-- + ['follow_location' => 1]]); +echo @file_get_contents("http://{{ ADDR }}/a/b", false, $ctx), "\n"; +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +uri=/ From 64664758cd8ee5928acf25b3f9ae11c7571c1b39 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Mon, 31 Aug 2026 19:16:00 +0200 Subject: [PATCH 13/25] ext/standard: Fix 1-char relative Location redirects after GH-23467 8196275133e changed the relative-Location check from location_len > 1 to > 0, so a single-character Location began resolving against the request path instead of the host root as before. Closes GH-23521 --- ext/standard/http_fopen_wrapper.c | 2 +- .../http_single_char_location_redirect.phpt | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/http/http_single_char_location_redirect.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 8249fe195b57..6342ff8a2387 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1099,7 +1099,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, { char *loc_path = NULL; if (*header_info.location != '/') { - if (header_info.location_len > 0 && resource->path) { + if (header_info.location_len > 1 && resource->path) { char *s = strrchr(ZSTR_VAL(resource->path), '/'); if (!s) { s = ZSTR_VAL(resource->path); diff --git a/ext/standard/tests/http/http_single_char_location_redirect.phpt b/ext/standard/tests/http/http_single_char_location_redirect.phpt new file mode 100644 index 000000000000..66af7a432537 --- /dev/null +++ b/ext/standard/tests/http/http_single_char_location_redirect.phpt @@ -0,0 +1,39 @@ +--TEST-- +Single-char relative Location header keeps resolving against the host root (pre-GH-23467 behavior) +--DESCRIPTION-- +Not RFC 3986 compliant ("x" against "/a/b" gives "/a/x"), but matches +PHP's long-standing behavior of resolving against the host root. See GH-23521. +--FILE-- + ['follow_location' => 1]]); +echo @file_get_contents("http://{{ ADDR }}/a/b", false, $ctx), "\n"; +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +uri=/x From a14181c944bee82a982e6bbcf36e61015b4294db Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 18 Sep 2026 17:07:21 +0200 Subject: [PATCH 14/25] Add NEWS entries --- NEWS | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/NEWS b/NEWS index a22154027d39..91bd9e2da7c4 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,48 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.2.34 +- Filter: + . Fixed GHSA-ch8v-r6jh-4vvr (FILTER_SANITIZE_ENCODED does not encode 0xFF). + (Ilia Alshanetsky) + +- FPM: + . Fixed GHSA-62xp-839h-2637 (IPv6 ACL bypass in FastCGI listen.allowed_clients + due to partial address comparison). (CVE-2026-91768) (Alexandre Daubois) + +- MySQLnd: + . Fixed GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd wire + protocol). (CVE-2025-1218) (Jakub Zelenka, Nora Dossche) + +- OpenSSL: + . Fixed GHSA-vvx9-73fr-5jjx (TLS hostname verification falls back to CN after + SAN mismatch). (CVE-2026-91769) (Jakub Zelenka) + . Fixed GHSA-xr7j-rvgx-xq5p (Heap buffer overflow in + php_openssl_matches_wildcard_name() on crafted server certificate wildcard + CN). (CVE-2026-91767) (Jakub Zelenka) + +- Phar: + . Fixed GHSA-j3wh-g957-2m85 (Integer overflow in phar_tar_number() allowing + TAR archive entry injection). (CVE-2026-6103) (Jakub Zelenka) + +- SOAP: + . Fixed GHSA-rgrp-mwpx-f6rm (Unbounded recursion in server-side + cleanup_xml_node()). (CVE-2026-91765) (Alexandre Daubois) + . Fixed GHSA-cj93-vc83-wgqv (Integer overflow to buffer overflow in SOAP HTTP + parsing). (CVE-2025-14181) (Nora Dossche, Jakub Zelenka) + +- Standard: + . Fixed GHSA-88hq-2827-7pg6 (Out-of-bounds read in convert.* stream filters + when line-break-chars contains NUL). (CVE-2026-92842) (geeknik) + . Fixed GHSA-fpwc-w8rq-cr92 (Cross-origin credential leak in HTTP stream + wrapper redirects). (CVE-2026-91766) (Alexandre Daubois, Jakub Zelenka) + . Fixed GHSA-7875-c8px-7q5f (Out-of-bounds read in the HTTP stream wrapper + when following a redirect with an empty Location header). (CVE-2026-93682) + (Ilia Alshanetsky, Jordi Kroon) + +- Windows: + . Fixed GHSA-9f67-6fw4-hpfp (Reserved device names are not rejected before + file and stream I/O). (CVE-2026-17545) (Shivam Mathur, Jakub Zelenka) + 30 Jul 2026, PHP 8.2.33 From 3aedde06bc329f139de811dc66244ec26c96820c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 18 Sep 2026 23:17:15 +0200 Subject: [PATCH 15/25] ext/soap: make GHSA-cj93-vc83-wgqv test lean and reliable The regression test for GHSA-cj93-vc83-wgqv sends a 2 GiB chunked body so that the client-side 32-bit length arithmetic overflows. Building that body as a PHP string and wrapping it in a data:// stream materialised the payload several times (str_repeat, concatenation, and the data:// copy), and those buffers lingered in the client process after the fork. Total footprint reached ~8 GiB and the run took ~16s locally, far more under slower/opcache CI jobs where it swapped and hit the run-tests timeout. Replace the generic http_server() helper with a minimal server that streams the 2 GiB filler in 8 MiB blocks. The bytes the client sees are identical, so the code path under test is unchanged, but the sender's memory stays small: only the client holds the large buffers (~4 GiB). Run time drops to ~4s and peak memory roughly halves. Also raise the free-RAM skip threshold from 4G to 6G to match the ~4 GiB the client actually allocates, so the test skips rather than swaps on memory-constrained machines. --- ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt | 103 ++++++++++++++----- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt index 8813d956ec5a..c96b67bb77fa 100644 --- a/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt +++ b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt @@ -19,12 +19,15 @@ if (!file_exists('/proc/meminfo')) { die('skip Cannot check free RAM from /proc/meminfo on this platform'); } +/* The client allocates a ~4 GiB response buffer (a 2 GiB chunk plus the + * oversized realloc for the second chunk), so require real headroom to avoid + * swapping, which is what makes this test time out on constrained machines. */ $free_ram = 0; if ($f = fopen("/proc/meminfo","r")) { while (!feof($f)) { if (preg_match('/MemFree[^\d]*(\d+)/i', fgets($f), $m)) { $free_ram = max($free_ram, $m[1]/1024/1024); - if ($free_ram > 4) { + if ($free_ram > 6) { $enough_free_ram = true; } } @@ -32,7 +35,7 @@ if ($f = fopen("/proc/meminfo","r")) { } if (empty($enough_free_ram)) { - die(sprintf("skip need +4G free RAM, but only %01.2f available", $free_ram)); + die(sprintf("skip need +6G free RAM, but only %01.2f available", $free_ram)); } --FILE-- -text0text1text2text3text4text5text6text7text8text9 -EOF; - -$responses = [ - "data://text/plain,HTTP/1.1 200 OK\r\n". +$headers = + "HTTP/1.1 200 OK\r\n". "Content-Type: text/xml;charset=utf-8\r\n". "Transfer-Encoding: \t chunked\t \r\n". "Connection: close\r\n". - "\r\n". - chunk_body($wsdl, 64), - "data://text/plain,HTTP/1.1 200 OK\r\n". - "Content-Type: text/xml;charset=utf-8\r\n". - "Transfer-Encoding: \t chunked\t \r\n". - "Connection: close\r\n". - "\r\n". - /* The second chunk only needs its size header: the reallocation for it - * happens before its body is read, so the overflow triggers on the first - * read into the undersized buffer. */ - sprintf("%08x\r\n", 0x7fffffff).str_repeat('x', 0x7fffffff)."\r\n" . - sprintf("%08x\r\n", 0x7fffffff)."xxxx", -]; + "\r\n"; + +/* Custom minimal server. Unlike the generic http_server() helper it streams the + * 2 GiB filler in bounded blocks instead of materialising it (and a data:// + * copy of it) in memory. That keeps the sender's footprint tiny: only the + * client needs to hold the large buffers, so total memory and run time stay far + * lower and the test no longer thrashes on slower machines. */ +function heavy_soap_server($wsdl, $headers) +{ + $server = stream_socket_server('tcp://localhost:0', $errno, $errstr); + if (!$server) { + return false; + } + $uri = 'http://' . stream_socket_get_name($server, false); + + $pid = pcntl_fork(); + if ($pid == -1) { + die('could not fork'); + } else if ($pid) { + return ['pid' => $pid, 'uri' => $uri]; + } + /* Child: streaming 2 GiB can exceed the 60s alarm the helper would use, so + * match the run-tests per-test timeout instead. */ + pcntl_alarm(120); + + $drain = static function ($sock) { + stream_set_blocking($sock, false); + while (!feof($sock)) { + $r = [$sock]; $w = $e = null; + if (!stream_select($r, $w, $e, 1)) continue; + $line = stream_get_line($sock, 8192, "\r\n"); + if ($line === '') break; + } + stream_set_blocking($sock, true); + }; + + /* Response 1: the WSDL, chunked. */ + $sock = stream_socket_accept($server, 60); + if ($sock) { + $drain($sock); + fwrite($sock, $headers . chunk_body($wsdl, 64)); + fclose($sock); + } + + /* Response 2: an oversized chunk. Only the size header of the second chunk + * is needed: the reallocation for it happens before its body is read, so on + * the unfixed code the overflow triggers on the first read into the + * undersized buffer. The 2 GiB first-chunk body is streamed in 8 MiB blocks + * rather than built as one string. */ + $sock = stream_socket_accept($server, 60); + if ($sock) { + $drain($sock); + fwrite($sock, $headers); + fwrite($sock, sprintf("%08x\r\n", 0x7fffffff)); + + $remaining = 0x7fffffff; + $block = str_repeat('x', 1 << 23); // 8 MiB + $block_len = strlen($block); + while ($remaining > 0) { + $n = $remaining < $block_len ? $remaining : $block_len; + fwrite($sock, $n === $block_len ? $block : substr($block, 0, $n)); + $remaining -= $n; + } + + fwrite($sock, "\r\n"); + fwrite($sock, sprintf("%08x\r\n", 0x7fffffff)); + fwrite($sock, "xxxx"); + fclose($sock); + } + + exit(0); +} -['pid' => $pid, 'uri' => $uri] = http_server($responses); +['pid' => $pid, 'uri' => $uri] = heavy_soap_server($wsdl, $headers); $options = [ 'trace' => false, From a1a7bd4b8b1b0eaccac025ce3fa7741ecae6c252 Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Tue, 22 Sep 2026 12:59:17 +0545 Subject: [PATCH 16/25] Fix GH-11662: Skip multicast test when unavailable The test assumes that IPv4 multicast is available. In an isolated Linux network namespace, joining the group fails before the test starts. Probe that capability in SKIPIF and skip the test when it is unavailable. Close GH-23838 --- ext/sockets/tests/bug63000.phpt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ext/sockets/tests/bug63000.phpt b/ext/sockets/tests/bug63000.phpt index 04265638a260..99723d68cdca 100644 --- a/ext/sockets/tests/bug63000.phpt +++ b/ext/sockets/tests/bug63000.phpt @@ -2,6 +2,23 @@ Bug #63000: Multicast on OSX --EXTENSIONS-- sockets +--SKIPIF-- + '224.0.0.251', + 'interface' => 0, +]); +if ($so === false) { + $errno = socket_last_error($socket); + if (in_array($errno, [SOCKET_ENODEV, SOCKET_ENXIO, SOCKET_EADDRNOTAVAIL], true)) { + die('skip no multicast-capable interface: ' . socket_strerror($errno)); + } +} +?> --FILE-- Date: Sun, 20 Sep 2026 09:08:18 +0200 Subject: [PATCH 17/25] Skip bz2 GH-20807 test when less than 13 GiB of memory is available The decompressed output of this test peaks at more than 12 GiB of RSS. On smaller machines, such as the 7 GB GitHub-hosted runners used for private repositories, this exhausts the whole VM and the OOM killer takes the test runner down with it, so the job dies with a runner shutdown signal instead of a test failure. Gate the test on MemAvailable from /proc/meminfo like the other resource-heavy tests gate on memory, so it keeps running on the 16 GB public runners and skips itself elsewhere. --- ext/bz2/tests/gh20807.phpt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/bz2/tests/gh20807.phpt b/ext/bz2/tests/gh20807.phpt index ee3238b711b9..ed52c7085858 100644 --- a/ext/bz2/tests/gh20807.phpt +++ b/ext/bz2/tests/gh20807.phpt @@ -12,6 +12,12 @@ if (PHP_OS === 'FreeBSD') die('skip Worker does not handle OOM gracefully'); if (PHP_OS_FAMILY === 'Darwin') die('skip Too slow'); if (PHP_INT_SIZE !== 8) die('skip Only for 64-bit systems'); if (getenv('SKIP_ASAN')) die('skip ASAN makes this test too slow'); +// The decompressed output needs more than 12 GiB of memory at its peak, which +// takes down smaller machines (e.g. 7 GB CI runners) with the OOM killer. +$memInfo = @file_get_contents('/proc/meminfo'); +if ($memInfo && preg_match('/MemAvailable:\s+(\d+) kB/', $memInfo, $m) && $m[1] < 13 * 1024 * 1024) { + die('skip Insufficient available memory (less than 13 GiB)'); +} ?> --FILE-- Date: Tue, 22 Sep 2026 13:06:25 +0200 Subject: [PATCH 18/25] Enable the TAILCALL VM when building with --disable-gcc-global-regs on GCC >= 16 (#23603) --- NEWS | 2 ++ Zend/zend_vm_gen.php | 2 +- Zend/zend_vm_opcodes.h | 2 +- ext/opcache/jit/zend_jit.c | 36 ++++++++++++++++++++---------------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/NEWS b/NEWS index c91c7ce877af..02da37e81086 100644 --- a/NEWS +++ b/NEWS @@ -114,6 +114,8 @@ PHP NEWS when $allow_string is false is now deprecated. (Daniel Scherzer) . Fixed bug GH-23232 (lone namespace separator asks the autoloader for an empty class name). (spawnia) + . Enabled the TAILCALL VM (--disable-gcc-global-regs) when building with GCC >= 16. + (henderkes) - CLI: . Fixed bug GH-23242 (PHP development server does not support Expect diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index e00aff17a924..674d1ed5b673 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -2522,7 +2522,7 @@ function gen_vm_opcodes_header( $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_HYBRID\n"; } if ($GLOBALS["vm_kind_name"][ZEND_VM_GEN_KIND] === "ZEND_VM_KIND_HYBRID" || $GLOBALS["vm_kind_name"][ZEND_VM_GEN_KIND] === "ZEND_VM_KIND_CALL") { - $str .= "#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__)) && defined(__clang__)\n"; + $str .= "#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__))\n"; $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_TAILCALL\n"; $str .= "#else\n"; $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_CALL\n"; diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h index 4e0d3ec43d9c..1d204b9281f5 100644 --- a/Zend/zend_vm_opcodes.h +++ b/Zend/zend_vm_opcodes.h @@ -41,7 +41,7 @@ static const char *const zend_vm_kind_name[] = { /* HYBRID requires support for computed GOTO and global register variables*/ #elif (defined(__GNUC__) && defined(HAVE_GCC_GLOBAL_REGS)) # define ZEND_VM_KIND ZEND_VM_KIND_HYBRID -#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__)) && defined(__clang__) +#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__)) # define ZEND_VM_KIND ZEND_VM_KIND_TAILCALL #else # define ZEND_VM_KIND ZEND_VM_KIND_CALL diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index e91da6aeb8d3..85109c7d0e03 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3100,23 +3100,10 @@ static int zend_real_jit_func(zend_op_array *op_array, zend_script *script, cons return FAILURE; } -/* Run-time JIT handler */ -#if ZEND_VM_KIND == ZEND_VM_KIND_CALL || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) -#else -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) -#endif +/* GCC cannot tail-call from a function that uses setjmp. */ +static zend_never_inline void zend_runtime_jit_compile(zend_op_array *op_array) { -#if GCC_GLOBAL_REGS - zend_execute_data *execute_data; - zend_op *opline; -#else - const zend_op *orig_opline = opline; -#endif - - execute_data = EG(current_execute_data); - zend_op_array *op_array = &EX(func)->op_array; - opline = op_array->opcodes; + const zend_op *opline = op_array->opcodes; zend_jit_op_array_extension *jit_extension; bool do_bailout = 0; @@ -3154,6 +3141,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(Z if (do_bailout) { zend_bailout(); } +} + +/* Run-time JIT handler */ +#if ZEND_VM_KIND == ZEND_VM_KIND_CALL || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) +#else +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) +#endif +{ +#if GCC_GLOBAL_REGS + zend_execute_data *execute_data; +#else + const zend_op *orig_opline = opline; +#endif + + execute_data = EG(current_execute_data); + zend_runtime_jit_compile(&EX(func)->op_array); /* JIT-ed code is going to be called by VM */ #if GCC_GLOBAL_REGS From 7f8a74fb29d4d2f99f7f9549b5bfd38ff68a8d52 Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Tue, 22 Sep 2026 13:36:46 +0200 Subject: [PATCH 19/25] master is now for PHP 8.7.0-dev --- NEWS | 1127 +------------------------------------- UPGRADING | 1093 +----------------------------------- UPGRADING.INTERNALS | 340 +----------- Zend/zend.h | 2 +- Zend/zend_extensions.h | 2 +- Zend/zend_modules.h | 2 +- configure.ac | 2 +- docs/release-process.md | 1 - main/php.h | 2 +- main/php_version.h | 6 +- win32/build/confutils.js | 4 +- 11 files changed, 13 insertions(+), 2568 deletions(-) diff --git a/NEWS b/NEWS index 02da37e81086..abfba60f153f 100644 --- a/NEWS +++ b/NEWS @@ -1,1130 +1,5 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.6.0RC1 - -- Core: - . Fixed incorrect internal pointer and foreach iterator positions when - compacting arrays with holes. (Weilin Du) - . Fix handling of references to typed properties during unserialization - of various internal classes. (ndossche, timwolla) - . Fixed OSS-Fuzz 532353396 (assertion failure with static type). (Girgias) - . Fix GH-23662 (Avoid NAN warning in print_r()). (CodedByManish) - . Fixed bug GH-23752 (Use scoped diagnostic suppression for the global - register declarations so the caller's -Wvolatile-register-var state is - restored). (yqtian-se) - . Fixed OSS-Fuzz #538730793 (Assertion failure when returning by-ref from - closure invoke). (ndossche) - . Fixed OSS-Fuzz #540904105 (ASSERT: ast->attr == T_CLASS_C). (ndossche) - -- CLI - . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during - request activation). (matyhtf) - -- Date: - . Fix unserialization of Time\Duration. (timwolla) - . Add comparison handler for Time\Duration. (timwolla) - -- DOM: - . Fixed use-after-free when re-constructing a DOMXPath whose php:function - registrations are freed while still reachable from the cycle collector. - (Ilia Alshanetsky) - . Fixed Dom\HTMLDocument::getElementById() not finding ids of SVG and - MathML elements. (Ilia Alshanetsky) - . Fixed Dom\HTMLDocument giving attributes the namespace of their element - when a fragment is parsed with an xlink, xml or xmlns context element. - (Ilia Alshanetsky) - -- Fileinfo: - . Upgrade to file 5.48. (Weilin Du) - -- Intl: - . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state - such as dateType, timeType, calendar and the message pattern. - (Ilia Alshanetsky) - . Fixed a crash when converting with a cloned UConverter that uses - toUCallback/fromUCallback. (Ilia Alshanetsky) - -- Lexbor: - . Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a - heap buffer overflow in :lexbor-contains() parsing and buffer overflows - in malformed decode replay. (alexandre-daubois) - -- MBString: - . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in - a truncated UTF-8 sequence). (Lazizbek Ergashev) - . Updated Unicode data tables to Unicode 18.0 (Yuya Hamada) - -- MySQLi: - . Fix GH-22854: Fixed failed assertion when accessing mysqli property after - failed reconnection. (Kamil Tekiela) - -- Opcache: - . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a - loop-invariant addition). (Ilia Alshanetsky) - . Fixed OSS-Fuzz #545352966 (default value AST of an SHM-persisted partial). - (ndossche) - -- PDO: - . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid - column index. (Ilia Alshanetsky) - . Fixed PDOStatement::bindColumn() registering a binding for a column name - that is not in the result set. (Ilia Alshanetsky) - -- PGSQL: - . Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky) - -- Posix: - . Reverted the validity check on the flags argument of posix_access(). - (David Carlier) - -- Readline: - . Fixed a heap over-read in the interactive shell prompt when cli.prompt is - set to an empty string. (Ilia Alshanetsky) - -- SPL: - . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when - __serialize() removes an element). (David Carlier) - -- SQLite: - . Fixed a crash when SQLite3::close() is called from a userland callback. - (Ilia Alshanetsky) - -- Standard: - . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false - properly). (Jakub Zelenka, Ilija Tovilo) - . Fixed three Windows-only proc_open() defects: an uninitialized - PROCESS_INFORMATION, an indeterminate comspec pointer after a failed - lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) - -- Zlib: - . Fixed inflate_init() dropping the preset dictionary for raw streams with - a non-default window. (Ilia Alshanetsky) - - -10 Sep 2026, PHP 8.6.0beta3 - -- BCMath: - . Fixed out-of-bounds read in bc_is_zero_for_scale() when scale exceeds - n_scale. (Ilia Alshanetsky) - -- Core: - . Fixed out-of-bounds reads during automatic UTF-16/32 encoding detection. - (Yudai Takada) - . Calling is_a() or is_subclass_of() with a string as the first argument - when $allow_string is false is now deprecated. (Daniel Scherzer) - . Fixed bug GH-23232 (lone namespace separator asks the autoloader for an - empty class name). (spawnia) - . Enabled the TAILCALL VM (--disable-gcc-global-regs) when building with GCC >= 16. - (henderkes) - -- CLI: - . Fixed bug GH-23242 (PHP development server does not support Expect - 100-continue flow control). (Sjoerd Langkemper) - -- Calendar: - . Fixed *tojd() functions and cal_to_jd() truncating arguments outside the - int range instead of rejecting them. (lacatoire) - -- DOM: - . Fixed NamedNodeMap::getNamedItemNS() with an empty URI not matching - the null namespace in spec-following mode. (Ilia Alshanetsky) - . Fixed stale getElementsByClassName() and other node list caches after - className/classList writes and attribute removals. (Ilia Alshanetsky) - . Fixed reference cycles through DOMXPath and XSLTProcessor php:function - callback arguments and return values not being collectable. - (Ilia Alshanetsky) - -- Hash: - . Fixed hash_file() reporting argument #1 ($algo) instead of argument #2 - ($filename) when the filename contains null bytes. (lacatoire) - -- Intl: - . Fixed grapheme_strpos() and grapheme_strrpos() with an empty needle - returning UTF-16 offsets instead of grapheme offsets. (Ilia Alshanetsky) - . Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky) - . Fixed Collator::sortWithSortKeys() allocating fixed 2MiB buffers - regardless of array size. (Ilia Alshanetsky) - . Fixed a memory leak when iterating IntlBreakIterator::getPartsIterator() - results. (iliaal) - . Fixed a leak in Locale::getKeywords() when a keyword value cannot be - read. (iliaal) - . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed - from compiled rules. (iliaal) - . Fixed Spoofchecker methods not recording the ICU error code when an ICU - call fails. (Ilia Alshanetsky) - . Fixed idn_to_ascii() and idn_to_utf8() reporting argument #2 ($flags) - instead of argument #3 ($variant) for an invalid IDNA variant, and the - domain length error message printing a literal "d" instead of the limit. - (lacatoire) - -- MBString: - . Fixed mb_ereg_replace() emitting a NUL or out-of-bounds bytes in the - replacement when a \k backref has no closing delimiter. - (Ilia Alshanetsky) - -- ODBC: - . Fixed odbc_field_len(), odbc_field_scale() and odbc_field_type() - returning uninitialized memory when SQLColAttribute fails. - (Ilia Alshanetsky) - . odbc_error() and odbc_errormsg() now also report SQLColAttribute - failures. (Ilia Alshanetsky) - -- PCNTL: - . Fixed the declared signature of pcntl_signal(), whose $restart_syscalls - argument accepts null and defaults to it. (lacatoire) - -- PDO_PGSQL: - . Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the - given number of rows. (KentarouTakeda) - -- PGSQL: - . Fixed the pg_insert(), pg_update() and pg_delete() flag error messages, - which did not name the set of flags actually accepted. (lacatoire) - -- Phar: - . Fixed bug GH-23418 (Use-after-free when looking up mounted directories). - (Weilin Du) - . Fixed bug GH-23477 (Memory leak on duplicate native Phar manifest entries). - (Weilin Du) - -- Sockets: - . Fixed socket_set_option() with SO_ATTACH_REUSEPORT_CBPF and a zero value, - which detached the classic BPF filter instead of the reuseport program. - (David Carlier) - -- SOAP: - . Fixed WSDL cache corruption when a soap:header defines headerfaults. - (Ilia Alshanetsky) - . Fixed stack overflow when parsing a WSDL with self-referential schema - groups or attributeGroups. (Ilia Alshanetsky) - -- Sodium: - . Added support for the libsodium 1.0.22 KEM APIs (X-Wing and ML-KEM768). - (Zachary DuBois) - -- Standard: - . Fixed a segfault when a stream filter callback unsets StreamBucket::$data - before re-attaching the bucket. (iliaal) - . Fixed an out-of-bounds read when following a redirect response with an - empty Location header. (iliaal) - . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm) - . Io\Poll\Context::wait() now rejects a $maxEvents value greater than - INT_MAX instead of truncating it. (marc-mabe) - . Fixed GH-23338 (fsockopen()/pfsockopen() ValueError reported wrong - argument number for $timeout). (lacatoire) - . Fixed bug GH-23576 (Next index for array returned from array_keys() is - wrong). (Lazizbek Ergashev) - -- SimpleXML: - . Fixed writing to a dimension of the object returned by attributes() not - creating the attribute. (Ilia Alshanetsky) - . Fixed child elements of the element returned by - SimpleXMLElement::addChild() not being accessible by property name when - namespaces are involved. (Ilia Alshanetsky) - -- Streams: - . Added so_rcvbuf and so_sndbuf stream socket context options, setting the - socket receive and send buffer sizes in bytes. (David Carlier) - - -27 Aug 2026, PHP 8.6.0beta2 - -- Core: - . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or - next() call on the inner generator). (iliaal) - . Fixed GH-10497 (Allow direct mutation of objects stored in constants or - class constants via OBJ->prop = $val). (Khaled Alam) - . Reverted GH-22833, which attempted to fix bug GH-18985. (ilutov) - . Using the return statement in a finally block is now deprecated. - (aldemeery) - -- Curl: - . Set content length using CURLOPT_POSTFIELDSIZE_LARGE instead of - CURLOPT_POSTFIELDSIZE. This makes it possible to post strings larger than - 2GB on some platforms, e.g. Windows. (Sjoerd Langkemper) - -- DOM: - . Fixed a typo in the DOMException message for INUSE_ATTRIBUTE_ERR. - (Weilin Du) - . Fixed bug GH-22624 (use-after-free via DOMNameSpaceNode after - DOMDocument::xinclude()). (David Carlier) - . Fixed a use-after-free when cloning a DOMNameSpaceNode after - DOMDocument::xinclude(). (iliaal) - . Fixed a crash in DOMXPath when a php:function callback receives a nodeset - and a later callback returns a node from another document. (iliaal) - . Fixed bug GH-23331 (UAF when node_list_unlink() skips attribute children - that still have a live wrapper). (iliaal) - . Fixed a use-after-free when Dom\Element::setAttributeNS() replaces the - value of an attribute whose child still has a live wrapper. (iliaal) - -- GD: - . Fixed imageaffinematrixget() and imageaffinematrixconcat() reporting the - wrong argument in error messages. (Weilin Du) - . Fixed imageaffinematrixget() to enforce the documented array|float type - for the $options parameter. (Weilin Du) - -- Intl: - . Fixed grapheme_strrev() treating UBRK_DONE as a byte index and leaving - the result without a terminating NUL. (iliaal) - . Fixed a double-free when IntlGregorianCalendar construction fails after - the ICU constructor adopts the TimeZone. (iliaal) - . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions - for UTF-8 strings). (ColumbusLabs) - . Fixed Locale::parseLocale() reading past a trailing '-' or '_'. - (iliaal, Xuyang Zhang) - . Fixed grapheme_str_split() treating UBRK_DONE as a byte index. (iliaal) - -- Opcache: - . Fixed a tracing JIT crash when compiling a side trace for a method of a - class that could not be stored in the inheritance cache. (GH-21710) - (Arnaud, iliaal) - -- PDO: - . Fixed a leak when a persistent connection failed a liveness check - with no other live PDO handle. (iliaal) - -- PDO_PGSQL: - . Fixed several lazy fetch (PDO::ATTR_PREFETCH => 0) defects: an infinite - loop when cleaning up a fetch left in a COPY, a use-after-free when a - statement with emulated or disabled prepares is destroyed, a connection - left busy for the next fetch, and rows delivered from a result another - statement took over. (KentarouTakeda) - -- PGSQL: - . Fixed the class name casing of pg_close_stmt()'s connection parameter. - (lacatoire) - -- Phar: - . Fixed Phar archives being automatically detected when ".phar" only occurs - in a directory name or is not a filename extension in an included file's - path. (Weilin Du) - -- Readline: - . Fixed class constant completion in the interactive shell. (Weilin Du) - -- Zip: - . Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be - garbage collected). (Weilin Du, ndossche) - -- SAPI: - . Fixed fuzzer targets failing to build in isolation. (Mrmaxmeier) - . Fixed returns uninitialized value on LiteSpeed lsapi SAPI (Go Kudo) - -- Session: - . Fixed bug GH-23056 (missing handler name in session write warning). - (lazerg) - . SessionHandler::validateId() is now implemented, so - session.use_strict_mode applies to the built-in handler. (Girgias) - . It is now deprecated to pass an object that does not implement the - create_sid() and validateId() methods. (Girgias) - . A deprecation is now emitted when implementing SessionHandlerInterface on a - class which doesn't define the create_sid() or validateId() methods, as - those will be moved from SessionUpdateTimestampHandlerInterface and - SessionIdInterface to SessionHandlerInterface. (Girgias) - -- SNMP: - . It is now possible to use the AES192, AES192C, AES256, and AES256C as - SNMPv3 security protocols if the underlying library supports them. - (eskyuu) - . It is now possible to reset the MIB tree using the new snmp_init_mib() - function. (eskyuu) - . Additional MIB parsing and output control functionality has been exposed - via the snmp_set_mib_option(), snmp_set_output_option(), - snmp_set_string_output_format() functions, the $numeric_index, - $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, - $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), - SNMP::setStringOutputFormat() methods. (eskyuu) - -- Sodium: - . Fixed incorrect parameter name in sodium_add(), sodium_memcmp(), and - sodium_compare() length-mismatch error messages. (lacatoire) - -- SPL: - . The following SplFileObject methods have been deprecated - SplFileObject::fgetcsv(), SplFileObject::fputcsv(), - SplFileObject::setCsvControl(), and SplFileObject::getCsvControl(). - (Girgias) - -- Standard: - . Fixed incorrect parameter name in convert_uudecode() warning. (lacatoire) - . Added support for the "<" and ">" endianness modifiers in pack() and - unpack() format codes. (alexandre-daubois) - -- Zip: - . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive - is freed while the stream is still open). (Eyüp Can Akman) - -- Zlib: - . Fixed bug GH-22142 (Assertion failure in deflate_init() when an option - object has uninitialised typed properties). (David Carlier) - -13 Aug 2026, PHP 8.6.0beta1 - -- Core: - . Deprecated "namespace" as a class constant name. (NickSdot) - . Changed run-tests.php to run in parallel by default, using up to 10 - automatically detected workers. Pass -j1 for sequential execution. - (NickSdot) - . Allowed readonly properties to declare default values. (NickSdot) - . Changed run-tests.php to run test subprocesses without a shell where - possible. (NickSdot) - . Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with - -d error_include_args=On). (David Carlier) - . Fixed GH-23121 (is_callable() wrongly accepts objects with no get_closure - handler). (David Carlier) - . Passing a 3rd argument to define() is now deprecated. (Girgias) - . Naming a function readonly is now deprecated. (Girgias) - . Added stateless closure cache. (ilutov) - -- BZ2: - . Passing an object for the Bzip2 {de}compression stream filter is now - deprecated. Use get_object_vars() on the object instead. (Girgias) - -- Curl: - . Improved cURL option validation errors to include the option name. - (Sjoerd Langkemper) - . Raise a value error when the callback registered with CURLOPT_READFUNCTION - returns an unexpected long. (Sjoerd Langkemper) - . Fix bug GH-16513 (curl: exceptions in callbacks do not abort the request). - (Sjoerd Langkemper) - -- Date: - . Update timelib to 2026.02. (Derick, timwolla) - . Added Time\Duration. (timwolla, Derick) - -- DOM: - . Fixed bug GH-23116 (Stack overflow when normalizing a deeply nested - DOMDocument). (Lazizbek Ergashev) - . Fixed bug GH-23117 (Stack overflow when normalizing a deeply nested - Dom\XMLDocument). (Lazizbek Ergashev) - -- Exif: - . Fixed exif_read_data() allocating a HEIF meta box larger than the file - it came from. (iliaal) - -- GMP: - . Added optional $definitely_prime output parameter to gmp_prevprime(). - (Weilin Du) - . Added gmp_powm_sec(). (Weilin Du) - -- Intl: - . Added static methods IntlDatePatternGenerator::getSkeleton() and - IntlDatePatternGenerator::getBaseSkeleton(). (Weilin Du) - . Fixed Collator::sort(), collator_sort(), Collator::asort(), and - collator_asort() to report UTF-8/UTF-16 conversion errors through the intl - error handler instead of emitting a warning and continuing with an empty - string. (Weilin Du) - . Fixed IntlListFormatter::__construct() leaving stale global error state - after successful calls. (Weilin Du) - . Fixed IntlNumberRangeFormatter leaving stale global error state after - successful createFromSkeleton() and format() calls. (Weilin Du) - . Implemented GH-20255 (Add a predefined calendar constant in - IntlDateFormatter for the proleptic gregorian calendar). (David Carlier) - . Added SpoofChecker::areBidiConfusable(). (David Carlier) - . Added SpoofChecker::getBidiSkeleton(). (Weilin Du) - . Added SpoofChecker::getSkeleton(). (David Carlier) - . Fixed IntlNumberRangeFormatter::format() crash when the formatting fails. - (David Carlier) - -- MbString: - . Passing objects to mb_convert_variables() is now deprecated. (Girgias) - -- MySQLi: - . The mysqli_get_charset() function and mysqli::get_charset() method are now deprecated. (Kamil Tekiela) - . The mysqli_stmt_init() function and mysqli::stmt_init() method are now deprecated. (Kamil Tekiela) - . Instantiation of mysqli_stmt without providing the $query parameter is now deprecated. (Kamil Tekiela) - -- PDO: - . Fixed pdo_raise_impl_error() emitting a warning under ERRMODE_SILENT. - (iliaal) - -- PDO_ODBC: - . Fixed bug GH-23016 (NULL values in long columns come back as garbage - binary strings). (Calvin Buckley, iliaal) - -- Readline: - . Fixed the interactive shell not waiting for the pager process to exit. - (Weilin Du) - -- Reflection: - . Added ReflectionAttribute::inNamespace(), - ReflectionAttribute::getNamespaceName(), and - ReflectionAttribute::getShortName(). (Girgias) - . Fixed bug GH-22905 (Reflection exception messages truncate on null bytes). - (DanielEScherzer) - . Fixed ReflectionProperty::isLazy() and skipLazyInitialization() using the - parent slot when a child class hooks an inherited property. (iliaal) - . Fixed segfault in ReflectionMethod::createFromMethodName() on an - uninstantiable subclass. (iliaal) - -- SimpleXML: - . Fixed integer element offsets that cannot resolve aliasing an existing - element. (iliaal) - . SimpleXMLElement::__construct() now raises a ValueError when the $data - argument contains NUL bytes. (iliaal) - . Fixed segfault when comparing uninitialized SimpleXMLElement - instances. (iliaal) - -- SPL: - . The spl_classes() function is now deprecated, use - ReflectionExtension::getClassNames() instead. (Girgias) - . The spl_object_hash() function is now deprecated, use spl_object_id() - instead. (Girgias) - . The following ArrayIterator methods are now deprecated: - * ArrayIterator::getFlags() - * ArrayIterator::setFlags() - * ArrayIterator::asort() - * ArrayIterator::ksort() - * ArrayIterator::uasort() - * ArrayIterator::uksort() - * ArrayIterator::natsort() - * ArrayIterator::natcasesort() - * ArrayIterator::unserialize() - * ArrayIterator::serialize() - (Girgias) - -- Standard: - . Passing an object as the $data argument to http_build_query() is now - deprecated. The interpretation of object values within $data as arrays - is also deprecated. Convert objects to arrays with get_object_vars() - before calling the function. (Girgias) - . Added the "filter.max_filter_count" stream context option for php://filter - URLs. Using more than 16 filters without configuring this option is now - deprecated. (Sjoerd Langkemper) - . The metaphone() function is now deprecated, use a userland phonetic - matching library instead. (Weilin Du) - . Improved performance of array_intersect(). (mehmetcansahin) - . Fixed bug GH-23006 (phpcredits() full-page HTML title says phpinfo()). - (Weilin Du) - . The following functions now raise a ValueError when the $filename argument - contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(), - filegroup(), fileatime(), filemtime(), filectime(), filetype(), - is_writable(), is_readable(), is_executable(), is_file(), is_dir(), - is_link(), file_exists(), lstat(), stat(). (Girgias) - . Fixed bug GH-22818 (stream_filter_register() orphaned user_filter_map on - shutdown re-registration). (David Carlier) - . Io\Poll\Context::wait() now takes a Time\Duration object as a timeout. - (timwolla) - . Passing an object to array_walk{_recursive} is now deprecated. Use - get_object_vars() on the object instead. (Girgias) - . The is_double() function is now deprecated, use is_float() instead. - (Girgias) - . The is_long() and is_integer() functions are now deprecated, use is_int() - instead. (Girgias) - . The doubleval() function is now deprecated, use floatval() instead. - (Girgias) - . The strcoll() function is now deprecated, use Collator::compare() instead. - (Girgias) - . The SORT_LOCALE_STRING constant for the family of sort functions is now - deprecated, use one of the following functions instead: - * Collator::asort() - * Collator::sort() - * Collator::sortWithSortKeys() - (Girgias) - -- Streams: - . Fixed file_put_contents() LOCK_EX early return leaking stream error - operation depth. (iliaal) - -- XSL: - . Fixed use-after-free when a DOMDocument subclass __clone() retains the - stylesheet copy made by XSLTProcessor::importStylesheet(). (iliaal) - -- Zlib - . Passing an object for the zlib deflate and inflate stream filter is now - deprecated. Use get_object_vars() on the object instead. (Girgias) - . Passing an object to the $option argument to deflate_init and inflate_init - is now deprecated. Use get_object_vars() on the object instead. (Girgias) - -30 Jul 2026, PHP 8.6.0alpha3 - -- Core: - . Implemented partial function application RFC. (Arnaud) - . Fixed bug GH-22263 (reset typed property default on every unserialize - failure path). (David Carlier) - . Fixed bug GH-18985 (Wrong line numbers for match with constant arms). - (ilutov) - . Fixed bug GH-18847 (SEGV in zend_fetch_debug_backtrace() when the memory - limit is reached while the tracing JIT enters a call frame). (Arnaud, - iliaal) - -- DOM: - . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD - declares a default value for the attribute). (iliaal) - . Fixed bug GH-23120 (Stack overflow when comparing deeply nested DOM nodes - with DOMNode::isEqualNode()). (Weilin Du) - -- Embed: - . Made php-cli functionality available in embed builds. (henderkes) - -- GMP: - . Added gmp_prevprime(). (Weilin Du, David Carlier) - . Fixed GMP power and shift operators to reject GMP right operands outside - the unsigned long range instead of silently truncating them. (Weilin Du) - . Fixed GMP integer string parsing to reject strings containing NUL bytes - instead of silently truncating them. (Weilin Du) - . Fixed GMP error messages that referenced outdated parameter names. - (Weilin Du) - -- Intl: - . Fixed grammatical issues in Normalizer invalid form and IntlCalendar time - zone offset error messages. (Weilin Du) - . Removed the dependency on the ICU IO library. (Weilin Du) - -- ODBC: - . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the - driver-reported display size). (iliaal) - -- Opcache: - . Re-enable JIT for ZTS builds on Apple Silicon. (realFlowControl) - -- PDO_ODBC: - . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the - driver-reported display size). (iliaal) - . Fixed bug GH-22666 (Heap buffer overflow when an output parameter value is - longer than the declared maxlen). (iliaal) - . Fixed bug GH-22665 (Out-of-bounds write when the ODBC driver reports a - diagnostic message length beyond the error buffer). (iliaal) - -- Phar: - . Fixed grammatical issues and outdated terminology in Phar error messages. - (Weilin Du) - -- Reflection: - . Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes). - (DanielEScherzer) - -- SOAP: - . Fixed header injection through the Content-Type context option, the - soapaction and the cookie names and values. (David Carlier) - . Fixed the SoapClient and SoapServer "classmap" option to reject arrays - containing integer keys, and made SoapClient throw TypeError/ValueError - for invalid "classmap" options. (Weilin Du, David Carlier) - -- MBString: - . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative - offset in a non-UTF-8 encoding). (Eyüp Can Akman) - . Fixed bug GH-21036 (mb_ereg_search_getregs() crashes after mb_eregi() - invalidates the regex cache). (Matthias Goergens) - -- PCRE: - . Fixed bug GH-21134 (Crash with \C + UTF-8). Using \C in UTF-8 patterns is - now forbidden. (Arnaud) - -- Sockets: - . Fixed socket_set_option() validation error messages for UDP_SEGMENT and - TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du) - . Fixed various memory related issues in ext/sockets. (David Carlier) - -- Standard: - . Fixed setlocale() to reject locale names containing NUL bytes instead of - silently truncating them, and to reject arrays passed after the $locales - argument or additional arguments passed after an array $locales argument. - (Weilin Du) - -- Streams: - . Added a new IO copy API used by php_stream_copy_to_stream_ex() that - leverages platform primitives (sendfile, splice, copy_file_range, - TransmitFile) for faster stream copying. (Jakub Zelenka, David Carlier) - . Fixed bug GH-22841 (php_stream_copy_to_stream_ex() drops progress - notifications when using the copy fast path). (David Carlier) - . Fixed bug GH-15836 (Use-after-free when a user stream filter accesses - $this->stream during the close flush). (iliaal) - -16 Jul 2026, PHP 8.6.0alpha2 - -- Core: - . Sync Boost.Context assembly with 1.91.0. (kn1g78) - . Fixed bug GH-22387 (AST pretty-printing drops meaningful parentheses around - RHS of instanceof). (timwolla) - . Fixed bug GH-15672 and GH-15911 (Stack overflow when an internal function - recurses through zend_call_function, such as a self-attached SPL - iterator). (iliaal) - . Lock unmodified readonly properties for modification after clone-with. - (NickSdot) - . abort() instead of exit() on hard OOM. (realFlowControl) - . perf: ZTS: move AG and SCNG into native __thread storage. (henderkes) - -- Calendar: - . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with - INT_MAX year). (arshidkv12) - -- Curl: - . Added CURLOPT_SEEKFUNCTION and the CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL - and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed - request body to resend it on a redirect, multi-pass authentication or a - retried reused connection. (GrahamCampbell) - -- Date: - . Update timelib to 2022.17. (Derick) - . Fixed bug GH-19803 (Parsing a string with a single white space does create - an error). (Derick) - . Fixed Unix timestamps in February of the year 0 are misparsed with - @-notation. (LukasGelbmann) - . Fixed bug GH-11368 (idate() doesn't work for the year -1). (Derick) - . Fixed bug GH-11310 (__debugInfo does nothing on userland classes extending - Date classes). (Derick) - -- DBA: - . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz) - -- DOM: - . Fixed bug GH-22570 (Stack overflow when serializing a deeply nested - Dom\XMLDocument). (iliaal) - . Fixed Dom\DtdNamedNodeMap integer dimension access so negative indexes - return NULL and indexes outside the int range throw ValueError instead of - returning the first entity or notation. (Weilin Du) - . Fixed bug GH-22623 (use after free with namespace nodes from - XSLTProcessor::registerFunctions())/ (David Carlier) - . Fixed bug GH-22554 (use-after-free with XPath callback returning a node - from a foreign document). (David Carlier) - -- Exif: - . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size" - warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) - -- GMP: - . Fixed bug GH-22549 (Assertion failure / UB on a compound GMP power or shift - assignment with a negative exponent). (iliaal) - -- Intl: - . Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to - reject offset values outside the 32-bit range instead of silently - truncating them. (Weilin Du) - . IntlDateFormatter::parse()/datefmt_parse() and - IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError - when the offset argument is not of type int. (Weilin Du) - -- JSON: - . Report unterminated JSON strings as syntax errors. (timwolla) - . Improve performance error position tracking during JSON decoding. - (henderkes) - . Fixed bug GH-22514 (Incorrect error column in PHP 8.6 JSON parser). - (henderkes, timwolla) - -- Opcache: - . Fixed bug GH-21770 (Infinite recursion in property hook getter in opcache - preloaded trait). (iliaal) - -- OpenSSL: - . Added $salt_length parameter to openssl_sign() and openssl_verify() with - new OPENSSL_RSA_PSS_SALTLEN_* constants. (Jakub Zelenka) - . Fixed timeout for supplemental read at end of a blocking stream in SSL - stream wrapper. (ilutov) - . Fixed stream_socket_get_crypto_status() after supplemental read. (ilutov) - -- PDO_ODBC: - . Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN - carries no credentials). (iliaal) - -- PHPDBG: - . Fixed fleaked lowercased lookup keys in phpdbg_resolve_opline_break. - (jorgsowa) - . Fixed off-by-one in phpdbg_safe_class_lookup() causing class lookups to - always fail during phpdbg's signal-safe interruption path. (jorgsowa) - -- Reflection: - . Fixed bug GH-22683 (Reflection(Class)Constant::__toString() should not warn - on NAN conversions). (Khaled Alam) - . Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes). - (DanielEScherzer) - -- Session: - . Fixed bug GH-21314 (Different session garbage collector behavior between - PHP 8.3 and PHP 8.5). (jorgsowa) - -- SOAP: - . Fixed bug GH-22585 (OOM on bailout with uninitialized - do_request() parameters). (David Carlier) - . Fixed xsd:hexBinary decoding to reject odd-length values instead of - silently truncating the last nibble. (Weilin Du) - . Made SOAP encoding errors report the affected type or failing operation - instead of the generic "Violation of encoding rules" message. (Weilin Du) - -- Standard: - . Fixed sleep() and usleep() to reject values that overflow the underlying - unsigned int timeout. (Weilin Du) - . Fixed bug GH-22671 (assert.bail aborts the process when the assert callback - throws an exception whose reporting re-throws). (iliaal) - . Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator - mutates the array being sorted). (azchin, iliaal) - -- Streams: - . Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL - and a proxy set). (CVE-2026-12184) (ndossche) - -- URI: - . Fixed bug GH-22628 (Percent-encoding of caret in WHATWG URL paths is not - performed). (kocsismate) - . Fixed bug GH-22629 (WHATWG Validation error incorrect with empty host and - non-empty userinfo). (kocsismate) - -- Zip: - . Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex() - could crash after overwriting an entry and resetting its inherited - unchanged comment). (Weilin Du) - . Fixed bug GH-21705 (ZipArchive::getFromIndex() ignores - ZipArchive::FL_UNCHANGED for deleted entries). (Weilin Du) - . Fixed bug GH-22176 (memory leak with ZipArchive::registerCancelBack() - is used with reference returning function during shutdown). - (David Carlier) - . ZipArchive::addGlob() and ZipArchive::addPattern() now raise a TypeError - for invalid "remove_all_path", "comp_method", "comp_flags", and - "enc_method" options instead of emitting a warning. (David Carlier) - -02 Jul 2026, PHP 8.6.0alpha1 - -- Core: - . Added first-class callable cache to share instances for the duration of the - request. (ilutov) - . It is now possible to use reference assign on WeakMap without the key - needing to be present beforehand. (ndossche) - . Added `clamp()`. (kylekatarnls, thinkverse) - . Fix OSS-Fuzz #429429090 (Failed assertion on unset() with uninitialized - container). (ilutov) - . Fixed GH-20564 (Don't call autoloaders with pending exception). (ilutov) - . Fix deprecation not showing when accessing null key of an array with JIT. - (alexandre-daubois) - . Fixed bug GH-20174 (Assertion failure in - ReflectionProperty::skipLazyInitialization after failed LazyProxy - initialization). (Arnaud) - . Enabled the TAILCALL VM on Windows when compiling with Clang >= 19 x86_64. - (henderkes) - . Deprecate specifying a nullable return type for __debugInfo(). (timwolla) - . Fixed bug GH-22142 (Assertion failure in zendi_try_get_long() on IS_UNDEF). - (David Carlier) - . Fixed bug GH-22046 (The unserialize function can lead to segfault when - non-Serializable internal classes are serialized back with the C format). - (kocsismate) - . Fixed bug GH-22292 (AST pretty printing does not correctly handle invalid - variable names). (timwolla) - . Fixed bug GH-22291 (AST pretty printing does not correctly handle braces in - string interpolation). (timwolla) - . Fixed bug GH-22373 (AST pretty-printing drops meaningful parentheses - surrounding property access). (timwolla) - . Fixed GH-22422 (zend_arena layout mismatch leaked memory in separately - built extensions under AddressSanitizer). (iliaal) - . TSRM: use local-exec TLS in PIE executables. (henderkes) - . perf: make all static extensions use TSRMG_STATIC. (henderkes) - . Fixed bug GH-22257 (type confusion in Exception::getTraceAsString()). - (David Carlier) - . TSRM: make CG, EG, SCNG and AG compile-time offsets. (henderkes) - . Deprecate returning values from __construct() and __destruct(). (timwolla) - . base_convert, bindex, hexdec and octdec now raise a notice when they cannot - precisely convert the given number. (Sjoerd Langkemper) - . Added error_include_args INI option to make the display of function - arguments consistent in error output. (Calvin Buckley) - -- BCMath: - . Added NUL-byte validation to BCMath functions. (jorgsowa) - -- BZ2: - . Reject oversized input in bzdecompress(). (arshidkv12) - -- Curl: - . Add support for CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). (Ayesh) - -- Date: - . Update timelib to 2022.16. (Derick) - -- DOM: - . Removed LIBXML_XINCLUDE from valid options for XMLDocument, as it was a - no-op. (ndossche) - . Readonly DOM properties are now declared with asymmetric visibility - (public private(set)). ReflectionProperty::isWritable() reports them - correctly, and external writes raise "Cannot modify private(set) - property" instead of the previous readonly modification error. - (David Carlier) - . Fixed Dom\Notation nodes missing tree connection, so that ownerDocument, - parentNode, isConnected and baseURI now return correct values, and - textContent returns NULL per the DOM specification. (jordikroon) - -- EXIF: - . Added support for reading EXIF metadata from WebP images (GH-19904). - (iliaal) - -- Fileinfo: - . Fixed bug GH-20679 (finfo_file() doesn't work on remote resources). - (ndossche) - . Fixed bug #66095 (Hide libmagic dynamic symbols). (orlitzky) - -- GD: - . imagesetstyle()/imagefilter()/imagecrop() check array argument entries - types. (David Carlier) - -- GMP: - . gmp_fact() reject values larger than unsigned long. (David Carlier) - . gmp_pow/binomial/root/rootrem and shift/pow operators reject values larger - than unsigned long. (David Carlier) - . GMP exponentiation and shift operators now emit a deprecation warning - when converting a float right operand to int loses precision. (Weilin Du) - -- Hash: - . Upgrade xxHash to 0.8.2. (timwolla) - -- Intl: - . Fixed malformed ResourceBundle::get() error message when fallback is - disabled. (Weilin Du) - . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(), - with the alias of locale_get_display_keyword() and - locale_get_display_keyword_value() respectively. (Weilin Du) - . Fix incorrect argument positions for invalid start/end arguments in - transliterator_transliterate(). (Weilin Du) - . Fixed IntlTimeZone::getDisplayName() to synchronize object error state - for invalid display types. (Weilin Du) - . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the - fallback locale when a language tag cannot be canonicalized. (Weilin Du) - . Added IntlNumberRangeFormatter class to format an interval of two numbers - with a given skeleton, locale, collapse type and identity fallback. - (BogdanUngureanu) - . Fixed bug GH-20426 (Spoofchecker::setRestrictionLevel() error message - suggests missing constants). (DanielEScherzer) - . Added grapheme_strrev (Yuya Hamada) - . Passing a non-stringable object as a time zone to Intl time zone - argument handling now raises TypeError instead of Error. (Weilin Du) - . IntlBreakIterator::getLocale() now raises ValueError for invalid locale - types. (Weilin Du) - . Fixed MessageFormatter::parse() and parseMessage() returning PHP_INT_MIN - as float rather than int on 64-bit platforms. (Weilin Du) - . Fixed UConverter::transcode() silently truncating from_subst and to_subst - option lengths greater than 127 bytes. (Weilin Du) - . Fixed IntlIterator::current() to return NULL instead of an undefined value - when the iterator is not positioned on a valid element. (Weilin Du) - -- IO: - . Added new polling API. (Jakub Zelenka) - -- JSON: - . Enriched JSON last error / exception message with error location. - (Juan Morales) - -- Fibers: - . Fixed bug GH-20483 (ASAN stack overflow with fiber.stack_size INI small - value). (David Carlier) - -- Mail: - . Fixed bug GH-20862 (null pointer dereference in - php_mail_detect_multiple_crlf via error_log (jordikroon) - -- Mbstring: - . ini_set() with mbstring.detect_order changes the order of mb_detect_order - as intended, since mbstring.detect_order is an INI_ALL setting. (tobee94) - . Added GB18030-2022 to default encoding list for zh-CN. (HeRaNO) - . Fixed bug GH-20836 (Stack overflow in mb_convert_variables with - recursive array references). (alexandre-daubois) - . Fixed bug GH-21223; mb_guess_encoding no longer crashes when passed huge - list of candidate encodings (with 200,000+ entries). (Jordi Kroon) - . mbregex has been deprecated. (youkidearitai) - -- Mysqli: - . Added mysqli_quote_string() and mysqli::quote_string(). (Kamil Tekiela) - -- Opcache: - . Fixed bug GH-20051 (apache2 shutdowns when restart is requested during - preloading). (Arnaud, welcomycozyhom) - -- OpenSSL: - . Added AES-SIV support. (jordikroon) - . Implemented GH-20310 (No critical extension indication in - openssl_x509_parse() output). (StephenWall) - . Added TLS session resumption support for streams with new context options - and Openssl\Session class. (Jakub Zelenka) - . Added TLS external PSK support for streams with new context options and - Openssl\Psk class. (Jakub Zelenka) - . Added TLS 1.3 early data (0-RTT) support for streams with new context - options early_data, max_early_data and early_data_cb. (Jakub Zelenka) - . Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE. - (Jakub Zelenka) - -- PCNTL: - . pcntl_exec() now throws a ValueError if the $args array is not a list - array. (Weilin Du) - -- PDO_DBLIB: - . Added dblib_handle_check_liveness handler. (freddy77) - -- PDO_PGSQL: - . Clear session-local state disconnect-equivalent processing. - (KentarouTakeda) - -- PGSQL: - . Enabled 64 bits support for pg_lo_truncate()/pg_lo_tell() if the server - supports it. (KentarouTakeda) - . pg_fetch_object() now surfaces non-instantiable class errors before - fetching, resolves the constructor via the get_constructor handler, and - reports the empty-constructor ValueError on the $constructor_args argument. - (David Carlier) - -- Phar: - . Support reference values in Phar::mungServer(). (ndossche) - . Invalid values now throw in Phar::mungServer() instead of being silently - ignored. (ndossche) - . Fixed a bypass of the magic ".phar" directory protection in - Phar::addEmptyDir() for paths starting with "/.phar". (Weilin Du) - . Fixed an integer underflow when parsing ZIP extra fields. (Weilin Du) - . Phar::addEmptyDir() now allows non-magic directory names that merely - share the ".phar" prefix. (Weilin Du) - . Support overridden methods in SplFileInfo for getMTime() and getPathname() - when building a phar. (ndossche) - . Mark Phar::buildFromIterator() base directory argument as a path. - (ndossche) - -- phpdbg: - . Fixed GH-22480 (Use-after-free when re-watching an already-watched - variable). (iliaal) - -- Posix: - . Added validity check to the flags argument for posix_access(). (arshidkv12) - -- Reflection: - . Fixed bug GH-20217 (ReflectionClass::isIterable() incorrectly returns true - for classes with property hooks). (alexandre-daubois) - . Added ReflectionConstant::inNamespace(). (Khaled Alam) - . Added ReflectionProperty::isReadable() and ::isWritable(). (ilutov) - . Fixed bug GH-21362 (ReflectionMethod::invoke/invokeArgs() did not verify - Closure instance identity for Closure::__invoke()). (Ilia Alshanetsky) - . Added ReflectionParameter::getDocComment(). (chschneider) - -- Session: - . Fixed bug 71162 (updateTimestamp never called when session data is empty). - (Girgias) - . Null bytes in session.cookie_path, session.cookie_domain, and - session.cache_limiter are now rejected with a warning. (jorgsowa) - . session.cookie_samesite now rejects invalid values with a warning; only - "Strict", "Lax", "None", or "" are accepted. (jorgsowa) - . session.cookie_lifetime now rejects non-integer and out-of-range values - with a warning. (jorgsowa) - . Session file GC now recursively cleans nested subdirectories when - session.save_path uses the dirdepth prefix. (jorgsowa) - . Changed defaults of session.use_strict_mode (now 1), session.cookie_httponly - (now 1) and session.cookie_samesite (now "Lax"). (jorgsowa) - -- Shmop: - . Fixed bug GH-9945 (shmop_open() silently truncates keys outside the key_t - range). (Weilin Du) - -- Soap: - . Soap::__setCookie() when cookie name is a digit is now not stored and - represented as a string anymore but a int. (David Carlier) - . Fixed bug GH-21421 (SoapClient typemap property breaks engine assumptions). - (ndossche) - . WSDL/XML Schema parsing now rejects out-of-range integer values for - occurrence constraints and integer restriction facets. Negative minOccurs - and maxOccurs values are rejected as well. (Weilin Du) - -- Sockets: - . Added the TCP_USER_TIMEOUT constant for Linux to set the maximum time in - milliseconds transmitted data can remain unacknowledged. (James Lucas) - . Added AF_UNSPEC support for sock_addrinfo_lookup() as a sole umbrella for - AF_INET* family only. (David Carlier) - . Fixed GH-20532 (socket_addrinfo_lookup gives the error code with a new - optional parameter). (David Carlier) - . Added AF_PACKET support completion for socket_sendto()/socket_recvfrom(). - (David Carlier) - -- Sodium: - . Added support for libsodium 1.0.21 IPcrypt and XOF APIs. (jedisct1) - . pwhash argument-validation errors now throw ValueError instead of - SodiumException. (iliaal) - -- SPL: - . DirectoryIterator key can now work better with filesystem supporting larger - directory indexing. (David Carlier) - . Fixed bug GH-21831 (SplObjectStorage::removeAllExcept() use-after-free with - re-entrant getHash()). (Pratik Bhujel) - . Fix bugs GH-8561, GH-8562, GH-8563, and GH-8564 (Fixing various - SplFileObject iterator desync bugs). (iliaal) - . Fix bug GH-22062 (SplDoublyLinkedList iterator UAF via destructor releasing - next node). (David Carlier) - -- Sysvshm: - . Fixed shm_attach() to throw ValueError for keys outside the key_t range. - (Weilin Du) - -- Sqlite3: - . Fix NUL byte truncation in sqlite3 TEXT column handling. (ndossche) - -- Standard: - . Fixed bug GH-19926 (reset internal pointer earlier while splicing array - while COW violation flag is still set). (alexandre-daubois) - . Added form feed (\f) in the default trimmed characters of trim(), rtrim() - and ltrim(). (Weilin Du) - . Invalid mode values now throw in array_filter() instead of being silently - defaulted to 0. (Jorg Sowa) - . Fixed bug GH-21058 (error_log() crashes with message_type 3 and - null destination). (David Carlier) - . Fixed bug GH-13204 (glob() fails if square bracket is in current directory). - (ndossche) - . Add array size maximum to array_diff(). (ndossche) - . Add enum SortDirection. (timwolla) - . pathinfo() raises a ValueError with an invalid $flags argument. - (David Carlier) - . Passing an invalid flag value to the second argument of scandir() will now - throw a ValueError. (alexandre-daubois) - . array_change_key_case() now raises a ValueError when an invalid $case - argument value is passed. (Girgias) - . linkinfo() now raises a ValueError when the argument is an empty string. - (Weilin Du) - . getenv() and putenv() now raises a ValueError when the first argument - contains NUL bytes. (Weilin Du) - . dl() now raises a ValueError when the $extension_filename argument contains - NUL bytes. (Weilin Du) - . openlog() now raises a ValueError when the $prefix argument contains NUL - bytes. (Weilin Du) - . parse_str() now raises a ValueError when the $string argument contains NUL - bytes. (Weilin Du) - . proc_open() now raises a ValueError when the $cwd argument contains NUL - bytes. (Weilin Du) - . ini_get_all() now includes the built-in default value in the details. - (sebastian) - . Fixed bug GH-22171 (Invalid auth header generation in http(s) stream - wrapper). (David Carlier) - . Fixed bug GH-17384 (number_format() may exhaust memory with decimals - outside the range from -2147483648 to 2147483647). (Weilin Du) - -- Streams: - . Added new stream errors API including new StreamException, StreamError - classes, StreamErrorStore, StreamErrorMode, StreamErrorCode enums, - stream_last_errors() and stream_clear_errors() functions, error_mode, - error_store and error_handler stream context options and extending some - stream functions with context param. (Jakub Zelenka) - . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream - socket context options. (Jakub Zelenka) - . Added so_reuseaddr streams context socket option that allows disabling - address resuse. (Jakub Zelenka) - . Added so_linger stream socket context option. (Jakub Zelenka) - . Fixed bug GH-20370 (User stream filters could violate typed property - constraints). (alexandre-daubois) - . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka) - . Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream). - (ilutov) - . Improved stream_socket_server() bind failure error reporting. (ilutov) - . Fixed bug #49874 (ftell() and fseek() inconsistency when using stream - filters). (Jakub Zelenka) - -- URI: - . Added Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme(). - (kocsismate) - . Added Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType(). - (kocsismate) - . Added Uri\Rfc3986\UriBuilder. (kocsismate) - -- Zip: - . Fixed bug GH-21682 (ZipArchive instances should not be serializable). - serialize()/unserialize() now throw unless a subclass overrides - __serialize()/__unserialize(). (iliaal) - . Fixed ZipArchive callback being called after executor has shut down. - (ilutov) - . Support minimum version for libzip dependency updated to 1.0.0. - (David Carlier) - . Added ZipArchive::openString() method. - (Tim Starling, Soner Sayakci, Ghaith Olabi) - -- Zlib: - . deflate_init() now raises a TypeError when the value for option - "level", "memory", "window", or "strategy" is not of type int. - (Weilin Du) - . inflate_init() now raises a TypeError when the value for option - "window" is not of type int. (Weilin Du) +?? ??? ????, PHP 8.7.0alpha1 <<< NOTE: Insert NEWS from last stable release here prior to actual release! >>> diff --git a/UPGRADING b/UPGRADING index 7f1fc588bd03..f66d5b6987f4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1,4 +1,4 @@ -PHP 8.6 UPGRADE NOTES +PHP 8.7 UPGRADE NOTES 1. Backward Incompatible Changes 2. New Features @@ -19,907 +19,30 @@ PHP 8.6 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== -- Core: - . By-reference foreach loops may now visit previously skipped elements - after array compaction. Internal pointers on deleted elements now move - to the next surviving element during copy-on-write. - . ??/empty() on a magic property no longer call __get() when __isset() - has materialized the property by writing into the property table. - The freshly-written value is returned directly. isset() is unaffected. - -- COM: - . It is no longer possible to clone variant objects because the cloning - behavior was ill-defined. - -- Curl: - . The callback registered with CURLOPT_READFUNCTION now throws a ValueError - when returning an integer other than 0, CURL_READFUNC_ABORT or - CURL_READFUNC_PAUSE. - -- DOM: - . Properties previously documented as @readonly (e.g. DOMNode::$nodeType, - DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, - DOMEntity::$encoding, DOMEntity::$version) are now declared with asymmetric - visibility (public private(set)). Attempts to write to them from outside - the class now raise "Cannot modify private(set) property ::$ - from global scope" instead of the prior readonly modification error. - ReflectionProperty::isWritable() also reports these properties - accurately. - . Array access on Dom\DtdNamedNodeMap objects now returns null for negative - integer indexes instead of returning the first node. - . Array access on Dom\DtdNamedNodeMap objects now raises a ValueError when - the integer index is greater than INT_MAX instead of overflowing to a - smaller index. - -- FTP: - . ftp_nb_fget(), ftp_nb_fput(), ftp_nb_get() and ftp_nb_put() now throw an - Error when the connection is already transferring, instead of emitting a - warning and returning false. ftp_close() already throws on the same - condition. - -- GD: - . imagesetstyle(), imagefilter() and imagecrop() filter the types / values of - their array arguments and raise a TypeError / ValueError accordingly. - . imageaffinematrixget() now enforces the documented array|float type for the - $options parameter, including the corresponding weak and strict typing - behavior. - -- GMP: - . gmp_fact() now throws a ValueError if $num does not fit into an unsigned - long. - . gmp_pow(), gmp_binomial(), gmp_root() and gmp_rootrem() now throw a - ValueError if their second argument does not fit into an unsigned long. - . The shift (<<, >>) and exponentiation (**) operators on GMP objects now - throw a ValueError when GMP right operands are outside the unsigned long range, - instead of silently truncating them. - . GMP integer string parsing now throws a ValueError for strings containing NUL - bytes, instead of silently truncating them. - . gmp_powm() modulo-by-zero now raises a DivisionByZeroError whose message - includes the function name and argument index ($modulus). - -- Intl: - . Passing a non-stringable object as a time zone to Intl APIs that accept - time zone objects or strings now raises a TypeError instead of an Error. - . IntlIterator::current() now returns null when called before the iterator is - positioned, or after the iterator becomes invalid, instead of exposing an - undefined value. - . IntlBreakIterator::getLocale() now raises a ValueError when the type is - neither Locale::ACTUAL_LOCALE nor Locale::VALID_LOCALE instead of - returning false. - . MessageFormatter::parse() and parseMessage() now return PHP_INT_MIN as - int, rather than float, on 64-bit platforms when parsing integer values. - . The $type parameter of IntlBreakIterator::getPartsIterator() has been - changed from string to int to match the underlying implementation. - . UConverter::transcode() now rejects from_subst and to_subst option values - longer than 127 bytes instead of silently truncating the length before - passing it to ICU. - . ResourceBundle::get() and resourcebundle_get() now report fallback-disabled - resource lookups with "without fallback to " instead of the - malformed "without fallback from to ". - . IntlDateFormatter::parse()/datefmt_parse() and - IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError - when the offset argument is not of type int instead of silently converting - the value. - . Collator::sort(), collator_sort(), Collator::asort(), and - collator_asort() now report UTF-8/UTF-16 conversion failures during - comparison through the intl error mechanism and return false. With - intl.use_exceptions enabled, these failures throw IntlException. Previously, - these paths emitted a warning and compared the value as an empty string. - -- MBstring: - . Unicode data tables have been updated to Unicode 18.0 - -- PCNTL: - . pcntl_alarm() now raises a ValueError if the seconds argument is - lower than zero or greater than the platform's UINT_MAX. - . pcntl_exec() now raises a ValueError if the $args argument is not a list - array. - -- PCRE: - . preg_grep() now returns false instead of a partial array when a PCRE - execution error occurs (e.g. malformed UTF-8 input with the /u modifier). - This is consistent with other preg_* functions. - -- PGSQL: - . pg_fetch_object() now reports the ValueError for a non-empty - $constructor_args on a class without a constructor on the - $constructor_args argument instead of $class. Errors raised when - the requested class is not instantiable (abstract, interface, enum) - now surface before the row is fetched. - -- Phar: - . Phar::mungServer() now raises a ValueError when an invalid argument value - is passed instead of being silently ignored. - . Phar::addEmptyDir() now rejects "/.phar" paths in addition to ".phar" - paths, and raises the same BadMethodCallException for attempts to create - the reserved magic ".phar" directory through that form. - . Phar::addEmptyDir() now treats non-magic names that merely share the - ".phar" prefix as ordinary directories. - . Files are only automatically interpreted as Phar archives when included - if ".phar" occurs as an extension in the filename component of their - paths. Previously, it could occur in a directory name or as part of an - extension such as ".pharma". - -- Session: - . Setting session.cookie_path, session.cookie_domain, or session.cache_limiter - to a value containing NUL bytes now emits a warning and leaves the setting - unchanged. Previously, NUL bytes were silently accepted: for cookie_path - and cookie_domain this caused the SAPI to drop the Set-Cookie header; for - cache_limiter the value was silently truncated at the NUL byte. - . A ValueError is thrown if $name is a string containing NUL bytes in - session_module_name(). - . session_encode() now returns an empty string instead of false for empty - sessions. It only returns false now when the session data could not be - encoded. This mainly happens with the default serialization handler - if a key contains the pipe | character. - . When session.lazy_write is enabled and a session handler implements - SessionUpdateTimestampHandlerInterface, sessions that were read as empty - and remain empty at write time will now trigger updateTimestamp() instead - of write(). Previously, write() was always called for empty sessions - because session_encode() returned false, bypassing the lazy_write - comparison. Custom session handlers that rely on write() being called - with empty data (e.g. to destroy the session) should implement the same - logic in their updateTimestamp() method. - . The defaults of three session INI settings have changed to provide secure - behavior out of the box: - - session.use_strict_mode is now 1 (was 0). Strict mode rejects - uninitialized session IDs, mitigating session fixation. Custom session - handlers that previously relied on accepting externally supplied IDs - without a corresponding storage entry must either implement - validateId() / create_sid() or explicitly set this to 0. - - session.cookie_httponly is now 1 (was 0). Session cookies are no - longer accessible to JavaScript via document.cookie. Applications - that read the session cookie from JavaScript must explicitly set - this to 0. - - session.cookie_samesite is now "Lax" (was unset). Session cookies - are no longer sent on cross-site requests other than top-level - navigations using safe HTTP methods. Applications that depend on - session cookies being sent on cross-site POST submissions must - explicitly set this to "None" (and also set session.cookie_secure - to 1). - RFC: https://wiki.php.net/rfc/session_security_defaults - . SessionHandler::validateId() has been added and delegates to the - configured save handler. A subclass that declares validateId() without - a return type now emits a deprecation notice for the tentative bool - return type. A subclass that overrides open() without calling - parent::open() keeps its previous behavior and emits a warning when an - ID is validated. - -- Shmop: - . shmop_open() now raises a ValueError when the $key argument is outside the - platform's key_t range instead of passing a truncated key to the operating - system. - -- SimpleXML: - . SimpleXMLElement::__construct() now raises a ValueError when the $data - argument contains NUL bytes, matching simplexml_load_file(). With - $dataIsURL set, it previously truncated the path at the first NUL byte. - Without it, the string went to libxml, which with default options rejects a - NUL on current versions but accepts the truncated document on older ones - and under LIBXML_RECOVER. - -- SOAP: - . The "classmap" option of SoapClient and SoapServer now rejects arrays - containing integer keys. Previously, sparse integer-keyed and mixed-keyed - arrays could be accepted. SoapClient now throws TypeError for non-array - "classmap" options and ValueError for arrays containing integer keys, also - when the "exceptions" option is disabled. - . WSDL/XML Schema parsing now rejects out-of-range integer values for - occurrence constraints and integer restriction facets. Negative minOccurs - and maxOccurs values are rejected as well. - . SOAP encoding errors now report the affected type or failing operation in - the error message instead of the generic "Encoding: Violation of encoding - rules" message. Code that compares the exact message may need to be - updated. - -- Sockets: - . socket_set_option() with SO_ATTACH_REUSEPORT_CBPF now requires an int - $value and a $level of SOL_SOCKET. Any other value type throws a TypeError - instead of being coerced, and any other level raises a warning and returns - false. - . socket_set_option() with SO_ATTACH_REUSEPORT_CBPF and a $value of 0 now - detaches the reuseport filter through SO_DETACH_REUSEPORT_BPF. It - previously used SO_DETACH_BPF, an alias of SO_DETACH_FILTER, which left the - reuseport program attached. - -- Sodium: - . The password-hashing functions sodium_crypto_pwhash(), - sodium_crypto_pwhash_str(), - sodium_crypto_pwhash_scryptsalsa208sha256() and - sodium_crypto_pwhash_scryptsalsa208sha256_str() now throw ValueError - instead of SodiumException when an argument is out of range, such as an - opslimit or memlimit below the documented minimum. SodiumException is - still thrown for internal libsodium failures. - -- SPL: - . SplObjectStorage::getHash() implementations may no longer mutate any - SplObjectStorage instance. Attempting to do so now throws an Error. - . SplFileObject::next() now advances the stream when no prior current() - call has cached a line. A subsequent current() call returns the new line - rather than the previous one. - . SplFileObject::fgets() no longer caches the returned line for subsequent - current() calls. current() now re-reads from the current stream position - instead of returning the line fgets() just returned. - . SplFileObject::next() past EOF no longer increments key() without bound. - SplFileObject::seek() past EOF now produces the same key() value as - SplTempFileObject; the two previously returned different values. - . DirectoryIterator::key() now returns int|string, - and DirectoryIterator::current() returns string|SplFileInfo|static. - -- Standard: - . array_intersect() with at least two arrays now converts values to strings - while scanning its inputs instead of during sort comparisons. This can - change the number and order of conversion warnings and __toString() calls, - which conversion exception is reached, and the result for stateful - __toString() implementations. Argument types are validated before checking - for empty arrays or converting values, so an invalid later argument can - suppress conversion side effects from earlier arrays. Values are not - converted if any input array is empty. - . Form feed (\f) is now added to the default trimmed characters of trim(), - rtrim() and ltrim(). - RFC: https://wiki.php.net/rfc/trim_form_feed - . array_filter() now raises a ValueError when an invalid $mode argument value - is passed. - . array_change_key_case() now raises a ValueError when an invalid $case - argument value is passed. - . getenv() and putenv() now raise a ValueError when the first argument - contains NUL bytes. - . dl() now raises a ValueError when the $extension_filename argument contains - NUL bytes. - . openlog() now raises a ValueError when the $prefix argument contains NUL - bytes. - . parse_str() now raises a ValueError when the $string argument contains NUL - bytes. - . setlocale() now raises a ValueError when a locale name contains NUL bytes, - instead of silently truncating it. - Arrays are now accepted only for the $locales argument. Passing an array as - a later variadic locale argument now throws a TypeError. Passing any - additional locale arguments when $locales is an array now throws an - ArgumentCountError. - . linkinfo() now raises a ValueError when the $path argument is empty. - . pathinfo() now raises a ValueError when an invalid $flags argument value is - passed. - . scandir() now raises a ValueError when an invalid $sorting_order argument - value is passed. - . number_format() now raises a ValueError when $decimals is outside the - integer range instead of silently clamping very large positive values. - . sleep() now raises a ValueError when $seconds is greater than the platform - limit (UINT_MAX seconds, or UINT_MAX / 1000 seconds on Windows) instead of - allowing the value to overflow. - . usleep() now raises a ValueError when $microseconds is greater than - UINT_MAX instead of allowing the value to overflow. - . proc_open() now raises a ValueError when the $cwd argument contains NUL - bytes. - . base_convert(), bindec(), hexdec() and octdec() now raise a notice when - they cannot precisely convert the given number. - . The following functions now raise a ValueError when the $filename argument - contains NUL bytes: - - fileperms() - - fileinode() - - filesize() - - fileowner() - - filegroup() - - fileatime() - - filemtime() - - filectime() - - filetype() - - is_writable() - - is_readable() - - is_executable() - - is_file() - - is_dir() - - is_link() - - file_exists() - - lstat() - - stat() - . unpack() now reads a "<" or ">" immediately following a format code as an - endianness modifier rather than as the first character of the element name. - Formats such as "sname" raises a ValueError because the C format code accepts no - endianness modifier. A name starting with these characters is unaffected - when a repeater precedes it, as in "s1prop = $val. - RFC: https://wiki.php.net/rfc/const_object_property_write - -- Curl: - . curl_getinfo() return array now includes a new size_delivered key, which - indicates the total number of bytes passed to the download write callback. - This value can also be obtained by passing CURLINFO_SIZE_DELIVERED as the - $option parameter. - Requires libcurl 8.20.0 or later. - . Added CURLOPT_SEEKFUNCTION to register a callback that repositions a - streamed request body so libcurl can rewind and resend it on a redirect, - multi-pass authentication, or a retried reused connection instead of - failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle, - offset and origin, and must return one of CURL_SEEKFUNC_OK, - CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK. - -- Date: - . Added a new Time\Duration class. - RFC: https://wiki.php.net/rfc/duration_class - -- Fileinfo: - . finfo_file() now works with remote streams. - -- GMP: - . Added gmp_powm_sec() for side-channel quiet modular exponentiation. - Requires GNU MP 5.0.0 or later; it is not available on official Windows - builds using MPIR. - . Added gmp_prevprime() to get the largest prime smaller than the given - number. The optional $definitely_prime output parameter indicates whether - the returned number is definitely prime, as opposed to probably prime. - A ValueError is thrown if no such prime exists. This function is available - only when PHP is built against GNU MP 6.3.0 or later; it is not available - on official Windows builds using MPIR. - -- Intl: - . Added the static methods IntlDatePatternGenerator::getSkeleton() and - IntlDatePatternGenerator::getBaseSkeleton() to generate the unique skeleton - and base skeleton for a date/time pattern. - . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(), - with the aliases locale_get_display_keyword() and - locale_get_display_keyword_value(), respectively. - RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue - . Added IntlNumberRangeFormatter class to format an interval of two numbers - with a given skeleton, locale, IntlNumberRangeFormatter::COLLAPSE_AUTO, - IntlNumberRangeFormatter::COLLAPSE_NONE, - IntlNumberRangeFormatter::COLLAPSE_UNIT, - IntlNumberRangeFormatter::COLLAPSE_ALL collapse and - IntlNumberRangeFormatter::IDENTITY_FALLBACK_SINGLE_VALUE, - IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, - IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and - IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks. - It is supported as of ICU 63. - . Added SpoofChecker::areBidiConfusable() to check whether two strings are - confusable for a given text direction, along with the SpoofChecker::LTR - and SpoofChecker::RTL direction constants. - It is supported as of ICU 74. - . Added SpoofChecker::getBidiSkeleton() to generate a confusable skeleton for - a given text direction. It is supported as of ICU 74. - . Added SpoofChecker::getSkeleton() to generate a confusable skeleton for a - given string. - -- IO: - . Added new polling API. - RFC: https://wiki.php.net/rfc/poll_api - -- JSON: - . Added extra info about error location to the JSON error messages returned - from json_last_error_msg() and JsonException message. - -- OpenSSL: - . Added TLS session resumption support for streams with new stream context - options: session_data, session_new_cb, session_cache, session_cache_size, - session_timeout, session_id_context, session_get_cb, session_remove_cb, - and num_tickets. This allows saving and restoring client sessions across - requests, implementing custom server-side session storage, and controlling - session cache behavior. - RFC: https://wiki.php.net/rfc/tls_session_resumption - . Added TLS external PSK support for streams with new stream context options: - psk_client_cb and psk_server_cb. This allows setting and receiving PSK. - . Added TLS 1.3 early data (0-RTT) support for streams. Clients send early - data with the early_data context option; servers accept it with - max_early_data and receive it through the early_data_cb callback. The - outcome is reported as 'accepted', 'rejected' or 'not_sent' in the - early_data key of the crypto stream_get_meta_data() array. - -- PDO_PGSQL: - . Added Pdo\Pgsql::ATTR_CHUNK_SIZE, the number of rows a statement fetches - per chunk. A value of 1 or more enters the lazy fetch mode of - PDO::ATTR_PREFETCH => 0. Setting PDO::ATTR_PREFETCH replaces the chunk - size. Statements that are prepared with neither it nor PDO::ATTR_PREFETCH - fall back to the value set on the connection. - Requires libpq 17 or later. - -- Phar: - . Overriding the getMTime() and getPathname() methods of SplFileInfo now - influences the result of the phar buildFrom family of functions. - This makes it possible to override the timestamp and names of files. - -- SNMP: - . It is now possible to use AES192, AES192C, AES256, and AES256C as - SNMPv3 security protocols if the underlying library supports them. - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#increase_the_number_of_snmpv3_security_protocols_supported - . It is now possible to reset the MIB tree using the new snmp_init_mib() - function. - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#allow_the_snmp_mib_to_be_reset - . Additional MIB parsing and output control functionality has been exposed - via the snmp_set_mib_option(), snmp_set_output_option(), - snmp_set_string_output_format() functions, the $numeric_index, - $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, - $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), - SNMP::setStringOutputFormat() methods. (eskyuu) - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls - -- Standard: - . pack() and unpack() now accept the "<" and ">" endianness modifiers on - the signed and unsigned integer format codes. - RFC: https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support - . pack() and unpack() now accept the "<" and ">" endianness modifiers on - the float and double format codes. - RFC: https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier - -- Streams: - . Added new stream errors API including new classes, enums, functions and - internal API. It is controlled using error_mode, error_store and - error_handler stream context options. - RFC: https://wiki.php.net/rfc/stream_errors - . Added the "filter.max_filter_count" stream context option for php://filter - URLs. When set, opening the stream fails with a warning if the URL would - add more filters than the configured value. Negative values disable the - check. - RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains - . Added stream socket context option so_reuseaddr that allows disabling - address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on - Windows. - . Added stream socket context options so_keepalive, tcp_keepidle, - tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive - options. - . Added stream socket context option so_linger that sets SO_LINGER on TCP - sockets. A positive value enables lingering for that many seconds, zero - or a negative value disables it. Values above 65535 are clamped as the - linger time is limited to an unsigned short on some platforms. - . Added stream socket context options so_rcvbuf and so_sndbuf that set the - socket receive and send buffer sizes in bytes (SO_RCVBUF and SO_SNDBUF) on - TCP and UDP sockets. The value must be an integer between 1 and 2147483647, - any other value makes the stream creation fail. The operating system may - round, cap or otherwise adjust the requested size, and may stop sizing that - buffer automatically, so the size read back can differ from the one - requested. - . Allowed casting filtered streams as file descriptors for select. - . Added the "write_seek_mode" filter parameter for the bz2, iconv, - zlib, and string stream filters. This parameter must be set via an - associative array where the key is "write_seek_mode" and the - value is one of the following strings: "preserve", "reset", or "strict". - -- URI: - . Added Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme(). - RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection - . Added Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType(). - RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection - . Added Uri\Rfc3986\UriBuilder and Uri\WhatWg\UrlBuilder. - RFC: https://wiki.php.net/rfc/uri_followup#uri_building - . Added Uri\url_percent_encode(). - RFC: https://wiki.php.net/rfc/uri_followup#percent-encoding_support - ======================================== 3. Changes in SAPI modules ======================================== -- CLI: - . The built-in development server now accepts requests using the HTTP QUERY - method instead of returning 501 Not Implemented. - . The built-in development server no longer reflects the "Host" header from - requests. - ======================================== 4. Deprecated Functionality ======================================== -- Core: - . Using "namespace" as a class constant name is deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_using_namespace_as_a_class_constant_name - . Using the return statement in a finally block is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_returning_from_a_finally_block - . Specifying a return type of array|null / ?array for __debugInfo() is now - deprecated. Specify array instead. - . Returning values from __construct() and __destruct() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecate-return-value-from-construct - . Making __construct() and __destruct() a Generator is now deprecated. - RFC: https://wiki.php.net/rfc/deprecate-return-value-from-construct - . Naming a function readonly is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_the_possibility_to_name_a_function_readonly - . Passing a 3rd argument to define() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_define_with_case_insensitive_being_specified - . Calling is_a() or is_subclass_of() with a string as the first argument - when $allow_string is false is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_a_with_string_when_allow_string_is_false - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_subclass_of_with_string_when_allow_string_is_false - -- BZ2: - . Passing an object for the Bzip2 {de}compression stream filter is now - deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_bzip2decompress_and_bzip2compress_stream_filters - -- GMP: - . The shift (<<, >>) and exponentiation (**) operators on GMP objects now - emit a deprecation warning when converting a float right operand to int - loses precision. - -- Mbstring: - . Mbregex has been deprecated, because the underlying Oniguruma library - is no longer maintained. - RFC: https://wiki.php.net/rfc/eol-oniguruma - . Passing objects to mb_convert_variables() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_vars_parameter_of_mb_convert_variables - -- MySQLi: - . The mysqli_get_charset() function and mysqli::get_charset() method are now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqli_get_charset - . The mysqli_stmt_init() function, mysqli::stmt_init() method, and calling the - mysqli_stmt constructor without providing the $query parameter are now - deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqlistmt_init - -- Reflection: - . Calling ReflectionProperty::setValue() with an object that is not an - instance of the class on which the property was declared is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionpropertysetvalue_and_reflectionpropertysetrawvalue_with_wrong_types - . Calling ReflectionProperty::setRawValue() with an object that is not an - instance of the class on which the property was declared is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionpropertysetvalue_and_reflectionpropertysetrawvalue_with_wrong_types - . Calling ReflectionMethod::invoke() or ReflectionMethod::invokeArgs() with - an object and a static method is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionmethodinvoke_and_reflectionmethodinvokeargs_with_objects_for_static_methods - -- Session: - . It is now deprecated to pass an object that does not implement the - create_sid() and validateId() methods. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid - . A deprecation is now emitted when implementing SessionHandlerInterface on a - class which doesn't define the create_sid() or validateId() methods, as - those will be moved from SessionUpdateTimestampHandlerInterface and - SessionIdInterface to SessionHandlerInterface. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid - -- SPL: - . The spl_classes() function is now deprecated. Use - ReflectionExtension::getClassNames() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_classes - . The spl_object_hash() function is now deprecated. Use spl_object_id() - instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_object_hash - . The following ArrayIterator methods are now deprecated: - * ArrayIterator::getFlags() - * ArrayIterator::setFlags() - * ArrayIterator::asort() - * ArrayIterator::ksort() - * ArrayIterator::uasort() - * ArrayIterator::uksort() - * ArrayIterator::natsort() - * ArrayIterator::natcasesort() - * ArrayIterator::unserialize() - * ArrayIterator::serialize() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_arrayiterator_methods_that_inherit_arrayobject_implementation - . The following SplFileObject methods are now deprecated: - * SplFileObject::fgetcsv() - * SplFileObject::fputcsv() - * SplFileObject::setCsvControl() - * SplFileObject::getCsvControl() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_splfileobject_csv_methods - -- Standard: - . metaphone() is deprecated. - Please use a userland phonetic matching library instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_metaphone_function - . Using more than 16 filters in a php://filter URL without configuring the - "filter.max_filter_count" stream context option now emits an E_DEPRECATED - warning. Use stream_filter_append() or configure this option explicitly. - RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains - . Passing an object to array_walk{_recursive} is now deprecated. Use - get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_array_parameter_of_array_walk_and_array_walk_recursive - . Passing an object as the $data argument to http_build_query() is now - deprecated. The interpretation of object values within $data as arrays - is also deprecated. Convert objects to arrays with get_object_vars() - before calling the function. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_data_parameter_of_http_build_query - . The is_double() function is now deprecated. Use is_float() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_double - . The is_long() and is_integer() functions are now deprecated. Use is_int() - instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_integer - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_long - . The doubleval() function is now deprecated. Use floatval() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_doubleval - . The strcoll() function is now deprecated. Use Collator::compare() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_strcoll - . The SORT_LOCALE_STRING constant for the family of sort functions is now - deprecated. Use one of the following functions instead: - * Collator::sort() - * Collator::asort() - * Collator::sortWithSortKeys() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_sort_locale_string_flag_for_sort_functions - -- Zlib: - . Passing an object for the zlib deflate and inflate stream filter is now - deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_zlibinflate_and_zlibdeflate_stream_filters - . Passing an object as the $option argument to deflate_init and inflate_init - is now deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_options_parameter_of_deflate_init_and_inflate_init - ======================================== 5. Changed Functions ======================================== -- Date: - . The following DateTime, DateTimeImmutable, DateTimeZone, DateInterval, and DatePeriod methods now have tentative static return types: - DateTime::createFromInterface() - DateTimeImmutable::__set_state() - DateTimeImmutable::modify() - DateTimeImmutable::add() - DateTimeImmutable::sub() - DateTimeImmutable::setTimezone() - DateTimeImmutable::setTime() - DateTimeImmutable::setDate() - DateTimeImmutable::setISODate() - DateTimeImmutable::setTimestamp() - DateTimeImmutable::createFromInterface() - DateTimeZone::__set_state() - DateInterval::__set_state() - DatePeriod::__set_state() - -- Filter: - . filter_var_array() return type has been narrowed from array|false|null to - array|false. The function always establishes an array before filtering, so - null was never returned. filter_input_array() is unaffected: it still - returns null when the requested superglobal does not exist. - -- LDAP: - . ldap_free_result() return type has been narrowed from bool to true. The - function already always returned true. - -- MySQLi: - . The return structure of mysqli_get_charset() no longer contains the - undocumented "comment" element. The value of "charsetnr" is now set to a - constant 0 as this number was an implementation detail that should not have - been exposed to the public. - -- OpenSSL: - . Output of openssl_x509_parse() contains criticalExtensions listing all - critical certificate extensions. - . openssl_sign() and openssl_verify() now have an additional optional - argument $salt_length that allows controlling the RSA-PSS salt length - when OPENSSL_PKCS1_PSS_PADDING is used. It accepts an explicit length or - one of the new OPENSSL_RSA_PSS_SALTLEN_* constants. - -- PDO_DBLIB: - . When using persistent connections, there is now a liveness check in the - constructor. - -- Phar: - . Phar::mungServer() now supports reference values. - -- Readline: - . readline_completion_function() now declares true as its return type. The - function assigns a static callback and then tests whether the assignment - landed, which is a tautology; an invalid callback throws a TypeError via - ZPP before the function body is reached. - -- Sockets: - . socket_addrinfo_lookup() now has an additional optional argument $error_code - that, when not null, receives the error code on failure (one of the EAI_* - constants). - . socket_cmsg_space() return type has been narrowed from ?int to int. Every - failure path has thrown a ValueError since PHP 8.0, so null was never - returned. - -- Standard: - . header_register_callback() now declares true as its return type. It has not - been able to return false since PHP 8.0.0, when passing an invalid - callback started throwing a TypeError instead. - . register_tick_function() now declares true as its return type. It has - always returned true on success; an invalid callback throws a TypeError - via ZPP before the function body is reached. - . ini_get_all() now includes a "builtin_default_value" element for each - directive when $details is true. It holds the built-in default value of the - directive (or null if it has none), independent of values set in php.ini, - on the command line, or at runtime. - . fclose(), file_put_contents() and copy() now return false when flushing - or closing the stream fails. Previously such failures were silently - ignored. - -- Zip: - . zip_entry_close() return type has been narrowed from bool to true. The - function already always returned true. - ======================================== 6. New Functions ======================================== -- GMP: - . gmp_powm_sec() - . gmp_prevprime() - -- Intl: - . grapheme_strrev() - RFC: https://wiki.php.net/rfc/grapheme_strrev - . IntlDatePatternGenerator::getSkeleton() - . IntlDatePatternGenerator::getBaseSkeleton() - . Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() - RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue - . SpoofChecker::areBidiConfusable() - . SpoofChecker::getBidiSkeleton() - . SpoofChecker::getSkeleton() - -- MySQLi: - . Added mysqli::quote_string() and mysqli_quote_string(). - RFC: https://wiki.php.net/rfc/mysqli_quote_string - -- Reflection: - . ReflectionConstant::inNamespace() - . ReflectionProperty::isReadable() and ReflectionProperty::isWritable() - RFC: https://wiki.php.net/rfc/isreadable-iswriteable - . ReflectionParameter::getDocComment() - RFC: https://wiki.php.net/rfc/parameter-doccomments - . ReflectionAttribute::inNamespace() - . ReflectionAttribute::getNamespaceName() - . ReflectionAttribute::getShortName() - -- SNMP: - . snmp_init_mib() - . snmp_set_mib_option() - . snmp_set_output_option() - . snmp_set_string_output_format() - -- Sodium: - . sodium_crypto_ipcrypt_keygen() - . sodium_crypto_ipcrypt_encrypt() - . sodium_crypto_ipcrypt_decrypt() - . sodium_crypto_ipcrypt_nd_keygen() - . sodium_crypto_ipcrypt_nd_encrypt() - . sodium_crypto_ipcrypt_nd_decrypt() - . sodium_crypto_ipcrypt_ndx_keygen() - . sodium_crypto_ipcrypt_ndx_encrypt() - . sodium_crypto_ipcrypt_ndx_decrypt() - . sodium_crypto_ipcrypt_pfx_keygen() - . sodium_crypto_ipcrypt_pfx_encrypt() - . sodium_crypto_ipcrypt_pfx_decrypt() - . sodium_bin2ip() - . sodium_ip2bin() - . sodium_crypto_xof_shake128() - . sodium_crypto_xof_shake128_init() - . sodium_crypto_xof_shake128_update() - . sodium_crypto_xof_shake128_squeeze() - . sodium_crypto_xof_shake256() - . sodium_crypto_xof_shake256_init() - . sodium_crypto_xof_shake256_update() - . sodium_crypto_xof_shake256_squeeze() - . sodium_crypto_xof_turboshake128() - . sodium_crypto_xof_turboshake128_init() - . sodium_crypto_xof_turboshake128_update() - . sodium_crypto_xof_turboshake128_squeeze() - . sodium_crypto_xof_turboshake256() - . sodium_crypto_xof_turboshake256_init() - . sodium_crypto_xof_turboshake256_update() - . sodium_crypto_xof_turboshake256_squeeze() - . sodium_crypto_kem_keypair(), sodium_crypto_kem_seed_keypair(), - sodium_crypto_kem_secretkey(), sodium_crypto_kem_publickey(), - sodium_crypto_kem_enc() and sodium_crypto_kem_dec() expose the X-Wing - KEM (hybrid ML-KEM768+X25519, libsodium's recommended KEM). - Available when PHP is built against libsodium >= 1.0.22. - . sodium_crypto_kem_mlkem768_keypair(), - sodium_crypto_kem_mlkem768_seed_keypair(), - sodium_crypto_kem_mlkem768_secretkey(), - sodium_crypto_kem_mlkem768_publickey(), - sodium_crypto_kem_mlkem768_enc() and sodium_crypto_kem_mlkem768_dec() - expose the ML-KEM768 (FIPS 203) KEM. - Available when PHP is built against libsodium >= 1.0.22. - -- Standard: - . clamp() returns the given value if in range, else returns the nearest - bound. - RFC: https://wiki.php.net/rfc/clamp_v2 - . stream_last_errors() and stream_clear_errors() - RFC: https://wiki.php.net/rfc/stream_errors - . stream_socket_get_crypto_status() - -- URI: - . Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme() - RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection - . Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType() - RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection - -- Zip: - . ZipArchive::openString() - . ZipArchive::closeString() - ======================================== 7. New Classes and Interfaces ======================================== -- Date: - . Time\Duration - RFC: https://wiki.php.net/rfc/duration_class - . Time\TimeException - RFC: https://wiki.php.net/rfc/duration_class - -- Intl: - . IntlNumberRangeFormatter - -- OpenSSL: - . Openssl\OpensslException - . Openssl\Session - RFC: https://wiki.php.net/rfc/tls_session_resumption - . Openssl\Psk - -- SNMP: - . enum: Snmp\Mib - . enum: Snmp\OidOutput - . enum: Snmp\Output - . enum: Snmp\StringOutput - -- Standard: - . enum SortDirection - RFC: https://wiki.php.net/rfc/sort_direction_enum - . StreamError - . StreamException - . enum StreamErrorStore - . enum StreamErrorMode - . enum StreamErrorCode - RFC: https://wiki.php.net/rfc/stream_errors - . Io\Poll\Context - . Io\Poll\Watcher - . enum Io\Poll\Backend - . enum Io\Poll\Event - . interface Io\Poll\Handle - . Io\IoException - . Io\Poll\PollException - . Io\Poll\FailedPollOperationException - . Io\Poll\FailedContextInitializationException - . Io\Poll\FailedHandleAddException - . Io\Poll\FailedWatcherModificationException - . Io\Poll\FailedPollWaitException - . Io\Poll\BackendUnavailableException - . Io\Poll\InactiveWatcherException - . Io\Poll\HandleAlreadyWatchedException - . Io\Poll\InvalidHandleException - . StreamPollHandle - RFC: https://wiki.php.net/rfc/poll_api - -- URI: - . Uri\Rfc3986\UriBuilder and Uri\WhatWg\UrlBuilder - RFC: https://wiki.php.net/rfc/uri_followup#uri_building - . enum Uri\WhatWg\UrlPercentEncodingMode - RFC: https://wiki.php.net/rfc/uri_followup#percent-encoding_support - ======================================== 8. Removed Extensions and SAPIs ======================================== @@ -928,236 +51,22 @@ PHP 8.6 UPGRADE NOTES 9. Other Changes to Extensions ======================================== -- Fileinfo: - . Upgraded to file 5.48. - Custom compiled magic databases from older file versions must be regenerated. - -- Hash: - . The bundled version of xxHash was upgraded to 0.8.2. - -- MySQLi: - . Added new constant MYSQLI_OPT_COMPRESS. - -- Opcache: - . JIT is now supported for ZTS builds on Apple Silicon. - ======================================== 10. New Global Constants ======================================== -- Curl: - . CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). - . CURLOPT_SEEKFUNCTION. - . CURL_SEEKFUNC_OK. - . CURL_SEEKFUNC_FAIL. - . CURL_SEEKFUNC_CANTSEEK. - . CURL_READFUNC_ABORT. - -- MySQLi: - . MYSQLI_OPT_COMPRESS. - -- OpenSSL: - . OPENSSL_RSA_PSS_SALTLEN_DIGEST. - . OPENSSL_RSA_PSS_SALTLEN_AUTO. - . OPENSSL_RSA_PSS_SALTLEN_MAX. - -- Sockets: - . TCP_USER_TIMEOUT (Linux only). - . AF_UNSPEC. - . EAI_BADFLAGS. - . EAI_NONAME. - . EAI_AGAIN. - . EAI_FAIL. - . EAI_NODATA. - . EAI_FAMILY. - . EAI_SOCKTYPE. - . EAI_SERVICE. - . EAI_ADDRFAMILY. - . EAI_SYSTEM. - . EAI_OVERFLOW. - . EAI_INPROGRESS. - . EAI_CANCELED. - . EAI_NOTCANCELED. - . EAI_ALLDONE. - . EAI_INTR. - . EAI_IDN_ENCODE. - . SO_DETACH_REUSEPORT_BPF (Linux only). - -- Sodium: - . SODIUM_CRYPTO_IPCRYPT_BYTES. - . SODIUM_CRYPTO_IPCRYPT_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_TWEAKBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_INPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_OUTPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_TWEAKBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_INPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_OUTPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_PFX_BYTES. - . SODIUM_CRYPTO_XOF_SHAKE128_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_SHAKE128_STATEBYTES. - . SODIUM_CRYPTO_XOF_SHAKE256_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_SHAKE256_STATEBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE128_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE128_STATEBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE256_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE256_STATEBYTES. - . SODIUM_CRYPTO_KEM_PUBLICKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SECRETKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_CIPHERTEXTBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SHAREDSECRETBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SEEDBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_KEYPAIRBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SECRETKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SHAREDSECRETBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SEEDBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_KEYPAIRBYTES (libsodium >= 1.0.22). - -- Standard: - . ARRAY_FILTER_USE_VALUE. - . STREAM_CRYPTO_STATUS_NONE. - . STREAM_CRYPTO_STATUS_WANT_READ. - . STREAM_CRYPTO_STATUS_WANT_WRITE. - ======================================== 11. Changes to INI File Handling ======================================== -- Core: - . The error_include_args INI option has been added to make the display of - function arguments consistent in errors; it is off by default. Previously, - some functions shown a parameter in the error that was up to each call to - the internal error function. Now, all parameters as were actually passed to - the function will be displayed. This uses the same infrastructure as stack - traces, so i.e. sensitive parameters will not be displayed, and strings - will be truncated according to zend.exception_string_param_max_len. - RFC: https://wiki.php.net/rfc/display_error_function_args - -- Mbstring: - . The mbstring.detect_order INI directive now updates the internal detection - order when changed at runtime via ini_set(). Previously, runtime changes - using ini_set() did not take effect for mb_detect_order(). Setting the - directive to NULL or an empty string at runtime now leaves the previously - configured detection order unchanged. - -- MySQLi: - . mysqli.default_port now checks the validity of the value which should be - between 0 and 65535 inclusive. - -- Opcache: - . opcache.jit_debug accepts a new flag: ZEND_JIT_DEBUG_TRACE_EXIT_INFO_SRC. - When used along with ZEND_JIT_DEBUG_TRACE_EXIT_INFO, the source of exit - points is printed in exit info output, in debug builds. - ======================================== 12. Windows Support ======================================== -- Core: - . The official Windows builds now use Visual Studio 2026 (VS18). - -- LibXML: - . The libxml2 library used by the official Windows builds has been upgraded - to version 2.15.3. As a result, DOMDocument::$documentURI for documents - loaded from a local file now contains a native filesystem path instead of - a file URI. - -- OpenSSL: - . The OpenSSL library used by the official Windows builds has been upgraded - to OpenSSL 4. - ======================================== 13. Other Changes ======================================== -- Core: - . In case of a hard OOM PHP now calls abort() instead of exit(1), changing - the exit code to 134 and possibly creating a core dump. - . The PHP_OS_FAMILY constant has an AIX value for when running on AIX or - IBM i via PASE. - ======================================== 14. Performance Improvements ======================================== - -- Core: - . printf() using only "%s" and "%d" will be compiled into the equivalent - string interpolation, avoiding the overhead of a function call and - repeatedly parsing the format string. - . Arguments are now passed more efficiently to known constructors (e.g. when - using new self()). - . array_map() using a first-class callable or partial function application - callback will be compiled into the equivalent foreach-loop, avoiding the - creation of intermediate Closures, the overhead of calling userland - callbacks from internal functions and providing for better insight for the - JIT. - . The performance of the TAILCALL VM has been improved. - . The TAILCALL VM is now enabled on Windows when compiling with Clang >= 19 - on x86_64. - . The performance of ZTS builds has been improved. - . Added stateless closure cache. - RFC: https://wiki.php.net/rfc/closure-optimizations#stateless_closure_caching - . Deeply recursive code that causes the VM to allocate new stack pages should - now be faster. - -- DOM: - . Made splitText() faster and consume less memory. - -- GD: - . imagebmp(), imagewbmp(), imagegd(), and imagegd2() now buffer output when - writing to PHP streams, significantly improving performance when writing - images to files. - . Improved performance of imagegrabscreen() and imagegrabwindow() on - Windows. - -- Intl: - . Improved performance of IntlCalendar::getAvailableLocales() and - IntlDateFormatter::localtime() / datefmt_localtime() by pre-allocating - their returned arrays. - . Improved performance of transliterator_list_ids() and - resourcebundle_locales() by pre-allocating their returned arrays. - . Optimized callback invocation in IntlChar::enumCharTypes(). - -- JSON: - . Improve performance of encoding arrays and objects. - . Improved performance of indentation generation in json_encode() - when using PHP_JSON_PRETTY_PRINT. - -- Mbstring: - . Improved performance of mb_str_pad(). - -- Phar: - . Reduced temporary allocations when iterating Phar directories. - -- Standard: - . Improved performance of addcslashes() when generating octal escapes. - . Improved performance of sorting single-element arrays. - . Improved performance of array_fill_keys(). - . Improved performance of array_intersect(). - . Improved performance of array_map() with multiple arrays passed. - . Improved performance of array_sum() and array_product() for - integer-only arrays. - . Improved performance of array_unshift(). - . Improved performance of array_walk(). - . Improved performance of intval('+0b...', 2) and intval('0b...', 2). - . Improved performance of pathinfo() when requesting a single component. - . Improved performance of str_split(). - . Improved performance of str_pad(). - . Improved performance of str_repeat() when the multiplier is 1. - -- URI: - . Improved performance of Uri\WhatWg\Url::parse() when collecting - validation errors by pre-allocating the error array. - . Reduced allocations when reading IPv6/IPvFuture hosts and paths with - Uri\Rfc3986\Uri. - . Improved performance and memory consumption when using normalizing - (non-raw) getters on already-normalized URIs with Uri\Rfc3986\Uri. - -- Zip: - . Improved performance of ZipArchive::addGlob() and - ZipArchive::addPattern() by pre-allocating their returned arrays. - . Avoid string copies in ZipArchive::addFromString(). diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 8bbdc5caabe5..41432be1e429 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -1,4 +1,4 @@ -PHP 8.6 INTERNALS UPGRADE NOTES +PHP 8.7 INTERNALS UPGRADE NOTES 1. Internal API changes @@ -14,356 +14,18 @@ PHP 8.6 INTERNALS UPGRADE NOTES 1. Internal API changes ======================== -- Breaking changes: - . String formatting functions now support the custom conversion specifiers - 'pS' (zend_string*) and 'pp' (same as 'p'). Following the 'p' specifier with - an alpha-numeric character other than 'S' or 'p' is now an error. - - Examples: - - zend_string *str; - zend_spprintf("%pS", str); // valid, same as "%S" - zend_spprintf("%pp", str); // valid, same as "%p" - zend_spprintf("%pA", str); // invalid - zend_spprintf("%ppA", str); // valid, same as zend_spprintf("%p%c", str, 'A') - -- Removed: - . The misnamed ZVAL_IS_NULL() has been removed. Use Z_ISNULL() instead. - . The zval_is_true() alias of zend_is_true() has been removed. Call - zend_is_true() directly instead. - . The _zval_get_*() compatibility macros for PHP 7.2 have been removed - call the variant without the leading underscore instead. - Affected: _zval_get_long, _zval_get_double, _zval_get_string, - _zval_get_long_func, _zval_get_double_func, _zval_get_string_func - . CHECK_ZVAL_NULL_PATH() and CHECK_NULL_PATH() have been removed, use - zend_str_has_nul_byte(Z_STR_P(...)) and zend_char_has_nul_byte() - respectively. - . ZEND_LTOA() (and ZEND_LTOA_BUF_LEN) has been removed, as it was - unsafe. Directly use ZEND_LONG_FMT with a function from the - printf family. - . The zval_dtor() alias of zval_ptr_dtor_nogc() has been removed. - Call zval_ptr_dtor_nogc() directly instead. - . The internal zend_copy_parameters_array() function is no longer exposed. - . The internal zend_hash_minmax() function is no longer exposed. Scan the - HashTable directly and use zend_compare() for value comparisons instead. - . The zend_make_callable() function has been removed, if a callable zval - needs to be obtained use the zend_get_callable_zval_from_fcc() function - instead. If this was used to store a callable, then an FCC should be - stored instead. - . The zend_exception_save() and zend_exception_restore() functions were - removed. - . The zend_set_hash_symbol() API has been removed. - . The WRONG_PARAM_COUNT and ZEND_WRONG_PARAM_COUNT() macros have been - removed. Call zend_wrong_param_count(); followed by RETURN_THROWS(); - instead. - . PHP_HAVE_STREAMS macro removed from . - . The INI_STR(), INI_INT(), INI_FLT(), and INI_BOOL() macros have been - removed. Instead new zend_ini_{bool|long|double|str|string}_literal() - macros have been added. This fixes an internal naming inconsistency as - "str" usually means zend_string*, and "string" means char*. - However INI_STR() returned a char* - . The INI_ORIG_{INT|STR|FLT|BOOL}() macros have been removed as they are - unused. If this behaviour is required fall back to the zend_ini_* - functions. - . The unused ZEND_AST_PARENT_PROPERTY_HOOK_CALL has been removed. - . The EMPTY_SWITCH_DEFAULT_CASE() macro has been removed. Use - default: ZEND_UNREACHABLE(); instead. - . The ZEND_RESULT_CODE type has been removed. Use zend_result directly. - . The zend_parse_parameters_none_throw(), zend_parse_parameters_throw(), - and ZEND_PARSE_PARAMS_THROW have been removed due to being misleading, - since ZPP always throws, unless ZEND_PARSE_PARAMS_QUIET is given. Use - the non-throw versions. - . The XtOffsetOf() alias of C’s offsetof() macro has been removed. Use - offsetof() directly. - . The deprecated Z_COPYABLE(), Z_COPYABLE_P(), Z_OPT_COPYABLE(), and - Z_OPT_COPYABLE_P() macros have been removed. Check for IS_ARRAY directly. - . The deprecated Z_IMMUTABLE(), Z_IMMUTABLE_P(), Z_OPT_IMMUTABLE(), and - Z_OPT_IMMUTABLE_P() macros have been removed. Check for - IS_ARRAY && !REFCOUNTED directly. - . The unused Z_GC_*() macros have been removed. Use the corresponding - GC_*() macro on the result of Z_COUNTED(). - . The zend_binary_zval_strcmp() and zend_binary_zval_strncmp() functions - have been removed, because they are unsafe by relying on the zvals - having a specific type. Use zend_binary_strcmp() / zend_binary_strncmp(), - string_compare_function() or similar instead. - . The OPENBASEDIR_CHECKPATH() compatibility macro has been removed, instead - use php_check_open_basedir() directly. - . The Z_CONSTANT(), Z_CONSTANT_P(), Z_OPT_CONSTANT(), and - Z_OPT_CONSTANT_P() macros have been removed. Check for IS_CONSTANT_AST - directly. - . The {_}php_stream_fopen_with_path() functions have been removed as they are - unused. - . The php_error_docref1() and php_error_docref2() functions have been - removed, instead rely on the error_include_args INI option to show the - arguments to functions in a consistent manner. - . The following PHP stream functions prefixed with _ have been removed, - and the macro without it has become the canonical function name: - * _php_stream_cast() - * _php_stream_free_enclosed() - * _php_stream_free() - * _php_stream_seek() - * _php_stream_tell() - * _php_stream_read() - * _php_stream_write() - * _php_stream_fill_read_buffer() - * _php_stream_printf() - * _php_stream_eof() - * _php_stream_getc() - * _php_stream_putc() - * _php_stream_flush() - * _php_stream_sync() - * _php_stream_get_line() - * _php_stream_puts() - * _php_stream_stat() - * _php_stream_mkdir() - * _php_stream_rmdir() - * _php_stream_readdir() - * _php_stream_set_option() - * _php_stream_get_url_stream_wrappers_hash() - * _php_get_stream_filters_hash() - * _php_stream_mmap_unmap() - * _php_stream_mmap_unmap_ex() - * _php_stream_filter_prepend() - * _php_stream_filter_append() - * _php_stream_filter_flush() - . The PHP stream function _php_stream_stat_path() has been renamed to - php_stream_stat_path_ex() - . The PHP stream function _php_stream_scandir() was removed, - insted the PHP macro php_stream_scandir() is now a function as the - flags parameter was never used. - . The PHP stream function _php_stream_flush() was removed, - instead the PHP macro php_stream_flush() is now a proper function. - . The zend_save_error_handling() function was removed. - . The zend_parse_parameter() function has been removed, use one fo the - zend_parse_arg_TYPE() APIs instead. - . The zend_is_countable() function was removed. - -- Changed: - . Internal functions that return by reference are now expected to - automatically unwrap references when the result of the call is stored in an - IS_TMP_VAR variable. This may be achieved by calling the - zend_return_unwrap_ref() function. - . ZEND_AST_METHOD_REFERENCE has been renamed to - ZEND_AST_TRAIT_METHOD_REFERENCE. - . Functions using zend_forbid_dynamic_call() *must* be flagged with - ZEND_ACC2_FORBID_DYN_CALLS (@forbid-dynamic-calls in stubs). In debug - builds, failing to include that flag will lead to assertion failures. - . The zend_get_call_trampoline_func() API now takes the __call or - __callStatic zend_function* instead of a CE and a boolean argument. - . ZSTR_INIT_LITERAL(), zend_string_starts_with_literal(), and - zend_string_starts_with_literal_ci() now support strings containing NUL - bytes. Passing non-literal char* is no longer supported. - . The zend_active_function{_ex}() functions now return a const zend_function - pointer. - . zend_function.arg_info is now always a zend_arg_info*. Before, it was a - zend_internal_arg_info on internal functions, unless the - ZEND_ACC_USER_ARG_INFO flag was set. - . ZEND_INI_GET_ADDR() is now a void* pointer instead of a char* pointer. This - more correctly represents the generic nature of the returned pointer and - allows to remove explicit casts, but possibly breaks pointer arithmetic - performed on the result. - . The zend_dval_to_lval_cap() function no longer takes a second - zend_string* parameter. - . EG(in_autoload) was renamed to EG(autoload_current_classnames) and no - longer is a pointer, but a directly embedded HashTable struct. - . Extended php_stream_filter_ops with seek method. - . php_print_info_htmlhead() now takes a title argument. - . zend_argument_error_variadic() now takes a new 'function' parameter. - . The param argument in the php_verror() function has been removed. - . The php_stream_wrapper_log_error() signature changed from - (wrapper, options, fmt, ...) to - (wrapper, context, options, severity, terminating, code, fmt, ...). - To keep the previous behaviour pass NULL for the context, or the context at - hand if there is one, E_WARNING for the severity, and - ZEND_ENUM_StreamErrorCode_Generic for the code. terminating should be true - only if the error aborts the operation. - . zend_create_closure(), zend_create_fake_closure() and - zend_create_partial_closure() now take the bound $this as a zend_object* - instead of a zval*. Accordingly, zend_get_closure_this_ptr() now returns - that zend_object*, or NULL when the closure is unbound, instead of a - zval* that is IS_UNDEF when the closure is unbound. - . object_properties_load() now verifies that the given value is assignable - to typed properties. The check is performed in strict mode. - -- Added: - . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were - added, given the primary flags were running out of bits. - . Added zend_hash_str_lookup(). - . Added zend_ast_call_get_args() to fetch the argument node from any call - node. - . Added Z_PARAM_ENUM(). - . Added PHP_GD_Z_PARAM_ARRAY_HT_OR_DOUBLE() in ext/gd to parse array|float - arguments into either a HashTable pointer or a double. - . Added zend_enum_fetch_case_id(). - . Added zend_enum_get_case_by_id(). - . Added zend_bin2hex() and zend_bin2hex_str() as helper functions to remove - dependencies on /ext/hash in various extensions. - . Added a C23_ENUM() helper macro to define forward-compatible fixed-size - enums. - . Added zend_fcall_info.consumed_args together with - zend_fci_consumed_arg(), which allows moving a selected callback argument - instead of copying it in zend_call_function(). Currently only a single - consumed argument is supported. - . Added ZEND_CONTAINER_OF(). - . Added zend_reflection_property_set_raw_value_without_lazy_initialization(), - zend_reflection_property_set_raw_value() to expose the functionality of - ReflectionProperty::setRawValueWithoutLazyInitialization() and - ReflectionProperty::setRawValue() to C extensions. - . Added zend_object_set_properties_reinitable() to centralise temporarily - allowing reinitialisation of initialised readonly properties during - controlled operations such as cloning and unserialisation. - . Added zend_argument_error_ex(), zend_argument_type_error_ex(), - zend_argument_value_error_ex(). - . Added zend_ast_dup(). - . Added zend_compile_ast(). - . Added zend_check_type_ex(). - . Added zend_create_partial_closure(). - . Added a new IO copy API in . php_io_copy() copies bytes between - file descriptors using the most efficient platform primitive available - (sendfile, splice, copy_file_range, TransmitFile), and is now used by - php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed. - . Added zend_string_equals_cstr_ci(). - . Added zend_cstr_append_char(), zend_cstr_concat(), and - zend_cstr_concat3() as helper functions to allocate NUL-terminated raw C - strings from one or more buffers. - . Added zend_string_ends_with() and related variants. - . Added trait support for internal classes. - . Added do_php_cli(). - . Added zval_try_get_double(), which converts a defined zval to a double and - reports conversion failures through a bool pointer. String conversion uses - the numeric-string semantics of zval_try_get_long(), rather than the - zend_strtod() semantics of zval_get_double(); non-numeric strings such as - "INF" and "NAN" fail, while leading-numeric strings emit E_WARNING. When - *failed is true, the returned value must not be used and an exception may - already be pending. Passing an IS_UNDEF zval is a caller error. - ======================== 2. Build system changes ======================== -- Abstract: - . run-tests.php now runs in parallel by default, using up to 10 automatically - detected workers. Pass -j1 for sequential execution. --asan, --msan, and - Valgrind default to at most two workers. - . Minimum required PHP version found on the host system for running scripts - like build/gen_stub.php during development has been updated from 7.4 to 8.1. - . build/gen_stub.php may now generate a _decl.h file in addition to - the _arginfo.h file, if the stub declares enums and is annotated with - @generate-c-enums. For each enum the file will contain a C enum. Enum - values can be compared to the result of - zend_enum_fetch_case_id(zend_object*). - -- Unix build system changes: - . scripts/dev/update-autoconf.sh has been added to update config.*/libtool. - . libtool has been upgraded to 2.5.4 (serial 63), which fixes many bugs. - . As part of the upgrade to the new libtool: - . libtool is now spread across multiple files. phpize has been updated to - handle this. - . On macOS, libtool now uses -undefined dynamic_lookup for shared objects, - instead of -undefined suppress -flat_namespace. - . --with-pic is now --enable-pic. The old flag will result in an error. - . Symbol HAVE_ST_BLOCKS has been removed from php_config.h (use - HAVE_STRUCT_STAT_ST_BLOCKS). - . Added a new configure option --disable-apache2-conf to prevent apxs from - editing httpd.conf during installation. - -- Windows build system changes: - . Function SETUP_OPENSSL() doesn't accept 6th argument anymore and doesn't - define the HAVE_OPENSSL_SSL_H preprocessor macro anymore. - . Function SETUP_SQLITE3() doesn't define HAVE_SQLITE3_H and HAVE_SQLITE3EXT_H - preprocessor macros anymore. - . Added a new function CHECK_HEADER() which is intended to be used instead of - the CHECK_HEADER_ADD_INCLUDE(). - -- Embed: - . The CLI SAPI can not be disabled when building the embed SAPI - (--enable-embed is incompatible with --disable-cli). - ======================== 3. Module changes ======================== -- ext/date: - . php_idate() now returns the result state, and moves the return value into an - out parameter. - -- ext/intl: - . Added intl_icu_compat.h with helpers and feature macros for ICU - version-specific API differences. Code in ext/intl should use the - intl_icu_compat_* helpers and INTL_ICU_HAS_* macros instead of adding - direct U_ICU_VERSION_* guards for supported ICU API variants. - . The internal grapheme_get_break_iterator() helper no longer accepts a - stack buffer argument; pass only the UErrorCode* status argument. - . Added PHP_INTL_FUNCTION_WITH_ERROR_RESET() for procedural functions that - reset the global error. Use it instead of PHP_FUNCTION() followed by a - manual intl_error_reset(NULL) call. - . IC_METHOD() now resets the global error before entering the method - implementation. IntlChar methods should no longer reset it manually. - -- ext/mbstring: - . Added GB18030-2022 to default encoding list for zh-CN. - -- ext/mysqlnd: - . Dropped session_options parameter from all methods in mysqlnd_auth. - The same information is present in conn->options and should be used - instead. - . Removed charsets plugin. - -- ext/session: - . php_session_flush() now returns a bool rather than a zend_result. - . The mod_user_names global has been removed. - . The mod_user_uses_object_methods_as_handlers global has been added, - it indicates whether the session handlers are methods of an object or not. - . Removed session_adapt_url(). - . PS_OPEN_ARGS is now defined as - `void **mod_data, zend_string *save_path, zend_string *session_name` - rather than - `void **mod_data, const char *save_path, const char *session_name` - . PS_FUNCS() now includes the PS_VALIDATE_SID_FUNC() - . PS_MOD() now requires that the PS_CREATE_SID_FUNC() and - PS_VALIDATE_SID_FUNC() functions are defined. - . PS_FUNCS_SID() and PS_MOD_SID() have been removed. - Either use PS_FUNCS()/PS_MOD() or PS_FUNCS_UPDATE_TIMESTAMP()/ - PS_MOD_UPDATE_TIMESTAMP() if timestamp support exists. - -- ext/standard: - . _php_error_log() now has a formal return type of zend_result. - . _php_error_log() now accepts zend_string* values instead of char*. - . _php_error_log_ex() has been removed. - . php_mail()'s extra_cmd parameter is now a zend_string*. - . The php_math_round_mode_from_enum() function now takes a - zend_enum_RoundingMode parameter. - -- ext/uri: - . The value parameter of the php_uri_property_handler_write callback is now - const zval * instead of zval *, reflecting that write handlers must - not modify the input zval. - -- ext/xml: - . Removed the XML_ExpatVersion() libxml compatibility wrapper, - as it was unused. - . Removed the XML_GetCurrentByteCount() libxml compatibility wrapper, - as it was unused and could return the wrong result. - ======================== 4. OpCode changes ======================== -- Added ZEND_TYPE_ASSERT to check a value's type against the parameter - type of a function, throwing a TypeError on failure as if the function - was called. Used in optimizations that elide function calls. - ======================== 5. SAPI changes ======================== - -- SAPIs should explicitly release a thread's resources by calling - ts_free_thread() before terminating it. tsrm_shutdown() can only release the - resources of the calling thread, for resources allocated with - ts_allocate_tls_id(). - -- AG and SCNG are now allocated with ts_allocate_tls_id() and live in native - __thread storage on ZTS builds. - -- php-cli functionality is now available in embed builds via the do_php_cli() - function. diff --git a/Zend/zend.h b/Zend/zend.h index a0f094324426..6faab6353920 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -19,7 +19,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.6.0-dev" +#define ZEND_VERSION "4.7.0-dev" #define ZEND_ENGINE_3 diff --git a/Zend/zend_extensions.h b/Zend/zend_extensions.h index 1e6887e444a6..b8a384a1f544 100644 --- a/Zend/zend_extensions.h +++ b/Zend/zend_extensions.h @@ -43,7 +43,7 @@ You can use the following macro to check the extension API version for compatibi /* The first number is the engine version and the rest is the date (YYYYMMDD). * This way engine 2/3 API no. is always greater than engine 1 API no.. */ -#define ZEND_EXTENSION_API_NO 420250926 +#define ZEND_EXTENSION_API_NO 420260925 typedef struct _zend_extension_version_info { int zend_extension_api_no; diff --git a/Zend/zend_modules.h b/Zend/zend_modules.h index 3a98b1c06e29..bb3050927992 100644 --- a/Zend/zend_modules.h +++ b/Zend/zend_modules.h @@ -30,7 +30,7 @@ #define ZEND_MODULE_INFO_FUNC_ARGS zend_module_entry *zend_module #define ZEND_MODULE_INFO_FUNC_ARGS_PASSTHRU zend_module -#define ZEND_MODULE_API_NO 20250926 +#define ZEND_MODULE_API_NO 20260925 #ifdef ZTS #define USING_ZTS 1 #else diff --git a/configure.ac b/configure.ac index b1ff51bcc2d1..5a62f91848f2 100644 --- a/configure.ac +++ b/configure.ac @@ -23,7 +23,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.6.0-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.7.0-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_MACRO_DIR([build]) AC_CONFIG_AUX_DIR([build]) diff --git a/docs/release-process.md b/docs/release-process.md index 2a53a00a2775..55bd45279b30 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -952,7 +952,6 @@ feature development that cannot go into the new version. `Zend/zend.h`, and `win32/build/confutils.js`; * update the API version numbers in `Zend/zend_extensions.h`, `Zend/zend_modules.h`, and `main/php.h`; and - * add the new branch to the list in `CONTRIBUTING.md`. See [Prepare for PHP 8.2][] and [Prepare for PHP 8.2 (bis)][] for an example of what this commit should include. diff --git a/main/php.h b/main/php.h index 275d07309ca1..89ceac346b50 100644 --- a/main/php.h +++ b/main/php.h @@ -20,7 +20,7 @@ #include #endif -#define PHP_API_VERSION 20250926 +#define PHP_API_VERSION 20260925 #define YYDEBUG 0 #define PHP_DEFAULT_CHARSET "UTF-8" diff --git a/main/php_version.h b/main/php_version.h index fa9484cbe150..515f48bb63c1 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -1,8 +1,8 @@ /* automatically generated by configure */ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 -#define PHP_MINOR_VERSION 6 +#define PHP_MINOR_VERSION 7 #define PHP_RELEASE_VERSION 0 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.6.0-dev" -#define PHP_VERSION_ID 80600 +#define PHP_VERSION "8.7.0-dev" +#define PHP_VERSION_ID 80700 diff --git a/win32/build/confutils.js b/win32/build/confutils.js index 7d9297e8c2d6..7c2cab480bf5 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -93,10 +93,10 @@ if (typeof(CWD) == "undefined") { if (!MODE_PHPIZE) { /* defaults; we pick up the precise versions from configure.ac */ var PHP_VERSION = 8; - var PHP_MINOR_VERSION = 6; + var PHP_MINOR_VERSION = 7; var PHP_RELEASE_VERSION = 0; var PHP_EXTRA_VERSION = ""; - var PHP_VERSION_STRING = "8.6.0"; + var PHP_VERSION_STRING = "8.7.0"; } /* Get version numbers and DEFINE as a string */ From 5a6c2234bf1a4528a157eac6a6deb34695a5735e Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Tue, 22 Sep 2026 14:08:04 +0200 Subject: [PATCH 20/25] CI updates after forking 8.6 --- .github/matrix.php | 3 ++- .github/scripts/windows/find-target-branch.bat | 2 +- .github/workflows/test.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/matrix.php b/.github/matrix.php index ee9cb4eb05ab..87486791cff5 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -1,7 +1,8 @@ 'master', 'ref' => 'master', 'version' => [8, 6]], + ['name' => 'master', 'ref' => 'master', 'version' => [8, 7]], + ['name' => 'PHP-8.6', 'ref' => 'PHP-8.6', 'version' => [8, 6]], ['name' => 'PHP-8.5', 'ref' => 'PHP-8.5', 'version' => [8, 5]], ['name' => 'PHP-8.4', 'ref' => 'PHP-8.4', 'version' => [8, 4]], ['name' => 'PHP-8.3', 'ref' => 'PHP-8.3', 'version' => [8, 3]], diff --git a/.github/scripts/windows/find-target-branch.bat b/.github/scripts/windows/find-target-branch.bat index 44b0bde1ec8c..1fd05a720ef7 100644 --- a/.github/scripts/windows/find-target-branch.bat +++ b/.github/scripts/windows/find-target-branch.bat @@ -3,6 +3,6 @@ for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i -if /i "%BRANCH%" equ "8.6" ( +if /i "%BRANCH%" equ "8.7" ( set BRANCH=master ) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eafedec5eafa..057ed83eb226 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ on: - PHP-8.3 - PHP-8.4 - PHP-8.5 + - PHP-8.6 - master pull_request: paths-ignore: *ignore_paths From 630128392b3f936551139eb8b457d8694c7116df Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 10:51:55 -0400 Subject: [PATCH 21/25] ext/intl: Reject unconstructed Collator in attribute and strength methods Collator::getAttribute(), setAttribute(), getStrength() and setStrength() dereferenced a NULL ICU collator when called on an object whose constructor skipped parent::__construct(), returning bogus values instead of failing, while compare(), getLocale(), sort() and getSortKey() already throw "Object not initialized". Apply the same guard to the four remaining methods through a collator_check_initialized() helper, which also replaces four existing copies. Closes GH-23797 --- NEWS | 2 + ext/intl/collator/collator_attr.c | 16 ++++++ ext/intl/collator/collator_class.h | 16 ++++++ ext/intl/collator/collator_compare.c | 7 +-- ext/intl/collator/collator_locale.c | 7 +-- ext/intl/collator/collator_sort.c | 14 +---- .../collator_attribute_unconstructed.phpt | 55 +++++++++++++++++++ 7 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 ext/intl/tests/collator_attribute_unconstructed.phpt diff --git a/NEWS b/NEWS index a37bfa30e4bc..e7f45c810b23 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,8 @@ PHP NEWS . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state such as dateType, timeType, calendar and the message pattern. (Ilia Alshanetsky) + . Fixed Collator attribute and strength methods not rejecting an + unconstructed Collator. (Ilia Alshanetsky) - Lexbor: . Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a diff --git a/ext/intl/collator/collator_attr.c b/ext/intl/collator/collator_attr.c index f16ae0cc5285..0bc680260928 100644 --- a/ext/intl/collator/collator_attr.c +++ b/ext/intl/collator/collator_attr.c @@ -40,6 +40,10 @@ PHP_FUNCTION( collator_get_attribute ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + value = ucol_getAttribute( co->ucoll, attribute, COLLATOR_ERROR_CODE_P( co ) ); COLLATOR_CHECK_STATUS( co, "Error getting attribute value" ); @@ -64,6 +68,10 @@ PHP_FUNCTION( collator_set_attribute ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Set new value for the given attribute. */ ucol_setAttribute( co->ucoll, attribute, value, COLLATOR_ERROR_CODE_P( co ) ); COLLATOR_CHECK_STATUS( co, "Error setting attribute value" ); @@ -87,6 +95,10 @@ PHP_FUNCTION( collator_get_strength ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Get current strength and return it. */ RETURN_LONG( ucol_getStrength( co->ucoll ) ); } @@ -109,6 +121,10 @@ PHP_FUNCTION( collator_set_strength ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Set given strength. */ ucol_setStrength( co->ucoll, strength ); diff --git a/ext/intl/collator/collator_class.h b/ext/intl/collator/collator_class.h index 5c69c2e5affb..9fdfbb0d01bd 100644 --- a/ext/intl/collator/collator_class.h +++ b/ext/intl/collator/collator_class.h @@ -46,6 +46,22 @@ static inline Collator_object *php_intl_collator_fetch_object(zend_object *obj) } #define Z_INTL_COLLATOR_P(zv) php_intl_collator_fetch_object(Z_OBJ_P(zv)) +static zend_always_inline zend_result collator_check_initialized(Collator_object *co) +{ + ZEND_ASSERT(co != NULL); + + if (UNEXPECTED(co->ucoll == NULL)) { + intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); + intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), + "Object not initialized", 0 ); + zend_throw_error(NULL, "Object not initialized"); + + return FAILURE; + } + + return SUCCESS; +} + void collator_register_Collator_symbols(int module_number); void collator_object_init( Collator_object* co ); void collator_object_destroy( Collator_object* co ); diff --git a/ext/intl/collator/collator_compare.c b/ext/intl/collator/collator_compare.c index f71d57f74f86..26d05601f94d 100644 --- a/ext/intl/collator/collator_compare.c +++ b/ext/intl/collator/collator_compare.c @@ -48,12 +48,7 @@ PHP_FUNCTION( collator_compare ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), - "Object not initialized", 0 ); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/collator/collator_locale.c b/ext/intl/collator/collator_locale.c index e1cdcdf2a609..22c6e672a5ac 100644 --- a/ext/intl/collator/collator_locale.c +++ b/ext/intl/collator/collator_locale.c @@ -41,12 +41,7 @@ PHP_FUNCTION( collator_get_locale ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), - "Object not initialized", 0 ); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/collator/collator_sort.c b/ext/intl/collator/collator_sort.c index 9d4cb4220203..6b8feaaa03f5 100644 --- a/ext/intl/collator/collator_sort.c +++ b/ext/intl/collator/collator_sort.c @@ -376,12 +376,7 @@ PHP_FUNCTION( collator_sort_with_sort_keys ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), - "Object not initialized", 0 ); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } @@ -532,12 +527,7 @@ PHP_FUNCTION( collator_get_sort_key ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), - "Object not initialized", 0 ); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/tests/collator_attribute_unconstructed.phpt b/ext/intl/tests/collator_attribute_unconstructed.phpt new file mode 100644 index 000000000000..f8eb5afa01d5 --- /dev/null +++ b/ext/intl/tests/collator_attribute_unconstructed.phpt @@ -0,0 +1,55 @@ +--TEST-- +Collator attribute and strength methods on unconstructed object +--EXTENSIONS-- +intl +--FILE-- + fn() => $c->getAttribute(Collator::NUMERIC_COLLATION), + 'setAttribute' => fn() => $c->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON), + 'getStrength' => fn() => $c->getStrength(), + 'setStrength' => fn() => $c->setStrength(Collator::SECONDARY), +]; + +foreach ($methods as $method => $call) { + try { + $call(); + } catch (Error $e) { + echo $method, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +$functions = [ + 'collator_get_attribute' => fn() => collator_get_attribute($c, Collator::NUMERIC_COLLATION), + 'collator_set_attribute' => fn() => collator_set_attribute($c, Collator::NUMERIC_COLLATION, Collator::ON), + 'collator_get_strength' => fn() => collator_get_strength($c), + 'collator_set_strength' => fn() => collator_set_strength($c, Collator::SECONDARY), +]; + +foreach ($functions as $function => $call) { + try { + $call(); + } catch (Error $e) { + echo $function, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +?> +--EXPECT-- +getAttribute: Error: Object not initialized +setAttribute: Error: Object not initialized +getStrength: Error: Object not initialized +setStrength: Error: Object not initialized +collator_get_attribute: Error: Object not initialized +collator_set_attribute: Error: Object not initialized +collator_get_strength: Error: Object not initialized +collator_set_strength: Error: Object not initialized From 95c29bbb2794b3390d8ca3cf5a87c231fed07785 Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Tue, 22 Sep 2026 14:08:04 +0200 Subject: [PATCH 22/25] CI updates after forking 8.6 --- .github/matrix.php | 3 ++- .github/scripts/windows/find-target-branch.bat | 2 +- .github/workflows/test.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/matrix.php b/.github/matrix.php index ee9cb4eb05ab..87486791cff5 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -1,7 +1,8 @@ 'master', 'ref' => 'master', 'version' => [8, 6]], + ['name' => 'master', 'ref' => 'master', 'version' => [8, 7]], + ['name' => 'PHP-8.6', 'ref' => 'PHP-8.6', 'version' => [8, 6]], ['name' => 'PHP-8.5', 'ref' => 'PHP-8.5', 'version' => [8, 5]], ['name' => 'PHP-8.4', 'ref' => 'PHP-8.4', 'version' => [8, 4]], ['name' => 'PHP-8.3', 'ref' => 'PHP-8.3', 'version' => [8, 3]], diff --git a/.github/scripts/windows/find-target-branch.bat b/.github/scripts/windows/find-target-branch.bat index 44b0bde1ec8c..1fd05a720ef7 100644 --- a/.github/scripts/windows/find-target-branch.bat +++ b/.github/scripts/windows/find-target-branch.bat @@ -3,6 +3,6 @@ for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i -if /i "%BRANCH%" equ "8.6" ( +if /i "%BRANCH%" equ "8.7" ( set BRANCH=master ) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eafedec5eafa..057ed83eb226 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ on: - PHP-8.3 - PHP-8.4 - PHP-8.5 + - PHP-8.6 - master pull_request: paths-ignore: *ignore_paths From 68d84faccf332b50b3c72bde9d57b8e301251107 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 22 Sep 2026 14:55:15 +0200 Subject: [PATCH 23/25] PHP 8.3 is now for PHP 8.3.36 --- NEWS | 5 ++++- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index 0ba2c03f8175..1240399e8c79 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.3.34 +?? ??? ????, PHP 8.3.36 + + +24 Sep 2026, PHP 8.3.35 - Filter: . Fixed GHSA-ch8v-r6jh-4vvr (FILTER_SANITIZE_ENCODED does not encode 0xFF). diff --git a/Zend/zend.h b/Zend/zend.h index cd91eb9f00bb..b2cac5ebbb4d 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.3.34-dev" +#define ZEND_VERSION "4.3.36-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 9ed44d6cfb63..1cf8531999bb 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.3.34-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.3.36-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index 7943f1295258..0b7852d68c39 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 3 -#define PHP_RELEASE_VERSION 34 +#define PHP_RELEASE_VERSION 36 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.3.34-dev" -#define PHP_VERSION_ID 80334 +#define PHP_VERSION "8.3.36-dev" +#define PHP_VERSION_ID 80336 From 5714009960595640b838853cb038889d66492ae9 Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Tue, 22 Sep 2026 14:08:04 +0200 Subject: [PATCH 24/25] CI updates after forking 8.6 --- .github/matrix.php | 3 ++- .github/scripts/windows/find-target-branch.bat | 2 +- .github/workflows/test.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/matrix.php b/.github/matrix.php index ee9cb4eb05ab..87486791cff5 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -1,7 +1,8 @@ 'master', 'ref' => 'master', 'version' => [8, 6]], + ['name' => 'master', 'ref' => 'master', 'version' => [8, 7]], + ['name' => 'PHP-8.6', 'ref' => 'PHP-8.6', 'version' => [8, 6]], ['name' => 'PHP-8.5', 'ref' => 'PHP-8.5', 'version' => [8, 5]], ['name' => 'PHP-8.4', 'ref' => 'PHP-8.4', 'version' => [8, 4]], ['name' => 'PHP-8.3', 'ref' => 'PHP-8.3', 'version' => [8, 3]], diff --git a/.github/scripts/windows/find-target-branch.bat b/.github/scripts/windows/find-target-branch.bat index 44b0bde1ec8c..1fd05a720ef7 100644 --- a/.github/scripts/windows/find-target-branch.bat +++ b/.github/scripts/windows/find-target-branch.bat @@ -3,6 +3,6 @@ for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i -if /i "%BRANCH%" equ "8.6" ( +if /i "%BRANCH%" equ "8.7" ( set BRANCH=master ) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eafedec5eafa..057ed83eb226 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ on: - PHP-8.3 - PHP-8.4 - PHP-8.5 + - PHP-8.6 - master pull_request: paths-ignore: *ignore_paths From 795440a704d4c2523c43666aef43c6a0fa193fee Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 22 Sep 2026 11:35:38 +0200 Subject: [PATCH 25/25] Fix GHSA-fpwc-w8rq-cr92: do not cut the request short when stripping the last user header Port the line-aware strip_header() from the PHP-8.2 fix. When the stripped header was the last line of the user header bag, the previous implementation left the line break in front of it behind, and the CRLF appended after the bag then ended the header block early. With Authorization, Cookie or Proxy-Authorization now being stripped on cross-origin redirects, this could hit a body-preserving 307/308 POST: the target received a header block cut off after the preceding header and the wrong bytes as the body. The ported version also removes folded continuation lines of the stripped header and tolerates whitespace before the colon, so a header written as "Authorization : ..." cannot slip through. --- ext/standard/http_fopen_wrapper.c | 78 +++++++++++++------ ext/standard/tests/http/bug61548.phpt | 2 - .../tests/http/ghsa-fpwc-w8rq-cr92-002.phpt | 61 +++++++++++++++ 3 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index b7a05e2942d8..4e6317a079a6 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -85,34 +85,68 @@ #define HTTP_WRAPPER_KEEP_METHOD 4 #define HTTP_WRAPPER_STRIP_AUTH 8 -/* Removes every line whose header name matches. Neither a repeated header nor an - * occurrence of the name inside another header's value may leave the real header - * behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */ +static char *next_header_line(char *line) +{ + while (*line != '\0' && *line != '\r' && *line != '\n') { + line++; + } + if (*line == '\r') { + line++; + } + if (*line == '\n') { + line++; + } + + return line; +} + +/* Removes every line whose header name matches, along with the folded + * continuation lines carrying the rest of its value. Neither a repeated header + * nor an occurrence of the name inside another header's value may leave the real + * header behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */ static inline void strip_header(char *header_bag, char *lc_header_bag, const char *lc_header_name) { - char *lc_header_start = lc_header_bag; + size_t name_len = strlen(lc_header_name); + char *lc_line = lc_header_bag; - while ((lc_header_start = strstr(lc_header_start, lc_header_name))) { - if (lc_header_start != lc_header_bag && *(lc_header_start-1) != '\n') { - lc_header_start += strlen(lc_header_name); + while (*lc_line != '\0') { + if (strncmp(lc_line, lc_header_name, name_len) != 0) { + lc_line = next_header_line(lc_line); continue; } - char *header_start = header_bag + (lc_header_start - lc_header_bag); - char *lc_eol = strchr(lc_header_start, '\n'); + /* the whitespace RFC 7230 forbids before the colon is tolerated by some + * servers, so it must not hide the header from us either */ + const char *lc_colon = lc_line + name_len; + while (*lc_colon == ' ' || *lc_colon == '\t') { + lc_colon++; + } + + if (*lc_colon != ':') { + lc_line = next_header_line(lc_line); + continue; + } - if (!lc_eol) { - *lc_header_start = '\0'; - *header_start = '\0'; - return; + char *lc_next = next_header_line(lc_line); + while (*lc_next == ' ' || *lc_next == '\t') { + lc_next = next_header_line(lc_next); + } + + if (*lc_next == '\0') { + /* drop the preceding line break too, or the one appended after the bag + * would close the header block early */ + while (lc_line > lc_header_bag + && (*(lc_line - 1) == '\r' || *(lc_line - 1) == '\n')) { + --lc_line; + } } - char *eol = header_start + (lc_eol - lc_header_start); - size_t eollen = strlen(lc_eol); + size_t tail_len = strlen(lc_next) + 1; + char *line = header_bag + (lc_line - lc_header_bag); - memmove(lc_header_start, lc_eol+1, eollen); - memmove(header_start, eol+1, eollen); + memmove(line, header_bag + (lc_next - lc_header_bag), tail_len); + memmove(lc_line, lc_next, tail_len); } } @@ -709,15 +743,15 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, if (!header_init && !redirect_keep_method) { /* strip POST headers on redirect */ - strip_header(user_headers, t, "content-length:"); - strip_header(user_headers, t, "content-type:"); + strip_header(user_headers, t, "content-length"); + strip_header(user_headers, t, "content-type"); } if (flags & HTTP_WRAPPER_STRIP_AUTH) { - strip_header(user_headers, t, "authorization:"); - strip_header(user_headers, t, "cookie:"); + strip_header(user_headers, t, "authorization"); + strip_header(user_headers, t, "cookie"); if (!use_proxy) { - strip_header(user_headers, t, "proxy-authorization:"); + strip_header(user_headers, t, "proxy-authorization"); } } diff --git a/ext/standard/tests/http/bug61548.phpt b/ext/standard/tests/http/bug61548.phpt index 5f21b3769dd8..ba46e65704f3 100644 --- a/ext/standard/tests/http/bug61548.phpt +++ b/ext/standard/tests/http/bug61548.phpt @@ -55,7 +55,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close @@ -69,7 +68,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close diff --git a/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt new file mode 100644 index 000000000000..c314a24ea2eb --- /dev/null +++ b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt @@ -0,0 +1,61 @@ +--TEST-- +GHSA-fpwc-w8rq-cr92: stripping the last user header must not cut the redirected request short +--INI-- +allow_url_fopen=1 +--SKIPIF-- + +--FILE-- + [ + 'method' => 'POST', + 'header' => "X-Test: 1\r\nContent-Type: text/plain\r\nAuthorization: Basic Zm9vOmJhcg==", + 'content' => 'hello=world', + 'follow_location' => 1, +]]); + +$captureB = null; +['pid' => $pidB, 'uri' => $uriB] = http_server([ + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureB); + +$captureA = null; +['pid' => $pidA, 'uri' => $uriA] = http_server([ + "data://text/plain,HTTP/1.1 307 Temporary Redirect\r\nLocation: $uriB/target\r\nContent-Length: 0\r\n\r\n", +], $captureA); + +var_dump(file_get_contents($uriA . '/start', false, $ctx)); + +http_server_kill($pidA); +http_server_kill($pidB); + +rewind($captureA); +echo "--- origin A ---\n", stream_get_contents($captureA), "\n"; +rewind($captureB); +echo "--- origin B ---\n", stream_get_contents($captureB), "\n"; +?> +--EXPECTF-- +string(2) "OK" +--- origin A --- +POST /start HTTP/1.1 +Host: %s:%d +Connection: close +Content-Length: 11 +X-Test: 1 +Content-Type: text/plain +Authorization: Basic Zm9vOmJhcg== + +hello=world +--- origin B --- +POST /target HTTP/1.1 +Host: %s:%d +Connection: close +Content-Length: 11 +X-Test: 1 +Content-Type: text/plain + +hello=world