From 1e8e679dd4ad00fc7e99885cd2668823f9524482 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 15 Sep 2026 19:25:29 -0700 Subject: [PATCH 1/4] GH-23686: Add regression tests for sensitive headers --- ext/soap/tests/gh23686/check_headers.inc | 101 ++++++++++++++++ .../tests/gh23686/header-from-stringable.phpt | 108 ++++++++++++++++++ .../gh23686/header-just-lf-separator.phpt | 93 +++++++++++++++ .../import-authorization-basic-lowercase.phpt | 41 +++++++ .../gh23686/import-authorization-basic.phpt | 39 +++++++ .../gh23686/import-authorization-bearer.phpt | 41 +++++++ .../gh23686/import-authorization-digest.phpt | 41 +++++++ ext/soap/tests/gh23686/import-cookies.phpt | 41 +++++++ .../gh23686/import-proxy-authorization.phpt | 41 +++++++ .../import-repeated-authorization.phpt | 44 +++++++ .../gh23686/import-repeated-cookies.phpt | 44 +++++++ .../gh23686/import-repeated-proxy-auth.phpt | 44 +++++++ .../schema-authorization-basic-lowercase.phpt | 41 +++++++ .../gh23686/schema-authorization-basic.phpt | 39 +++++++ .../gh23686/schema-authorization-bearer.phpt | 41 +++++++ .../gh23686/schema-authorization-digest.phpt | 41 +++++++ ext/soap/tests/gh23686/schema-cookies.phpt | 41 +++++++ .../gh23686/schema-proxy-authorization.phpt | 41 +++++++ .../schema-repeated-authorization.phpt | 42 +++++++ .../gh23686/schema-repeated-cookies.phpt | 42 +++++++ .../gh23686/schema-repeated-proxy-auth.phpt | 42 +++++++ .../gh23686/schema-respond-with-headers.inc | 9 ++ .../gh23686/soap-respond-with-headers.inc | 16 +++ ext/soap/tests/gh23686/wsdl-with-import.wsdl | 7 ++ ext/soap/tests/gh23686/wsdl-with-schema.wsdl | 19 +++ sapi/cli/tests/php_cli_server.inc | 3 +- 26 files changed, 1101 insertions(+), 1 deletion(-) create mode 100644 ext/soap/tests/gh23686/check_headers.inc create mode 100644 ext/soap/tests/gh23686/header-from-stringable.phpt create mode 100644 ext/soap/tests/gh23686/header-just-lf-separator.phpt create mode 100644 ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt create mode 100644 ext/soap/tests/gh23686/import-authorization-basic.phpt create mode 100644 ext/soap/tests/gh23686/import-authorization-bearer.phpt create mode 100644 ext/soap/tests/gh23686/import-authorization-digest.phpt create mode 100644 ext/soap/tests/gh23686/import-cookies.phpt create mode 100644 ext/soap/tests/gh23686/import-proxy-authorization.phpt create mode 100644 ext/soap/tests/gh23686/import-repeated-authorization.phpt create mode 100644 ext/soap/tests/gh23686/import-repeated-cookies.phpt create mode 100644 ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt create mode 100644 ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt create mode 100644 ext/soap/tests/gh23686/schema-authorization-basic.phpt create mode 100644 ext/soap/tests/gh23686/schema-authorization-bearer.phpt create mode 100644 ext/soap/tests/gh23686/schema-authorization-digest.phpt create mode 100644 ext/soap/tests/gh23686/schema-cookies.phpt create mode 100644 ext/soap/tests/gh23686/schema-proxy-authorization.phpt create mode 100644 ext/soap/tests/gh23686/schema-repeated-authorization.phpt create mode 100644 ext/soap/tests/gh23686/schema-repeated-cookies.phpt create mode 100644 ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt create mode 100644 ext/soap/tests/gh23686/schema-respond-with-headers.inc create mode 100644 ext/soap/tests/gh23686/soap-respond-with-headers.inc create mode 100644 ext/soap/tests/gh23686/wsdl-with-import.wsdl create mode 100644 ext/soap/tests/gh23686/wsdl-with-schema.wsdl diff --git a/ext/soap/tests/gh23686/check_headers.inc b/ext/soap/tests/gh23686/check_headers.inc new file mode 100644 index 000000000000..f698d74ff97a --- /dev/null +++ b/ext/soap/tests/gh23686/check_headers.inc @@ -0,0 +1,101 @@ + 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, "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 . + $server_code = "echo <<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 leads to a 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, "port, $schema_wsdl); + $schema_server_code = "echo <<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 <<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 000000000000..4e2d7f02aa9c --- /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-- + +--SKIPIF-- + +--FILE-- +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 000000000000..11a3a688cdf7 --- /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-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..fa27f1c87cff --- /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 (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..aa04078851fd --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..8eb87a526531 --- /dev/null +++ b/ext/soap/tests/gh23686/import-authorization-bearer.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for `Authorization: Bearer` (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..d74058bb4cf3 --- /dev/null +++ b/ext/soap/tests/gh23686/import-authorization-digest.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for `Authorization: Digest` (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..d7b47daa556b --- /dev/null +++ b/ext/soap/tests/gh23686/import-cookies.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for cookie headers (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..e0b0bb0a4579 --- /dev/null +++ b/ext/soap/tests/gh23686/import-proxy-authorization.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for proxy authorization (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..3b7d5c167269 --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..ebe67c075f36 --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..edeb149e4ca6 --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..dedee8b85c41 --- /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 (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..22a0ecb311ab --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..be786c8f6b16 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-authorization-bearer.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for `Authorization: Bearer` (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..ad48118d6c05 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-authorization-digest.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for `Authorization: Digest` (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..ff33d8ace5da --- /dev/null +++ b/ext/soap/tests/gh23686/schema-cookies.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for cookie headers (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..2f06bc9e7358 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-proxy-authorization.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: Regression test for proxy authorization (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..e99c7c9c7d97 --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..76cc2b95a6dd --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..9e6b2e6768b3 --- /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) (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['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 000000000000..f970b3d84e10 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-respond-with-headers.inc @@ -0,0 +1,9 @@ + + + +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 000000000000..0cf44d3dea0e --- /dev/null +++ b/ext/soap/tests/gh23686/soap-respond-with-headers.inc @@ -0,0 +1,16 @@ + + + + + + + + + + +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 000000000000..ff898898c05a --- /dev/null +++ b/ext/soap/tests/gh23686/wsdl-with-import.wsdl @@ -0,0 +1,7 @@ + + + + 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 000000000000..f74bfcf63ba6 --- /dev/null +++ b/ext/soap/tests/gh23686/wsdl-with-schema.wsdl @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc index ec370753573c..6e5449f5eb15 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() { From c1873412a51a20ea6366e2352004b6c8cd240ddb Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 15 Sep 2026 20:39:25 -0700 Subject: [PATCH 2/4] `sdl_set_uri_credentials()`: use early returns to improve readability --- ext/soap/php_sdl.c | 60 +++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c index 3ffa67cfb910..517f8a6afd2f 100644 --- a/ext/soap/php_sdl.c +++ b/ext/soap/php_sdl.c @@ -248,33 +248,39 @@ void sdl_set_uri_credentials(sdlCtx *ctx, char *uri) l2 -= 4; } } - if (l1 != l2 || memcmp(ctx->sdl->source, uri, l1) != 0) { - /* another server. clear authentication credentals */ - php_libxml_switch_context(NULL, &context); - php_libxml_switch_context(&context, NULL); - if (Z_TYPE(context) != IS_UNDEF) { - zval *context_ptr = &context; - ctx->context = php_stream_context_from_zval(context_ptr, 1); - - if (ctx->context && - (header = php_stream_context_get_option(ctx->context, "http", "header")) != NULL && - Z_TYPE_P(header) == IS_STRING) { - /* TODO: should support header as an array, but this code path is untested */ - s = strstr(Z_STRVAL_P(header), "Authorization: Basic"); - if (s && (s == Z_STRVAL_P(header) || *(s-1) == '\n' || *(s-1) == '\r')) { - char *rest = strstr(s, "\r\n"); - if (rest) { - zval new_header; - - rest += 2; - ZVAL_NEW_STR(&new_header, zend_string_alloc(Z_STRLEN_P(header) - (rest - s), 0)); - memcpy(Z_STRVAL(new_header), Z_STRVAL_P(header), s - Z_STRVAL_P(header)); - memcpy(Z_STRVAL(new_header) + (s - Z_STRVAL_P(header)), rest, Z_STRLEN_P(header) - (rest - Z_STRVAL_P(header)) + 1); - ZVAL_COPY(&ctx->old_header, header); - php_stream_context_set_option(ctx->context, "http", "header", &new_header); - zval_ptr_dtor(&new_header); - } - } + if (l1 == l2 && memcmp(ctx->sdl->source, uri, l1) == 0) { + return; + } + /* another server. clear authentication credentals */ + php_libxml_switch_context(NULL, &context); + php_libxml_switch_context(&context, NULL); + if (Z_TYPE(context) == IS_UNDEF) { + return; + } + zval *context_ptr = &context; + ctx->context = php_stream_context_from_zval(context_ptr, 1); + + if (!ctx->context) { + return; + } + + if ((header = php_stream_context_get_option(ctx->context, "http", "header")) != NULL && + Z_TYPE_P(header) == IS_STRING + ) { + /* TODO: should support header as an array, but this code path is untested */ + s = strstr(Z_STRVAL_P(header), "Authorization: Basic"); + if (s && (s == Z_STRVAL_P(header) || *(s-1) == '\n' || *(s-1) == '\r')) { + char *rest = strstr(s, "\r\n"); + if (rest) { + zval new_header; + + rest += 2; + ZVAL_NEW_STR(&new_header, zend_string_alloc(Z_STRLEN_P(header) - (rest - s), 0)); + memcpy(Z_STRVAL(new_header), Z_STRVAL_P(header), s - Z_STRVAL_P(header)); + memcpy(Z_STRVAL(new_header) + (s - Z_STRVAL_P(header)), rest, Z_STRLEN_P(header) - (rest - Z_STRVAL_P(header)) + 1); + ZVAL_COPY(&ctx->old_header, header); + php_stream_context_set_option(ctx->context, "http", "header", &new_header); + zval_ptr_dtor(&new_header); } } } From 1b6e96cb5ecc984e6f1d568ce578129a7f8359d9 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 15 Sep 2026 21:54:41 -0700 Subject: [PATCH 3/4] ext/soap: redact more sensitive headers --- NEWS | 4 + ext/soap/php_sdl.c | 182 ++++++++++++++++-- .../gh23686/header-just-lf-separator.phpt | 16 +- .../import-authorization-basic-lowercase.phpt | 2 - .../gh23686/import-authorization-bearer.phpt | 2 - .../gh23686/import-authorization-digest.phpt | 2 - ext/soap/tests/gh23686/import-cookies.phpt | 2 - .../gh23686/import-proxy-authorization.phpt | 2 - .../import-repeated-authorization.phpt | 20 +- .../gh23686/import-repeated-cookies.phpt | 20 +- .../gh23686/import-repeated-proxy-auth.phpt | 20 +- .../tests/gh23686/redact-array-headers.phpt | 74 +++++++ ext/soap/tests/gh23686/redact-not-first.phpt | 67 +++++++ .../schema-authorization-basic-lowercase.phpt | 2 - .../gh23686/schema-authorization-bearer.phpt | 2 - .../gh23686/schema-authorization-digest.phpt | 2 - ext/soap/tests/gh23686/schema-cookies.phpt | 2 - .../gh23686/schema-proxy-authorization.phpt | 2 - .../schema-repeated-authorization.phpt | 18 +- .../gh23686/schema-repeated-cookies.phpt | 18 +- .../gh23686/schema-repeated-proxy-auth.phpt | 18 +- 21 files changed, 375 insertions(+), 102 deletions(-) create mode 100644 ext/soap/tests/gh23686/redact-array-headers.phpt create mode 100644 ext/soap/tests/gh23686/redact-not-first.phpt diff --git a/NEWS b/NEWS index 6852f98646de..3a663fa341dc 100644 --- a/NEWS +++ b/NEWS @@ -95,6 +95,10 @@ PHP NEWS . Fixed session data loss in the files save handler when writing the session file fails after it was already truncated. (Ilia Alshanetsky) +- SOAP: + . Fixed bug GH-23686 (SoapClient only strips `Authorization: Basic` when a + WSDL imports from another host). (DanielEScherzer) + - Standard: . Fixed chown() and lchown() failing to resolve user names in ZTS builds when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky) diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c index 517f8a6afd2f..a2065cb0a815 100644 --- a/ext/soap/php_sdl.c +++ b/ext/soap/php_sdl.c @@ -198,6 +198,75 @@ static int is_wsdl_element(xmlNodePtr node) return 1; } +// Which headers need to be redacted +#define REDACT_NONE 0 +#define REDACT_AUTHORIZATION (1 << 0) +#define REDACT_PROXY_AUTH (1 << 1) +#define REDACT_COOKIE (1 << 2) + +static uint32_t header_needs_redaction(zend_string *header) { + uint32_t redaction = REDACT_NONE; + /* The various loops are so that if the first use of "authorization" is in + * some other header, we still redact the actual "authorization" header. + * + * We check if the character before the start of the header is a '\n' but + * not if the character before that was a `\r`; per RFC9112 while the + * separator is officially both (CRLF) recipients "MAY" recognize just a + * '\n' (LF) as a line terminator. A recipient "MUST" treat a bare '\r' (CR) + * as invalid or replace it, so a '\r' on its own does not start a new + * header. + */ + char *auth_start = strstr(ZSTR_VAL(header), "authorization:"); + while (auth_start && *auth_start) { + if (auth_start == ZSTR_VAL(header) || *(auth_start - 1) == '\n') { + redaction |= REDACT_AUTHORIZATION; + break; + } + auth_start = strstr(auth_start + 1, "authorization:"); + } + + char *proxy_start = strstr(ZSTR_VAL(header), "proxy-authorization:"); + while (proxy_start && *proxy_start) { + if (proxy_start == ZSTR_VAL(header) || *(proxy_start - 1) == '\n') { + redaction |= REDACT_PROXY_AUTH; + break; + } + proxy_start = strstr(proxy_start + 1, "proxy-authorization:"); + } + + char *cookie_start = strstr(ZSTR_VAL(header), "cookie:"); + while (cookie_start && *cookie_start) { + if (cookie_start == ZSTR_VAL(header) || *(cookie_start - 1) == '\n') { + redaction |= REDACT_COOKIE; + break; + } + cookie_start = strstr(cookie_start + 1, "cookie:"); + } + return redaction; +} + +static bool redact_header(char *header_start, uint32_t to_redact) { + /* When redacting, we don't remove the REDACT_* flag from to_redact, because + * a header might have been specified multiple times and we want to remove + * all of those uses. */ + if (to_redact & REDACT_AUTHORIZATION + && strncmp(header_start, "authorization:", strlen("authorization:")) == 0 + ) { + return true; + } + if (to_redact & REDACT_PROXY_AUTH + && strncmp(header_start, "proxy-authorization:", strlen("proxy-authorization:")) == 0 + ) { + return true; + } + if (to_redact & REDACT_COOKIE + && strncmp(header_start, "cookie:", strlen("cookie:")) == 0 + ) { + return true; + } + return false; +} + void sdl_set_uri_credentials(sdlCtx *ctx, char *uri) { char *s; @@ -264,26 +333,107 @@ void sdl_set_uri_credentials(sdlCtx *ctx, char *uri) return; } - if ((header = php_stream_context_get_option(ctx->context, "http", "header")) != NULL && - Z_TYPE_P(header) == IS_STRING - ) { - /* TODO: should support header as an array, but this code path is untested */ - s = strstr(Z_STRVAL_P(header), "Authorization: Basic"); - if (s && (s == Z_STRVAL_P(header) || *(s-1) == '\n' || *(s-1) == '\r')) { - char *rest = strstr(s, "\r\n"); - if (rest) { - zval new_header; + header = php_stream_context_get_option(ctx->context, "http", "header"); + if (header == NULL) { + return; + } - rest += 2; - ZVAL_NEW_STR(&new_header, zend_string_alloc(Z_STRLEN_P(header) - (rest - s), 0)); - memcpy(Z_STRVAL(new_header), Z_STRVAL_P(header), s - Z_STRVAL_P(header)); - memcpy(Z_STRVAL(new_header) + (s - Z_STRVAL_P(header)), rest, Z_STRLEN_P(header) - (rest - Z_STRVAL_P(header)) + 1); - ZVAL_COPY(&ctx->old_header, header); - php_stream_context_set_option(ctx->context, "http", "header", &new_header); - zval_ptr_dtor(&new_header); + zend_string *flat_headers; + if (Z_TYPE_P(header) == IS_STRING) { + flat_headers = zend_string_copy(Z_STR_P(header)); + } else if (Z_TYPE_P(header) == IS_ARRAY) { + smart_str buf = {0}; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(header), zval *value) { + if (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value)) { + if (buf.s) { + smart_str_appendl(&buf, "\r\n", 2); + } + smart_str_append(&buf, Z_STR_P(value)); + } + } ZEND_HASH_FOREACH_END(); + flat_headers = smart_str_extract(&buf); + } else { + return; + } + zend_string *lc_headers = zend_string_tolower(flat_headers); + // Fast path: no redaction needed + uint32_t redaction = header_needs_redaction(lc_headers); + if (redaction == REDACT_NONE) { + zend_string_release(lc_headers); + zend_string_release(flat_headers); + return; + } + // At least one of the headers needs to be stripped. To avoid repeated + // copying and case comparisons, we go through the headers one at a time. + // Will be allocated on the first header that should be kept. + zend_string *redacted = NULL; + // Pointer to the current read location + char *read_ptr = ZSTR_VAL(flat_headers); + // Offset from the start of the read location + size_t read_offset = 0; + // Offset from the start of the write location + size_t write_offset = 0; + + while (true) { + if (redact_header(ZSTR_VAL(lc_headers) + read_offset, redaction)) { + /* Current header should be skipped + * As with header_needs_redaction() we treat a '\n' as the end + * of the header even if not preceded by '\r'. */ + char *header_end = memchr(read_ptr, '\n', ZSTR_LEN(flat_headers) - read_offset); + if (header_end) { + // Skip the current header and the \n + size_t header_length = (header_end - read_ptr) + 1; + read_offset += header_length; + read_ptr += header_length; + } else { + // This is the end + break; + } + } else { + // Current header (from `read_offset`) should be kept + if (redacted == NULL) { + // This will over allocate the redacted header string but + // that isn't a big deal, the headers should be fairly short + redacted = zend_string_alloc(ZSTR_LEN(flat_headers), false); + } + char *header_end = memchr(read_ptr, '\n', ZSTR_LEN(flat_headers) - read_offset); + if (header_end) { + // Copy the current header and the \n + size_t header_length = (header_end - read_ptr) + 1; + memcpy(ZSTR_VAL(redacted) + write_offset, read_ptr, header_length); + // Update offsets + read_offset += header_length; + read_ptr += header_length; + write_offset += header_length; + } else { + // Copy until the end of the headers + size_t header_length = ZSTR_LEN(lc_headers) - read_offset; + memcpy(ZSTR_VAL(redacted) + write_offset, read_ptr, header_length); + write_offset += header_length; + break; } } } + zend_string_release(lc_headers); + zend_string_release(flat_headers); + + ZVAL_COPY(&ctx->old_header, header); + zval new_header; + if (redacted == NULL) { + // All headers were redacted + ZVAL_EMPTY_STRING(&new_header); + } else { + // Only some headers were redacted + // The zval_ptr_dtor call will ensure that the redacted headers + // are freed; + // Make sure that we don't read garbage + ZSTR_VAL(redacted)[write_offset] = '\0'; + ZSTR_LEN(redacted) = write_offset; + + ZVAL_STR(&new_header, redacted); + } + php_stream_context_set_option(ctx->context, "http", "header", &new_header); + zval_ptr_dtor(&new_header); } void sdl_restore_uri_credentials(sdlCtx *ctx) diff --git a/ext/soap/tests/gh23686/header-just-lf-separator.phpt b/ext/soap/tests/gh23686/header-just-lf-separator.phpt index 11a3a688cdf7..fc743aaed77b 100644 --- a/ext/soap/tests/gh23686/header-just-lf-separator.phpt +++ b/ext/soap/tests/gh23686/header-just-lf-separator.phpt @@ -23,7 +23,7 @@ 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"], + 'http' => ['header' => "Authorization: Basic\nFoo: bar\nProxy-Authorization: Something\nBaz: Qux\nCookie: Tasty"], ]); echo "Headers are in a string:\n"; @@ -35,7 +35,7 @@ $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" + 'header' => "Authorization: Basic\nFoo: bar\nProxy-Authorization: Something\nBaz: Qux\nCookie: Tasty" ], ]); @@ -50,44 +50,48 @@ string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', 'Foo' => 'bar', + 'Baz' => 'Qux', )" Headers are in an array: string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', - 'Authorization' => 'Basic', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', - 'Authorization' => 'Basic', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', - 'Authorization' => 'Basic', 'Foo' => 'bar', + 'Baz' => 'Qux', )" string(%d) "array ( 'Host' => 'localhost:%d', 'Connection' => 'close', - 'Authorization' => 'Basic', 'Foo' => 'bar', + 'Baz' => 'Qux', )" diff --git a/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt b/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt index fa27f1c87cff..7d0c694dc638 100644 --- a/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt +++ b/ext/soap/tests/gh23686/import-authorization-basic-lowercase.phpt @@ -32,10 +32,8 @@ check_headers_for_import(LOGS_PATH, $context); 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-bearer.phpt b/ext/soap/tests/gh23686/import-authorization-bearer.phpt index 8eb87a526531..e8445e8b3264 100644 --- a/ext/soap/tests/gh23686/import-authorization-bearer.phpt +++ b/ext/soap/tests/gh23686/import-authorization-bearer.phpt @@ -32,10 +32,8 @@ check_headers_for_import(LOGS_PATH, $context); 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 index d74058bb4cf3..ff3374b0c848 100644 --- a/ext/soap/tests/gh23686/import-authorization-digest.phpt +++ b/ext/soap/tests/gh23686/import-authorization-digest.phpt @@ -32,10 +32,8 @@ check_headers_for_import(LOGS_PATH, $context); 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 index d7b47daa556b..7871ca0874fe 100644 --- a/ext/soap/tests/gh23686/import-cookies.phpt +++ b/ext/soap/tests/gh23686/import-cookies.phpt @@ -32,10 +32,8 @@ check_headers_for_import(LOGS_PATH, $context); 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 index e0b0bb0a4579..f4356f312840 100644 --- a/ext/soap/tests/gh23686/import-proxy-authorization.phpt +++ b/ext/soap/tests/gh23686/import-proxy-authorization.phpt @@ -32,10 +32,8 @@ check_headers_for_import(LOGS_PATH, $context); 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 index 3b7d5c167269..7ec369b7b92b 100644 --- a/ext/soap/tests/gh23686/import-repeated-authorization.phpt +++ b/ext/soap/tests/gh23686/import-repeated-authorization.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated authorization headers (which doesn't work) (``) +GH-23686: Regression tests for repeated authorization headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,13 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" diff --git a/ext/soap/tests/gh23686/import-repeated-cookies.phpt b/ext/soap/tests/gh23686/import-repeated-cookies.phpt index ebe67c075f36..b6c4f2e1d1db 100644 --- a/ext/soap/tests/gh23686/import-repeated-cookies.phpt +++ b/ext/soap/tests/gh23686/import-repeated-cookies.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated cookies headers (which doesn't work) (``) +GH-23686: Regression tests for repeated cookies headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,13 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" diff --git a/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt index edeb149e4ca6..4628fcb7fe0b 100644 --- a/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt +++ b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated proxy authorization headers (which doesn't work) (``) +GH-23686: Regression tests for repeated proxy authorization headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,13 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" diff --git a/ext/soap/tests/gh23686/redact-array-headers.phpt b/ext/soap/tests/gh23686/redact-array-headers.phpt new file mode 100644 index 000000000000..ca27bf278a65 --- /dev/null +++ b/ext/soap/tests/gh23686/redact-array-headers.phpt @@ -0,0 +1,74 @@ +--TEST-- +GH-23686: Verify that a header is redacted when it reaches sdl_set_uri_credentials() as an array +--DESCRIPTION-- +Most of the time soap will add its own headers and in the process convert user headers +from an array to a string, check for the case when that does not happen +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + [ + // having a protocol here means that SOAP won't add it itself + 'protocol_version' => 1.1, + 'header' => [ + "X-Authorization: This should be kept", + "Authorization: This should be removed", + "X-Proxy-Authorization: This should also be kept", + "Proxy-Authorization: This should also be removed", + "X-Cookie: Last one to keep", + "Cookie: Last one to remove", + ] + ], +]); + +check_headers_for_import(LOGS_PATH, $context); +check_headers_for_schema(LOGS_PATH, $context); + +?> +--EXPECTF-- +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" diff --git a/ext/soap/tests/gh23686/redact-not-first.phpt b/ext/soap/tests/gh23686/redact-not-first.phpt new file mode 100644 index 000000000000..f85b80f47b56 --- /dev/null +++ b/ext/soap/tests/gh23686/redact-not-first.phpt @@ -0,0 +1,67 @@ +--TEST-- +GH-23686: Verify that a header is redacted when it isn't the first use of the name +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => [ + "X-Authorization: This should be kept", + "Authorization: This should be removed", + "X-Proxy-Authorization: This should also be kept", + "Proxy-Authorization: This should also be removed", + "X-Cookie: Last one to keep", + "Cookie: Last one to remove", + ]], +]); + +check_headers_for_import(LOGS_PATH, $context); +check_headers_for_schema(LOGS_PATH, $context); + +?> +--EXPECTF-- +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', + 'X-Authorization' => 'This should be kept', + 'X-Proxy-Authorization' => 'This should also be kept', + 'X-Cookie' => 'Last one to keep', +)" diff --git a/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt b/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt index dedee8b85c41..8a55c870d7af 100644 --- a/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt +++ b/ext/soap/tests/gh23686/schema-authorization-basic-lowercase.phpt @@ -32,10 +32,8 @@ check_headers_for_schema(LOGS_PATH, $context); 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-bearer.phpt b/ext/soap/tests/gh23686/schema-authorization-bearer.phpt index be786c8f6b16..8b575c74edd8 100644 --- a/ext/soap/tests/gh23686/schema-authorization-bearer.phpt +++ b/ext/soap/tests/gh23686/schema-authorization-bearer.phpt @@ -32,10 +32,8 @@ check_headers_for_schema(LOGS_PATH, $context); 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 index ad48118d6c05..a218e694d77c 100644 --- a/ext/soap/tests/gh23686/schema-authorization-digest.phpt +++ b/ext/soap/tests/gh23686/schema-authorization-digest.phpt @@ -32,10 +32,8 @@ check_headers_for_schema(LOGS_PATH, $context); 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 index ff33d8ace5da..57c5984eee8a 100644 --- a/ext/soap/tests/gh23686/schema-cookies.phpt +++ b/ext/soap/tests/gh23686/schema-cookies.phpt @@ -32,10 +32,8 @@ check_headers_for_schema(LOGS_PATH, $context); 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 index 2f06bc9e7358..51920558c9b8 100644 --- a/ext/soap/tests/gh23686/schema-proxy-authorization.phpt +++ b/ext/soap/tests/gh23686/schema-proxy-authorization.phpt @@ -32,10 +32,8 @@ check_headers_for_schema(LOGS_PATH, $context); 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 index e99c7c9c7d97..066e1541b0b9 100644 --- a/ext/soap/tests/gh23686/schema-repeated-authorization.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-authorization.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated authorization headers (which doesn't work) (``) +GH-23686: Regression tests for repeated authorization headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,11 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" diff --git a/ext/soap/tests/gh23686/schema-repeated-cookies.phpt b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt index 76cc2b95a6dd..5eaa8ef9465e 100644 --- a/ext/soap/tests/gh23686/schema-repeated-cookies.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated cookies headers (which doesn't work) (``) +GH-23686: Regression tests for repeated cookies headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,11 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" diff --git a/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt index 9e6b2e6768b3..e23759b03a9b 100644 --- a/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23686: Regression tests for repeated proxy authorization headers (which doesn't work) (``) +GH-23686: Regression tests for repeated proxy authorization headers (which previously didn't work) (``) --EXTENSIONS-- soap --INI-- @@ -32,11 +32,11 @@ 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: +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" +string(%d) "array ( + 'Host' => 'localhost:%d', + 'Connection' => 'close', +)" From 68aa3372a15d836d7baaba93e1b15d364497f8fe Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 15 Sep 2026 22:25:21 -0700 Subject: [PATCH 4/4] SoapClient: add new `keep_headers` option --- UPGRADING | 8 ++++ ext/soap/php_sdl.c | 14 +++++++ ext/soap/php_sdl.h | 1 + ext/soap/php_soap.h | 6 +++ ext/soap/soap.c | 21 ++++++++++ ext/soap/soap.stub.php | 1 + ext/soap/soap_arginfo.h | 8 +++- ext/soap/tests/bugs/gh21421.phpt | 4 +- ext/soap/tests/gh23686/check_headers.inc | 16 +++++++- .../gh23686/import-keep-authorization.phpt | 41 +++++++++++++++++++ .../tests/gh23686/import-keep-cookies.phpt | 41 +++++++++++++++++++ .../tests/gh23686/import-keep-proxy-auth.phpt | 41 +++++++++++++++++++ .../import-repeated-authorization.phpt | 2 +- .../gh23686/import-repeated-cookies.phpt | 2 +- .../gh23686/import-repeated-proxy-auth.phpt | 2 +- .../gh23686/schema-keep-authorization.phpt | 41 +++++++++++++++++++ .../tests/gh23686/schema-keep-cookies.phpt | 41 +++++++++++++++++++ .../tests/gh23686/schema-keep-proxy-auth.phpt | 41 +++++++++++++++++++ .../schema-repeated-authorization.phpt | 2 +- .../gh23686/schema-repeated-cookies.phpt | 2 +- .../gh23686/schema-repeated-proxy-auth.phpt | 2 +- 21 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 ext/soap/tests/gh23686/import-keep-authorization.phpt create mode 100644 ext/soap/tests/gh23686/import-keep-cookies.phpt create mode 100644 ext/soap/tests/gh23686/import-keep-proxy-auth.phpt create mode 100644 ext/soap/tests/gh23686/schema-keep-authorization.phpt create mode 100644 ext/soap/tests/gh23686/schema-keep-cookies.phpt create mode 100644 ext/soap/tests/gh23686/schema-keep-proxy-auth.phpt diff --git a/UPGRADING b/UPGRADING index d426f960abea..2d48a1da05ca 100644 --- a/UPGRADING +++ b/UPGRADING @@ -481,6 +481,14 @@ PHP 8.6 UPGRADE NOTES SNMP::setStringOutputFormat() methods. (eskyuu) RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls +- SOAP: + . It is now possible to specify what headers a `SoapClient` should redact + when importing a WSDL from a host different from the original. The default + behavior is to redact the "Authorization", "Proxy-Authorization", and + "Cookie" headers. Use the 'keep_headers' key in the array of options passe + as the second argument to `SoapClient::__construct()` to specify which + headers should be kept. + - Standard: . pack() and unpack() now accept the "<" and ">" endianness modifiers on the signed and unsigned integer format codes. diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c index a2065cb0a815..69ab56169c40 100644 --- a/ext/soap/php_sdl.c +++ b/ext/soap/php_sdl.c @@ -358,6 +358,15 @@ void sdl_set_uri_credentials(sdlCtx *ctx, char *uri) zend_string *lc_headers = zend_string_tolower(flat_headers); // Fast path: no redaction needed uint32_t redaction = header_needs_redaction(lc_headers); + if (ctx->headers_to_keep & WSDL_HEADER_KEEP_AUTHORIZATION) { + redaction &= ~REDACT_AUTHORIZATION; + } + if (ctx->headers_to_keep & WSDL_HEADER_KEEP_PROXY_AUTHORIZATION) { + redaction &= ~REDACT_PROXY_AUTH; + } + if (ctx->headers_to_keep & WSDL_HEADER_KEEP_COOKIES) { + redaction &= ~REDACT_COOKIE; + } if (redaction == REDACT_NONE) { zend_string_release(lc_headers); zend_string_release(flat_headers); @@ -863,6 +872,11 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) zend_hash_init(&ctx.portTypes, 0, NULL, NULL, 0); zend_hash_init(&ctx.services, 0, NULL, NULL, 0); + ctx.headers_to_keep = 0; + if (instanceof_function(Z_OBJCE_P(this_ptr), soap_class_entry)) { + ctx.headers_to_keep = Z_LVAL_P(Z_CLIENT_KEEP_HEADERS_P(this_ptr)); + } + zend_try { load_wsdl_ex(this_ptr, struri, &ctx, false); schema_pass2(&ctx); diff --git a/ext/soap/php_sdl.h b/ext/soap/php_sdl.h index 7249f094f165..b076285ebb60 100644 --- a/ext/soap/php_sdl.h +++ b/ext/soap/php_sdl.h @@ -74,6 +74,7 @@ typedef struct sdlCtx { HashTable fixupInProgress; php_stream_context *context; zval old_header; + zend_long headers_to_keep; /* Bitmask of WSDL_HEADER_KEEP_* from php_soap.h */ } sdlCtx; struct _sdlBinding { diff --git a/ext/soap/php_soap.h b/ext/soap/php_soap.h index d3c273615eb6..c067b633f488 100644 --- a/ext/soap/php_soap.h +++ b/ext/soap/php_soap.h @@ -151,6 +151,11 @@ struct _soapService { #define SOAP_MAX_XML_DEPTH 2048 #define SOAP_MAX_DECODE_DEPTH (SOAP_MAX_XML_DEPTH * 2) +/* Some headers are redacted on requests to other hosts unless requested otherwise */ +#define WSDL_HEADER_KEEP_AUTHORIZATION (1 << 0) +#define WSDL_HEADER_KEEP_PROXY_AUTHORIZATION (1 << 1) +#define WSDL_HEADER_KEEP_COOKIES (1 << 2) + ZEND_BEGIN_MODULE_GLOBALS(soap) HashTable *typemap; int cur_uniq_ns; @@ -255,6 +260,7 @@ static zend_always_inline zval *php_soap_deref(zval *zv) { #define Z_CLIENT_LAST_RESPONSE_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 32) #define Z_CLIENT_LAST_REQUEST_HEADERS_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 33) #define Z_CLIENT_LAST_RESPONSE_HEADERS_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 34) +#define Z_CLIENT_KEEP_HEADERS_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 35) typedef struct soap_url_object { php_uri *uri; diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 97a653955825..2dc6802310b0 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -2106,6 +2106,7 @@ PHP_METHOD(SoapClient, __construct) sdlPtr sdl = NULL; HashTable *typemap_ht = NULL; zval *this_ptr = ZEND_THIS; + zend_long keep_headers = 0; if (zend_parse_parameters(ZEND_NUM_ARGS(), "S!|a", &wsdl, &options) == FAILURE) { RETURN_THROWS(); @@ -2303,6 +2304,26 @@ PHP_METHOD(SoapClient, __construct) "The \"ssl_method\" option is deprecated. " "Use \"ssl\" stream context options instead"); } + + if ((tmp = zend_hash_str_find(ht, "keep_headers", sizeof("keep_headers")-1)) != NULL + && Z_TYPE_P(tmp) == IS_ARRAY + ) { + ZEND_HASH_FOREACH_VAL(Z_ARR_P(tmp), zval *to_keep) { + if (Z_TYPE_P(to_keep) != IS_STRING) { + continue; + } + zend_string *header_name = Z_STR_P(to_keep); + if (zend_string_equals_literal_ci(header_name, "authorization")) { + keep_headers |= WSDL_HEADER_KEEP_AUTHORIZATION; + } else if (zend_string_equals_literal_ci(header_name, "proxy-authorization")) { + keep_headers |= WSDL_HEADER_KEEP_PROXY_AUTHORIZATION; + } else if (zend_string_equals_literal_ci(header_name, "cookie")) { + keep_headers |= WSDL_HEADER_KEEP_COOKIES; + } + } ZEND_HASH_FOREACH_END(); + ZVAL_LONG(Z_CLIENT_KEEP_HEADERS_P(this_ptr), keep_headers); + + } } else if (!wsdl) { php_error_docref(NULL, E_ERROR, "'location' and 'uri' options are required in nonWSDL mode"); } diff --git a/ext/soap/soap.stub.php b/ext/soap/soap.stub.php index fdd4a46e109f..d614fe8d963d 100644 --- a/ext/soap/soap.stub.php +++ b/ext/soap/soap.stub.php @@ -572,6 +572,7 @@ class SoapClient private ?string $__last_response = null; private ?string $__last_request_headers = null; private ?string $__last_response_headers = null; + private int $_keep_headers = 0; public function __construct(?string $wsdl, array $options = []) {} diff --git a/ext/soap/soap_arginfo.h b/ext/soap/soap_arginfo.h index f6b146d779d6..5757cea7e361 100644 --- a/ext/soap/soap_arginfo.h +++ b/ext/soap/soap_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit soap.stub.php instead. - * Stub hash: 14c74a5d6f547837f536920d5abb741e2b6e4373 */ + * Stub hash: c3509a1552db7edabc48d89eea67ff2e1e7d9115 */ #include "zend_attributes.h" #include "zend_constants.h" @@ -748,5 +748,11 @@ static zend_class_entry *register_class_SoapClient(void) zend_declare_typed_property(class_entry, property___last_response_headers_name, &property___last_response_headers_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); zend_string_release_ex(property___last_response_headers_name, true); + zval property__keep_headers_default_value; + ZVAL_LONG(&property__keep_headers_default_value, 0); + zend_string *property__keep_headers_name = zend_string_init("_keep_headers", sizeof("_keep_headers") - 1, true); + zend_declare_typed_property(class_entry, property__keep_headers_name, &property__keep_headers_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(property__keep_headers_name, true); + return class_entry; } diff --git a/ext/soap/tests/bugs/gh21421.phpt b/ext/soap/tests/bugs/gh21421.phpt index 1b8bb3be716f..e20b79aed56b 100644 --- a/ext/soap/tests/bugs/gh21421.phpt +++ b/ext/soap/tests/bugs/gh21421.phpt @@ -15,7 +15,7 @@ $client = new SoapClient(NULL, $options); var_dump($client); ?> --EXPECTF-- -object(SoapClient)#%d (35) { +object(SoapClient)#%d (36) { ["uri":"SoapClient":private]=> string(26) "http://schemas.nothing.com" ["style":"SoapClient":private]=> @@ -87,4 +87,6 @@ object(SoapClient)#%d (35) { NULL ["__last_response_headers":"SoapClient":private]=> NULL + ["_keep_headers":"SoapClient":private]=> + int(0) } diff --git a/ext/soap/tests/gh23686/check_headers.inc b/ext/soap/tests/gh23686/check_headers.inc index f698d74ff97a..707d12844322 100644 --- a/ext/soap/tests/gh23686/check_headers.inc +++ b/ext/soap/tests/gh23686/check_headers.inc @@ -20,7 +20,12 @@ function start_server(string $code, ?string $entry_point) { * When an 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) { +function check_headers_for_import( + string $logs_path, + $context, + array $options = [], + bool $verbose_failure = true, +) { $code = file_get_contents(__DIR__ . "/soap-respond-with-headers.inc"); $code = substr($code, strpos($code, " $version, 'stream_context' => $context, + ...$options, ]); } catch (SoapFault $e) { if ($verbose_failure) { @@ -61,7 +67,12 @@ function check_headers_for_import(string $logs_path, $context, bool $verbose_fai * When an leads to a 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) { +function check_headers_for_schema( + string $logs_path, + $context, + array $options = [], + bool $verbose_failure = true +) { $code = file_get_contents(__DIR__ . "/schema-respond-with-headers.inc"); $code = substr($code, strpos($code, " $version, 'stream_context' => $context, + ...$options, ]); } catch (SoapFault $e) { if ($verbose_failure) { diff --git a/ext/soap/tests/gh23686/import-keep-authorization.phpt b/ext/soap/tests/gh23686/import-keep-authorization.phpt new file mode 100644 index 000000000000..cc545e812931 --- /dev/null +++ b/ext/soap/tests/gh23686/import-keep-authorization.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Authorization` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_import(LOGS_PATH, $context, ["keep_headers" => ["authorization"]]); + +?> +--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-keep-cookies.phpt b/ext/soap/tests/gh23686/import-keep-cookies.phpt new file mode 100644 index 000000000000..0732e3585bd6 --- /dev/null +++ b/ext/soap/tests/gh23686/import-keep-cookies.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Cookie` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_import(LOGS_PATH, $context, ["keep_headers" => ["cookie"]]); + +?> +--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-keep-proxy-auth.phpt b/ext/soap/tests/gh23686/import-keep-proxy-auth.phpt new file mode 100644 index 000000000000..5dcf75f3502a --- /dev/null +++ b/ext/soap/tests/gh23686/import-keep-proxy-auth.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Proxy-Authorization` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_import(LOGS_PATH, $context, ["keep_headers" => ["proxy-authorization"]]); + +?> +--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 index 7ec369b7b92b..2263e538ba96 100644 --- a/ext/soap/tests/gh23686/import-repeated-authorization.phpt +++ b/ext/soap/tests/gh23686/import-repeated-authorization.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_import(LOGS_PATH, $context, false); +check_headers_for_import(LOGS_PATH, $context, [], false); ?> --EXPECTF-- diff --git a/ext/soap/tests/gh23686/import-repeated-cookies.phpt b/ext/soap/tests/gh23686/import-repeated-cookies.phpt index b6c4f2e1d1db..4a7734547f4c 100644 --- a/ext/soap/tests/gh23686/import-repeated-cookies.phpt +++ b/ext/soap/tests/gh23686/import-repeated-cookies.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_import(LOGS_PATH, $context, false); +check_headers_for_import(LOGS_PATH, $context, [], false); ?> --EXPECTF-- diff --git a/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt index 4628fcb7fe0b..9dca3e1a0bbb 100644 --- a/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt +++ b/ext/soap/tests/gh23686/import-repeated-proxy-auth.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_import(LOGS_PATH, $context, false); +check_headers_for_import(LOGS_PATH, $context, [], false); ?> --EXPECTF-- diff --git a/ext/soap/tests/gh23686/schema-keep-authorization.phpt b/ext/soap/tests/gh23686/schema-keep-authorization.phpt new file mode 100644 index 000000000000..676e0f818849 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-keep-authorization.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Authorization` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_schema(LOGS_PATH, $context, ["keep_headers" => ["authorization"]]); + +?> +--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-keep-cookies.phpt b/ext/soap/tests/gh23686/schema-keep-cookies.phpt new file mode 100644 index 000000000000..6ba48e3fc24f --- /dev/null +++ b/ext/soap/tests/gh23686/schema-keep-cookies.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Cookie` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_schema(LOGS_PATH, $context, ["keep_headers" => ["cookie"]]); + +?> +--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-keep-proxy-auth.phpt b/ext/soap/tests/gh23686/schema-keep-proxy-auth.phpt new file mode 100644 index 000000000000..ae45bb74b217 --- /dev/null +++ b/ext/soap/tests/gh23686/schema-keep-proxy-auth.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23686: `Proxy-Authorization` header kept but others removed (``) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--CLEAN-- + +--SKIPIF-- + +--FILE-- + ['header' => "Authorization: Bearer secret-token\r\nProxy-Authorization: FooBar\r\nCookie: foo=bar"], +]); + +check_headers_for_schema(LOGS_PATH, $context, ["keep_headers" => ["proxy-authorization"]]); + +?> +--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 index 066e1541b0b9..d23d2592d8ca 100644 --- a/ext/soap/tests/gh23686/schema-repeated-authorization.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-authorization.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_schema(LOGS_PATH, $context, false); +check_headers_for_schema(LOGS_PATH, $context, [], false); ?> --EXPECTF-- diff --git a/ext/soap/tests/gh23686/schema-repeated-cookies.phpt b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt index 5eaa8ef9465e..dfc01e966a62 100644 --- a/ext/soap/tests/gh23686/schema-repeated-cookies.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-cookies.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_schema(LOGS_PATH, $context, false); +check_headers_for_schema(LOGS_PATH, $context, [], false); ?> --EXPECTF-- diff --git a/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt index e23759b03a9b..ea83b7ad9e85 100644 --- a/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt +++ b/ext/soap/tests/gh23686/schema-repeated-proxy-auth.phpt @@ -28,7 +28,7 @@ $context = stream_context_create([ ]], ]); -check_headers_for_schema(LOGS_PATH, $context, false); +check_headers_for_schema(LOGS_PATH, $context, [], false); ?> --EXPECTF--