Commit d1d144e44bd for php.net

commit d1d144e44bd3a23c7f80c6d29dd77d4d43e089a0
Author: Sjoerd Langkemper <sjoerd-github@linuxonly.nl>
Date:   Fri Jul 10 14:37:32 2026 +0200

    Throw an error on null byte in curl array options (#22662)

    This is already done for string options in php_curl_option_str
    (see bug #68089). Similarly, array arguments should also not contain
    null bytes, so we throw the same error for these.

    This was extracted from https://github.com/php/php-src/pull/22651, as
    this seems less controversial.

    Using null bytes is clearly not supported and curl just throws away
    everything after the null byte, so
    https://wiki.php.net/rfc/policy-exempt-type-value-error-bc-policy seems
    to apply here.

diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 1dff14b7c8c..09c7009068b 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -2030,42 +2030,42 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
 			HashTable *ph;
 			zend_string *val, *tmp_val;
 			struct curl_slist *slist = NULL;
+			const char *name = NULL;

-			if (Z_TYPE_P(zvalue) != IS_ARRAY) {
-				const char *name = NULL;
-				switch (option) {
-					case CURLOPT_HTTPHEADER:
-						name = "CURLOPT_HTTPHEADER";
-						break;
-					case CURLOPT_QUOTE:
-						name = "CURLOPT_QUOTE";
-						break;
-					case CURLOPT_HTTP200ALIASES:
-						name = "CURLOPT_HTTP200ALIASES";
-						break;
-					case CURLOPT_POSTQUOTE:
-						name = "CURLOPT_POSTQUOTE";
-						break;
-					case CURLOPT_PREQUOTE:
-						name = "CURLOPT_PREQUOTE";
-						break;
-					case CURLOPT_TELNETOPTIONS:
-						name = "CURLOPT_TELNETOPTIONS";
-						break;
-					case CURLOPT_MAIL_RCPT:
-						name = "CURLOPT_MAIL_RCPT";
-						break;
-					case CURLOPT_RESOLVE:
-						name = "CURLOPT_RESOLVE";
-						break;
-					case CURLOPT_PROXYHEADER:
-						name = "CURLOPT_PROXYHEADER";
-						break;
-					case CURLOPT_CONNECT_TO:
-						name = "CURLOPT_CONNECT_TO";
-						break;
-				}
+			switch (option) {
+				case CURLOPT_HTTPHEADER:
+					name = "CURLOPT_HTTPHEADER";
+					break;
+				case CURLOPT_QUOTE:
+					name = "CURLOPT_QUOTE";
+					break;
+				case CURLOPT_HTTP200ALIASES:
+					name = "CURLOPT_HTTP200ALIASES";
+					break;
+				case CURLOPT_POSTQUOTE:
+					name = "CURLOPT_POSTQUOTE";
+					break;
+				case CURLOPT_PREQUOTE:
+					name = "CURLOPT_PREQUOTE";
+					break;
+				case CURLOPT_TELNETOPTIONS:
+					name = "CURLOPT_TELNETOPTIONS";
+					break;
+				case CURLOPT_MAIL_RCPT:
+					name = "CURLOPT_MAIL_RCPT";
+					break;
+				case CURLOPT_RESOLVE:
+					name = "CURLOPT_RESOLVE";
+					break;
+				case CURLOPT_PROXYHEADER:
+					name = "CURLOPT_PROXYHEADER";
+					break;
+				case CURLOPT_CONNECT_TO:
+					name = "CURLOPT_CONNECT_TO";
+					break;
+			}

+			if (Z_TYPE_P(zvalue) != IS_ARRAY) {
 				zend_type_error("%s(): The %s option must have an array value", get_active_function_name(), name);
 				return FAILURE;
 			}
@@ -2074,6 +2074,14 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
 			ZEND_HASH_FOREACH_VAL(ph, current) {
 				ZVAL_DEREF(current);
 				val = zval_get_tmp_string(current, &tmp_val);
+
+				if (zend_str_has_nul_byte(val)) {
+					curl_slist_free_all(slist);
+					zend_tmp_string_release(tmp_val);
+					zend_value_error("%s(): cURL option %s must not contain any null bytes", get_active_function_name(), name);
+					return FAILURE;
+				}
+
 				struct curl_slist *new_slist = curl_slist_append(slist, ZSTR_VAL(val));
 				zend_tmp_string_release(tmp_val);
 				if (!new_slist) {
diff --git a/ext/curl/tests/curl_setopt_error_nul_byte.phpt b/ext/curl/tests/curl_setopt_error_nul_byte.phpt
new file mode 100644
index 00000000000..23be973b82c
--- /dev/null
+++ b/ext/curl/tests/curl_setopt_error_nul_byte.phpt
@@ -0,0 +1,52 @@
+--TEST--
+curl_setopt() throws ValueError for NUL bytes in lists
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+
+$ch = curl_init();
+
+$list_options = [
+    "CURLOPT_HTTP200ALIASES",
+    "CURLOPT_HTTPHEADER",
+    "CURLOPT_POSTQUOTE",
+    "CURLOPT_PREQUOTE",
+    "CURLOPT_QUOTE",
+    "CURLOPT_TELNETOPTIONS",
+    "CURLOPT_MAIL_RCPT",
+    "CURLOPT_RESOLVE",
+    "CURLOPT_PROXYHEADER",
+    "CURLOPT_CONNECT_TO",
+];
+
+foreach ($list_options as $option) {
+    try {
+        curl_setopt($ch, constant($option), ["Something: foo\0bar"]);
+    } catch (ValueError $exception) {
+        echo $option . ": " . $exception->getMessage() . "\n\n";
+    }
+}
+
+$ch = null;
+?>
+--EXPECT--
+CURLOPT_HTTP200ALIASES: curl_setopt(): cURL option CURLOPT_HTTP200ALIASES must not contain any null bytes
+
+CURLOPT_HTTPHEADER: curl_setopt(): cURL option CURLOPT_HTTPHEADER must not contain any null bytes
+
+CURLOPT_POSTQUOTE: curl_setopt(): cURL option CURLOPT_POSTQUOTE must not contain any null bytes
+
+CURLOPT_PREQUOTE: curl_setopt(): cURL option CURLOPT_PREQUOTE must not contain any null bytes
+
+CURLOPT_QUOTE: curl_setopt(): cURL option CURLOPT_QUOTE must not contain any null bytes
+
+CURLOPT_TELNETOPTIONS: curl_setopt(): cURL option CURLOPT_TELNETOPTIONS must not contain any null bytes
+
+CURLOPT_MAIL_RCPT: curl_setopt(): cURL option CURLOPT_MAIL_RCPT must not contain any null bytes
+
+CURLOPT_RESOLVE: curl_setopt(): cURL option CURLOPT_RESOLVE must not contain any null bytes
+
+CURLOPT_PROXYHEADER: curl_setopt(): cURL option CURLOPT_PROXYHEADER must not contain any null bytes
+
+CURLOPT_CONNECT_TO: curl_setopt(): cURL option CURLOPT_CONNECT_TO must not contain any null bytes