Commit 1e8e679dd4a for php

commit 1e8e679dd4ad00fc7e99885cd2668823f9524482
Author: Daniel Scherzer <daniel.e.scherzer+phpf@gmail.com>
Date:   Tue Sep 15 19:25:29 2026 -0700

    GH-23686: Add regression tests for sensitive headers

diff --git a/ext/soap/tests/gh23686/check_headers.inc b/ext/soap/tests/gh23686/check_headers.inc
new file mode 100644
index 00000000000..f698d74ff97
--- /dev/null
+++ b/ext/soap/tests/gh23686/check_headers.inc
@@ -0,0 +1,101 @@
+<?php
+
+include __DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc";
+
+function start_server(string $code, ?string $entry_point) {
+	$args = [
+		"-d",
+		"extension_dir=" . ini_get("extension_dir"),
+		"-d",
+		"extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX,
+	];
+	if (php_ini_loaded_file()) {
+		$args[] = "-c";
+		$args[] = php_ini_loaded_file();
+	}
+	return @php_cli_server_start($code, $entry_point, $args);
+}
+
+/**
+ * When an <import> switches to a different server, check the headers that the
+ * second server recieves
+ */
+function check_headers_for_import(string $logs_path, $context, bool $verbose_failure = true) {
+	$code = file_get_contents(__DIR__ . "/soap-respond-with-headers.inc");
+	$code = substr($code, strpos($code, "<?php") + strlen("<?php"));
+	$code = str_replace('"{LOG_FILE}"', var_export($logs_path, true), $code);
+
+	$logging_server = start_server($code, null);
+
+	$wsdl = file_get_contents(__DIR__ . "/wsdl-with-import.wsdl");
+	$wsdl = str_replace("{TARGET_PORT}", $logging_server->port, $wsdl);
+
+	// We cannot just load the SoapClient directly from the WSDL because
+	// otherwise the credentials are not stripped because we are not switching
+	// to another host in the <import>.
+	$server_code = "echo <<<END\n$wsdl\nEND;";
+	$wsdl_server = start_server($server_code, "router.php");
+	$wsdl_server_port = $wsdl_server->port;
+
+	foreach ([SOAP_1_1, SOAP_1_2] as $version) {
+		try {
+			$client = new SoapClient("http://localhost:$wsdl_server_port", [
+				'soap_version' => $version,
+				'stream_context' => $context,
+			]);
+		} catch (SoapFault $e) {
+			if ($verbose_failure) {
+				var_dump($e);
+				echo "--- Raw Server Response Body ---\n";
+				var_dump(file_get_contents("http://localhost:$wsdl_server_port", false, $context));
+			} else {
+				echo get_class($e) . ': ' . $e->getMessage() . "\n";
+			}
+		}
+
+		var_dump(@file_get_contents($logs_path));
+	}
+}
+
+/**
+ * When an <import> leads to a <schema> on a different server, check the
+ * headers that the *third* server recieves
+ */
+function check_headers_for_schema(string $logs_path, $context, bool $verbose_failure = true) {
+	$code = file_get_contents(__DIR__ . "/schema-respond-with-headers.inc");
+	$code = substr($code, strpos($code, "<?php") + strlen("<?php"));
+	$code = str_replace('"{LOG_FILE}"', var_export($logs_path, true), $code);
+
+	$logging_server = start_server($code, null);
+
+	$schema_wsdl = file_get_contents(__DIR__ . "/wsdl-with-schema.wsdl");
+	$schema_wsdl = str_replace("{TARGET_PORT}", $logging_server->port, $schema_wsdl);
+	$schema_server_code = "echo <<<END\n$schema_wsdl\nEND;";
+	$schema_wsdl_server = start_server($schema_server_code, "schema.php");
+	$schema_wsdl_server_port = $schema_wsdl_server->port;
+
+	$import_wsdl = file_get_contents(__DIR__ . "/wsdl-with-import.wsdl");
+	$import_wsdl = str_replace("{TARGET_PORT}", $schema_wsdl_server_port, $import_wsdl);
+	$import_server_code = "echo <<<END\n$import_wsdl\nEND;";
+	$import_wsdl_server = start_server($import_server_code, "router.php");
+	$import_wsdl_server_port = $import_wsdl_server->port;
+
+	foreach ([SOAP_1_1, SOAP_1_2] as $version) {
+		try {
+			$client = new SoapClient("http://localhost:$import_wsdl_server_port", [
+				'soap_version' => $version,
+				'stream_context' => $context,
+			]);
+		} catch (SoapFault $e) {
+			if ($verbose_failure) {
+				var_dump($e);
+				echo "--- Raw Server Response Body ---\n";
+				var_dump(file_get_contents("http://localhost:$import_wsdl_server_port", false, $context));
+			} else {
+				echo get_class($e) . ': ' . $e->getMessage() . "\n";
+			}
+		}
+
+		var_dump(@file_get_contents($logs_path));
+	}
+}
diff --git a/ext/soap/tests/gh23686/header-from-stringable.phpt b/ext/soap/tests/gh23686/header-from-stringable.phpt
new file mode 100644
index 00000000000..4e2d7f02aa9
--- /dev/null
+++ b/ext/soap/tests/gh23686/header-from-stringable.phpt
@@ -0,0 +1,108 @@
+--TEST--
+GH-23686: Confirm that headers set with stringable objects are ignored
+--DESCRIPTION--
+If these objects were converted to strings and the headers used, then they might
+need to be redacted, but php_stream_url_wrap_http_ex() only cares about strings
+and string elements in arrays as of PHP 8.6. This test means that if that were
+to ever change, this test would fail and developers would know to update
+the redaction logic in the soap extension.
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/header-from-stringable-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/header-from-stringable-logs.txt";
+
+class StringWrapper {
+	public function __construct(private readonly string $val) {}
+	public function __toString() { return $this->val; }
+}
+
+// Headers reach sdl_set_uri_credentials() as a string
+$context = stream_context_create([
+	'http' => ['header' => [
+		new StringWrapper("Authorization: Foo"),
+		new StringWrapper("Bar: Baz"),
+		"X-non-object: value",
+	]],
+]);
+
+echo "Headers are in a string:\n";
+check_headers_for_import(LOGS_PATH, $context);
+check_headers_for_schema(LOGS_PATH, $context);
+
+// Headers reach sdl_set_uri_credentials() as an array
+$context = stream_context_create([
+	'http' => [
+		// having a protocol here means that SOAP won't add it itself
+		'protocol_version' => 1.1,
+		'header' => [
+			new StringWrapper("Authorization: Foo"),
+			new StringWrapper("Bar: Baz"),
+			"X-non-object: value",
+		]
+	],
+]);
+
+echo "Headers are in an array:\n";
+check_headers_for_import(LOGS_PATH, $context);
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+Headers are in a string:
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+Headers are in an array:
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'X-non-object' => 'value',
+)"
diff --git a/ext/soap/tests/gh23686/header-just-lf-separator.phpt b/ext/soap/tests/gh23686/header-just-lf-separator.phpt
new file mode 100644
index 00000000000..11a3a688cdf
--- /dev/null
+++ b/ext/soap/tests/gh23686/header-just-lf-separator.phpt
@@ -0,0 +1,93 @@
+--TEST--
+GH-23686: Confirm that headers separated with just LF are separated
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/header-just-lf-separator-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/header-just-lf-separator-logs.txt";
+
+// Headers reach sdl_set_uri_credentials() as a string
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Basic\nFoo: bar"],
+]);
+
+echo "Headers are in a string:\n";
+check_headers_for_import(LOGS_PATH, $context);
+check_headers_for_schema(LOGS_PATH, $context);
+
+// Headers reach sdl_set_uri_credentials() as an array
+$context = stream_context_create([
+	'http' => [
+		// having a protocol here means that SOAP won't add it itself
+		'protocol_version' => 1.1,
+		'header' => "Authorization: Basic\nFoo: bar"
+	],
+]);
+
+echo "Headers are in an array:\n";
+check_headers_for_import(LOGS_PATH, $context);
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+Headers are in a string:
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Foo' => 'bar',
+)"
+Headers are in an array:
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Basic',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Basic',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Basic',
+  'Foo' => 'bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Basic',
+  'Foo' => 'bar',
+)"
diff --git a/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt b/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt
new file mode 100644
index 00000000000..fa27f1c87cf
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Basic` but lowercase (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-authorization-basic-lowercase-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-authorization-basic-lowercase-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "authorization: Basic foo bar"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'authorization' => 'Basic foo bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'authorization' => 'Basic foo bar',
+)"
diff --git a/ext/soap/tests/gh23686/import-authorization-basic.phpt b/ext/soap/tests/gh23686/import-authorization-basic.phpt
new file mode 100644
index 00000000000..aa04078851f
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-authorization-basic.phpt
@@ -0,0 +1,39 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Basic` (which was stripped)  (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-authorization-basic-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-authorization-basic-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Basic foo bar"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+)"
diff --git a/ext/soap/tests/gh23686/import-authorization-bearer.phpt b/ext/soap/tests/gh23686/import-authorization-bearer.phpt
new file mode 100644
index 00000000000..8eb87a52653
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-authorization-bearer.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Bearer`  (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-authorization-bearer-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-authorization-bearer-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Bearer secret-token"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Bearer secret-token',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Bearer secret-token',
+)"
diff --git a/ext/soap/tests/gh23686/import-authorization-digest.phpt b/ext/soap/tests/gh23686/import-authorization-digest.phpt
new file mode 100644
index 00000000000..d74058bb4cf
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-authorization-digest.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Digest` (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-authorization-digest-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-authorization-digest-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Digest secret-token"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Digest secret-token',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Digest secret-token',
+)"
diff --git a/ext/soap/tests/gh23686/import-cookies.phpt b/ext/soap/tests/gh23686/import-cookies.phpt
new file mode 100644
index 00000000000..d7b47daa556
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-cookies.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for cookie headers (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-cookies-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-cookies-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Cookie: foo=bar"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Cookie' => 'foo=bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Cookie' => 'foo=bar',
+)"
diff --git a/ext/soap/tests/gh23686/import-proxy-authorization.phpt b/ext/soap/tests/gh23686/import-proxy-authorization.phpt
new file mode 100644
index 00000000000..e0b0bb0a457
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-proxy-authorization.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for proxy authorization (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-proxy-authorization-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-proxy-authorization-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Proxy-Authorization: FooBar"],
+]);
+
+check_headers_for_import(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Proxy-Authorization' => 'FooBar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Proxy-Authorization' => 'FooBar',
+)"
diff --git a/ext/soap/tests/gh23686/import-repeated-authorization.phpt b/ext/soap/tests/gh23686/import-repeated-authorization.phpt
new file mode 100644
index 00000000000..3b7d5c16726
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-repeated-authorization.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-23686: Regression tests for repeated authorization headers (which doesn't work) (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-repeated-authorization-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-repeated-authorization-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Authorization: Foo",
+		"aUTHORIZATION: Bar",
+	]],
+]);
+
+check_headers_for_import(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/import-repeated-cookies.phpt b/ext/soap/tests/gh23686/import-repeated-cookies.phpt
new file mode 100644
index 00000000000..ebe67c075f3
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-repeated-cookies.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-23686: Regression tests for repeated cookies headers (which doesn't work) (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-repeated-cookies-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-repeated-cookies-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Cookie: Foo",
+		"cOOKIE: Bar",
+	]],
+]);
+
+check_headers_for_import(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt
new file mode 100644
index 00000000000..edeb149e4ca
--- /dev/null
+++ b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-23686: Regression tests for repeated proxy authorization headers (which doesn't work) (`<import>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/import-repeated-proxy-auth-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/import-repeated-proxy-auth-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Proxy-Authorization: Foo",
+		"pROXY-aUTHORIZATION: Bar",
+	]],
+]);
+
+check_headers_for_import(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+SoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost:%d/index.php' : failed to load external entity "http://localhost:%d/index.php"
+
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt b/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt
new file mode 100644
index 00000000000..dedee8b85c4
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Basic` but lowercase (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-authorization-basic-lowercase-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-authorization-basic-lowercase-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "authorization: Basic foo bar"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'authorization' => 'Basic foo bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'authorization' => 'Basic foo bar',
+)"
diff --git a/ext/soap/tests/gh23686/schema-authorization-basic.phpt b/ext/soap/tests/gh23686/schema-authorization-basic.phpt
new file mode 100644
index 00000000000..22a0ecb311a
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-authorization-basic.phpt
@@ -0,0 +1,39 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Basic` (which was stripped)  (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-authorization-basic-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-authorization-basic-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Basic foo bar"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+)"
diff --git a/ext/soap/tests/gh23686/schema-authorization-bearer.phpt b/ext/soap/tests/gh23686/schema-authorization-bearer.phpt
new file mode 100644
index 00000000000..be786c8f6b1
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-authorization-bearer.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Bearer`  (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-authorization-bearer-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-authorization-bearer-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Bearer secret-token"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Bearer secret-token',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Bearer secret-token',
+)"
diff --git a/ext/soap/tests/gh23686/schema-authorization-digest.phpt b/ext/soap/tests/gh23686/schema-authorization-digest.phpt
new file mode 100644
index 00000000000..ad48118d6c0
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-authorization-digest.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for `Authorization: Digest` (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-authorization-digest-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-authorization-digest-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Authorization: Digest secret-token"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Digest secret-token',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Authorization' => 'Digest secret-token',
+)"
diff --git a/ext/soap/tests/gh23686/schema-cookies.phpt b/ext/soap/tests/gh23686/schema-cookies.phpt
new file mode 100644
index 00000000000..ff33d8ace5d
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-cookies.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for cookie headers (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-cookies-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-cookies-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Cookie: foo=bar"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Cookie' => 'foo=bar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Cookie' => 'foo=bar',
+)"
diff --git a/ext/soap/tests/gh23686/schema-proxy-authorization.phpt b/ext/soap/tests/gh23686/schema-proxy-authorization.phpt
new file mode 100644
index 00000000000..2f06bc9e735
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-proxy-authorization.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23686: Regression test for proxy authorization (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-proxy-authorization-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-proxy-authorization-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => "Proxy-Authorization: FooBar"],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context);
+
+?>
+--EXPECTF--
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Proxy-Authorization' => 'FooBar',
+)"
+string(%d) "array (
+  'Host' => 'localhost:%d',
+  'Connection' => 'close',
+  'Proxy-Authorization' => 'FooBar',
+)"
diff --git a/ext/soap/tests/gh23686/schema-repeated-authorization.phpt b/ext/soap/tests/gh23686/schema-repeated-authorization.phpt
new file mode 100644
index 00000000000..e99c7c9c7d9
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-repeated-authorization.phpt
@@ -0,0 +1,42 @@
+--TEST--
+GH-23686: Regression tests for repeated authorization headers (which doesn't work) (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-repeated-authorization-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-repeated-authorization-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Authorization: Foo",
+		"aUTHORIZATION: Bar",
+	]],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/schema-repeated-cookies.phpt b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt
new file mode 100644
index 00000000000..76cc2b95a6d
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt
@@ -0,0 +1,42 @@
+--TEST--
+GH-23686: Regression tests for repeated cookies headers (which doesn't work) (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-repeated-cookies-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-repeated-cookies-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Cookie: Foo",
+		"cOOKIE: Bar",
+	]],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt
new file mode 100644
index 00000000000..9e6b2e6768b
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt
@@ -0,0 +1,42 @@
+--TEST--
+GH-23686: Regression tests for repeated proxy authorization headers (which doesn't work) (`<schema>`)
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=0
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/schema-repeated-proxy-auth-logs.txt");
+?>
+--SKIPIF--
+<?php
+if (!file_exists(__DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc")) {
+	echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+}
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/check_headers.inc";
+
+const LOGS_PATH = __DIR__ . "/schema-repeated-proxy-auth-logs.txt";
+
+$context = stream_context_create([
+	'http' => ['header' => [
+		"Proxy-Authorization: Foo",
+		"pROXY-aUTHORIZATION: Bar",
+	]],
+]);
+
+check_headers_for_schema(LOGS_PATH, $context, false);
+
+?>
+--EXPECTF--
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+SoapFault: SOAP-ERROR: Parsing Schema: can't import schema from 'http://localhost:%d/index.php'
+bool(false)
+Server exited with non-zero status: 1
+
+Warning: file_get_contents(): Failed to open stream: No such file or directory in %s on line %d
+Server output:
diff --git a/ext/soap/tests/gh23686/schema-respond-with-headers.inc b/ext/soap/tests/gh23686/schema-respond-with-headers.inc
new file mode 100644
index 00000000000..f970b3d84e1
--- /dev/null
+++ b/ext/soap/tests/gh23686/schema-respond-with-headers.inc
@@ -0,0 +1,9 @@
+<?php
+
+file_put_contents("{LOG_FILE}", var_export(getallheaders(), true));
+
+echo <<<END
+<?xml version="1.0" encoding="utf-8"?>
+<schema>
+</schema>
+END;
diff --git a/ext/soap/tests/gh23686/soap-respond-with-headers.inc b/ext/soap/tests/gh23686/soap-respond-with-headers.inc
new file mode 100644
index 00000000000..0cf44d3dea0
--- /dev/null
+++ b/ext/soap/tests/gh23686/soap-respond-with-headers.inc
@@ -0,0 +1,16 @@
+<?php
+
+file_put_contents("{LOG_FILE}", var_export(getallheaders(), true));
+
+echo <<<END
+<?xml version="1.0" encoding="utf-8"?>
+<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/">
+  <portType name="soapServicePortType" />
+  <binding name="soapServiceBinding" type="typens:soapServicePortType" />
+  <service name="soapServiceService">
+    <port name="soapServicePort" binding="typens:soapServiceBinding">
+      <soap:address location="###PHP_SELF###" />
+    </port>
+  </service>
+</definitions>
+END;
diff --git a/ext/soap/tests/gh23686/wsdl-with-import.wsdl b/ext/soap/tests/gh23686/wsdl-with-import.wsdl
new file mode 100644
index 00000000000..ff898898c05
--- /dev/null
+++ b/ext/soap/tests/gh23686/wsdl-with-import.wsdl
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<definitions
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/"
+>
+  <import location="http://localhost:{TARGET_PORT}/index.php"/>
+</definitions>
diff --git a/ext/soap/tests/gh23686/wsdl-with-schema.wsdl b/ext/soap/tests/gh23686/wsdl-with-schema.wsdl
new file mode 100644
index 00000000000..f74bfcf63ba
--- /dev/null
+++ b/ext/soap/tests/gh23686/wsdl-with-schema.wsdl
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<definitions
+	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+	xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+	xmlns="http://schemas.xmlsoap.org/wsdl/"
+>
+<types>
+	<xsd:schema>
+		<xsd:include schemaLocation="http://localhost:{TARGET_PORT}/index.php"/>
+	</xsd:schema>
+</types>
+<portType name="soapServicePortType" />
+<binding name="soapServiceBinding" type="typens:soapServicePortType" />
+<service name="soapServiceService">
+	<port name="soapServicePort" binding="typens:soapServiceBinding">
+		<soap:address location="###PHP_SELF###" />
+	</port>
+</service>
+</definitions>
diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc
index ec370753573..6e5449f5eb1 100644
--- a/sapi/cli/tests/php_cli_server.inc
+++ b/sapi/cli/tests/php_cli_server.inc
@@ -6,6 +6,7 @@ class CliServerInfo {
         public string $docRoot,
         public $processHandle,
         public $outputFile,
+        public int $port,
     ) {}
 }

@@ -119,7 +120,7 @@ function php_cli_server_start(
     define("PHP_CLI_SERVER_PORT", $port);
     define("PHP_CLI_SERVER_ADDRESS", PHP_CLI_SERVER_HOSTNAME.":".PHP_CLI_SERVER_PORT);

-    return new CliServerInfo($doc_root, $handle, $output_file);
+    return new CliServerInfo($doc_root, $handle, $output_file, $port);
 }

 function php_cli_server_connect() {