Commit 60dba5a1a27 for php

commit 60dba5a1a271656123eb9c30464de1817c3cf9bf
Merge: ad0d61ae799 047c4501f28
Author: Máté Kocsis <kocsismate90@gmail.com>
Date:   Sun Nov 8 22:21:20 2026 +0100

    Merge branch 'PHP-8.5' into PHP-8.6

    * PHP-8.5:
      Fix in-place URL decoding of interned userinfo in the http and ftp wrappers

diff --cc NEWS
index ab99b600e65,4ed7877b679..943c2a8de9f
--- a/NEWS
+++ b/NEWS
@@@ -60,11 -51,9 +60,13 @@@ PH
    . Fixed incorrect handling of non-ASCII pending_input bytes in readline_info(),
      which could cause readline() to return false prematurely. (Weilin Du)

 +- Reflection:
 +  . Fixed bug GH-25266 (Misaligned read in ReflectionProperty::isReadable()).
 +    (Lazizbek Ergashev)
 +
  - Streams:
+   . Fixed in-place URL decoding of interned strings in the HTTP and FTP
+     wrappers. (Alejandro Pacheco)
    . Fixed bug GH-22063 (Stream filter chain UAF on self-removal during
      callback). (Ilia Alshanetsky)
    . Fixed leaks and data loss when user filter leaves buckets on input
diff --cc ext/standard/ftp_fopen_wrapper.c
index da6562f1d14,cacfa9e1013..c1341ff6266
--- a/ext/standard/ftp_fopen_wrapper.c
+++ b/ext/standard/ftp_fopen_wrapper.c
@@@ -247,24 -261,18 +265,18 @@@ static php_stream *php_ftp_fopen_connec
  #endif
  	}

- #define PHP_FTP_CNTRL_CHK(val, val_len, err_msg) {	\
- 	unsigned char *s = (unsigned char *) val, *e = (unsigned char *) s + val_len;	\
- 	while (s < e) {	\
- 		if (iscntrl((unsigned char)*s)) {	\
- 			php_stream_wrapper_log_warn(wrapper, context, options, AuthFailed, err_msg, val);	\
- 			goto connect_errexit;	\
- 		}	\
- 		s++;	\
- 	}	\
- }
-
  	/* send the user name */
  	if (resource->user != NULL) {
- 		ZSTR_LEN(resource->user) = php_raw_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user));
+ 		zend_string *user = php_ftp_url_decode(resource->user);

- 		PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user), "Invalid login %s")
+ 		if (php_ftp_has_cntrl(user)) {
 -			php_stream_wrapper_log_error(wrapper, options, "Invalid login %s", ZSTR_VAL(user));
++			php_stream_wrapper_log_warn(wrapper, context, options, AuthFailed, "Invalid login %s", ZSTR_VAL(user));
+ 			zend_string_efree(user);
+ 			goto connect_errexit;
+ 		}

- 		php_stream_printf(stream, "USER %pS\r\n", resource->user);
 -		php_stream_printf(stream, "USER %s\r\n", ZSTR_VAL(user));
++		php_stream_printf(stream, "USER %pS\r\n", user);
+ 		zend_string_efree(user);
  	} else {
  		php_stream_write_string(stream, "USER anonymous\r\n");
  	}
@@@ -277,11 -285,16 +289,16 @@@
  		php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, tmp_line, 0);

  		if (resource->password != NULL) {
- 			ZSTR_LEN(resource->password) = php_raw_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password));
+ 			zend_string *password = php_ftp_url_decode(resource->password);

- 			PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password), "Invalid password %s")
+ 			if (php_ftp_has_cntrl(password)) {
 -				php_stream_wrapper_log_error(wrapper, options, "Invalid password %s", ZSTR_VAL(password));
++				php_stream_wrapper_log_warn(wrapper, context, options, AuthFailed, "Invalid password %s", ZSTR_VAL(password));
+ 				zend_string_efree(password);
+ 				goto connect_errexit;
+ 			}

- 			php_stream_printf(stream, "PASS %pS\r\n", resource->password);
 -			php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(password));
++			php_stream_printf(stream, "PASS %pS\r\n", password);
+ 			zend_string_efree(password);
  		} else {
  			/* if the user has configured who they are,
  			   send that as the password */
diff --cc ext/standard/http_fopen_wrapper.c
index f1c36defb1e,ac91e4210eb..21a2be87571
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@@ -148,9 -158,35 +148,35 @@@ static bool check_has_header(const cha
  		}
  		s++;
  	}
 -	return 0;
 +	return false;
  }

+ /* The components of a php_uri may be interned or shared, so they are decoded into a new string. */
+ static zend_string *php_stream_http_url_decode(const zend_string *component)
+ {
+ 	zend_string *decoded = zend_string_alloc(ZSTR_LEN(component), false);
+ 	ZSTR_LEN(decoded) = php_url_decode_ex(ZSTR_VAL(decoded), ZSTR_VAL(component), ZSTR_LEN(component));
+ 	return decoded;
+ }
+
+ static bool php_stream_http_url_component_has_cntrl(const zend_string *component)
+ {
+ 	if (!component) {
+ 		return false;
+ 	}
+
+ 	zend_string *decoded = php_stream_http_url_decode(component);
+ 	bool has_cntrl = false;
+ 	for (size_t i = 0; i < ZSTR_LEN(decoded); i++) {
+ 		if (iscntrl((unsigned char) ZSTR_VAL(decoded)[i])) {
+ 			has_cntrl = true;
+ 			break;
+ 		}
+ 	}
+ 	zend_string_efree(decoded);
+ 	return has_cntrl;
+ }
+
  static zend_result php_stream_handle_proxy_authorization_header(const char *s, smart_str *header)
  {
  	const char *p;
@@@ -1157,11 -1175,9 +1173,10 @@@ finish
  				header_info.location = NULL;
  			}

 +			/* check for invalid redirection URLs */
  			php_uri *new_resource = php_uri_parse_to_struct(uri_parser, new_path, strlen(new_path), PHP_URI_COMPONENT_READ_MODE_RAW, true);
  			if (new_resource == NULL) {
- 				php_stream_wrapper_log_warn(wrapper, context, options, InvalidUrl,
- 					"Invalid redirect URL! %s", new_path);
 -				php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path);
++				php_stream_wrapper_log_warn(wrapper, context, options, InvalidUrl, "Invalid redirect URL! %s", new_path);
  				efree(new_path);
  				goto out;
  			}
@@@ -1177,27 -1193,15 +1192,15 @@@
  			php_uri_struct_free(resource);
  			resource = new_resource;

- #define CHECK_FOR_CNTRL_CHARS(val) { \
- 	if (val) { \
- 		unsigned char *s, *e; \
- 		ZSTR_LEN(val) = php_url_decode(ZSTR_VAL(val), ZSTR_LEN(val)); \
- 		s = (unsigned char*)ZSTR_VAL(val); e = s + ZSTR_LEN(val); \
- 		while (s < e) { \
- 			if (iscntrl(*s)) { \
- 				php_stream_wrapper_log_warn(wrapper, context, options, InvalidUrl, \
- 					"Invalid redirect URL! %s", new_path); \
- 				efree(new_path); \
- 				goto out; \
- 			} \
- 			s++; \
- 		} \
- 	} \
- }
  			/* check for control characters in login, password & path */
  			if (strncasecmp(new_path, "http://", sizeof("http://") - 1) || strncasecmp(new_path, "https://", sizeof("https://") - 1)) {
- 				CHECK_FOR_CNTRL_CHARS(resource->user);
- 				CHECK_FOR_CNTRL_CHARS(resource->password);
- 				CHECK_FOR_CNTRL_CHARS(resource->path);
+ 				if (php_stream_http_url_component_has_cntrl(resource->user)
+ 					|| php_stream_http_url_component_has_cntrl(resource->password)
+ 					|| php_stream_http_url_component_has_cntrl(resource->path)) {
 -					php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path);
++					php_stream_wrapper_log_warn(wrapper, context, options, InvalidUrl, "Invalid redirect URL! %s", new_path);
+ 					efree(new_path);
+ 					goto out;
+ 				}
  			}
  			int new_flags = HTTP_WRAPPER_REDIRECTED | (flags & HTTP_WRAPPER_STRIP_AUTH);
  			if (!same_origin) {