Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ PHP NEWS
- SOAP:
. Fixed the SOAP client accepting a response body shorter than its
Content-Length and reusing the connection. (Ilia Alshanetsky)
. Fixed bug GH-24315 (Harden SoapClient cross-origin redirect handling).
(Ilia Alshanetsky)

- SPL:
. Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when
Expand Down
8 changes: 8 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ PHP 8.7 UPGRADE NOTES
not present in the result set. It previously reported the condition
through PDO::ATTR_ERRMODE and returned false.

- SOAP:
. SoapClient no longer follows an HTTP redirect to a different origin
(scheme, host, or port) and throws a SoapFault instead. Previously the
request body and the configured credentials and headers were resent to
the new origin. A redirect from http to https on the same host is still
followed. Set the new follow_cross_origin_redirects option to true to
restore the previous behavior.

- Session:
. Setting session.cookie_path to a value containing one of ";\r\n", or
session.cookie_domain to a value containing one of ",; \t\r\n\013\014",
Expand Down
31 changes: 31 additions & 0 deletions ext/soap/php_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,28 @@ static bool in_domain(const zend_string *host, const zend_string *domain)
}
}

static bool is_allowed_redirect(const php_uri *from, const php_uri *to)
{
if (to->scheme == NULL || to->host == NULL) {
return true;
}

if (!zend_string_equals_ci(from->host, to->host)) {
return false;
}

zend_long to_port = to->port;
if (to_port == 0) {
to_port = zend_string_equals_literal(to->scheme, "https") ? 443 : 80;
}

if (zend_string_equals(from->scheme, to->scheme)) {
return from->port == to_port;
}

return zend_string_equals_literal(from->scheme, "http") && zend_string_equals_literal(to->scheme, "https") && to_port == 443;
}

bool make_http_soap_request(
zval *this_ptr, zend_string *buf, zend_string *location, char *soapaction,
int soap_version, zend_string *uri_parser_class, zval *return_value
Expand Down Expand Up @@ -1191,6 +1213,15 @@ bool make_http_soap_request(
}
}
}

if (Z_TYPE_P(Z_CLIENT_FOLLOW_CROSS_ORIGIN_REDIRECTS_P(this_ptr)) != IS_TRUE && !is_allowed_redirect(uri, new_uri)) {
php_uri_struct_free(new_uri);
add_soap_fault(this_ptr, "HTTP", "Redirection to a different origin is not allowed", NULL, NULL, soap_lang_en);
smart_str_free(&soap_headers_z);
efree(http_msg);
return false;
}

uri = new_uri;

if (--redirect_max < 1) {
Expand Down
1 change: 1 addition & 0 deletions ext/soap/php_soap.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,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_FOLLOW_CROSS_ORIGIN_REDIRECTS_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 35)

typedef struct soap_url_object {
php_uri *uri;
Expand Down
6 changes: 6 additions & 0 deletions ext/soap/soap.c
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,12 @@ PHP_METHOD(SoapClient, __construct)
ZVAL_FALSE(Z_CLIENT_KEEP_ALIVE_P(this_ptr));
}

if ((tmp = zend_hash_str_find(ht, "follow_cross_origin_redirects", sizeof("follow_cross_origin_redirects")-1)) != NULL &&
(Z_TYPE_P(tmp) == IS_TRUE ||
(Z_TYPE_P(tmp) == IS_LONG && Z_LVAL_P(tmp) == 1))) {
ZVAL_TRUE(Z_CLIENT_FOLLOW_CROSS_ORIGIN_REDIRECTS_P(this_ptr));
}

if ((tmp = zend_hash_str_find(ht, "ssl_method", sizeof("ssl_method")-1)) != NULL &&
Z_TYPE_P(tmp) == IS_LONG) {
ZVAL_LONG(Z_CLIENT_SSL_METHOD_P(this_ptr), Z_LVAL_P(tmp));
Expand Down
1 change: 1 addition & 0 deletions ext/soap/soap.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ class SoapClient
private ?string $__last_response = null;
private ?string $__last_request_headers = null;
private ?string $__last_response_headers = null;
private bool $_follow_cross_origin_redirects = false;

public function __construct(?string $wsdl, array $options = []) {}

Expand Down
8 changes: 7 additions & 1 deletion ext/soap/soap_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion ext/soap/tests/bugs/gh21421.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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]=>
Expand Down Expand Up @@ -87,4 +87,6 @@ object(SoapClient)#%d (35) {
NULL
["__last_response_headers":"SoapClient":private]=>
NULL
["_follow_cross_origin_redirects":"SoapClient":private]=>
bool(false)
}
75 changes: 75 additions & 0 deletions ext/soap/tests/bugs/gh24315.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
--TEST--
GH-24315 (SoapClient follows redirects to a different origin)
--EXTENSIONS--
soap
openssl

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it needed ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

soap just for completion really, not strictly needed,
openssl does have a purpose, the allowed HTTP-to-HTTPS upgrade case emits a missing-wrapper warning without OpenSSL. Catching SoapFault doesn't suppress it, so the test fails.

--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__ . "/../../../../sapi/cli/tests/php_cli_server.inc";

$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();
}

$code = <<<'PHP'
if ($_SERVER["REQUEST_URI"] === "/ok") {
header("Content-Type: text/xml; charset=utf-8");
echo '<?xml version="1.0" encoding="UTF-8"?>',
'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">',
'<SOAP-ENV:Body><fooResponse><result>ok</result></fooResponse></SOAP-ENV:Body>',
'</SOAP-ENV:Envelope>';
} else {
http_response_code(302);
header("Location: " . base64_decode(substr($_SERVER["REQUEST_URI"], 1)));
}
PHP;

php_cli_server_start($code, null, $args);

$targets = [
"different host" => "http://127.0.0.1:" . PHP_CLI_SERVER_PORT . "/ok",
"different port" => "http://" . PHP_CLI_SERVER_HOSTNAME . ":" . (PHP_CLI_SERVER_PORT + 1) . "/ok",
"protocol-relative different host" => "//127.0.0.1:" . PHP_CLI_SERVER_PORT . "/ok",
"https upgrade to a non-default port" => "https://" . PHP_CLI_SERVER_ADDRESS . "/ok",
"same origin" => "http://" . PHP_CLI_SERVER_ADDRESS . "/ok",
"same origin, host case differs" => "http://" . strtoupper(PHP_CLI_SERVER_HOSTNAME) . ":" . PHP_CLI_SERVER_PORT . "/ok",
"https upgrade on the same host" => "https://" . PHP_CLI_SERVER_HOSTNAME . "/ok",
];

$targets["different host, follow_cross_origin_redirects"] = $targets["different host"];

foreach ($targets as $label => $target) {
$client = new SoapClient(null, [
'location' => 'http://' . PHP_CLI_SERVER_ADDRESS . '/' . base64_encode($target),
'uri' => 'test-uri',
'login' => 'user',
'password' => 'secret',
'follow_cross_origin_redirects' => str_ends_with($label, 'follow_cross_origin_redirects'),
]);

$rejected = false;
try {
$client->__soapCall("foo", []);
} catch (SoapFault $e) {
$rejected = $e->getMessage() === "Redirection to a different origin is not allowed";
}
echo $label, ": ", $rejected ? "rejected" : "not rejected", "\n";
}
?>
--EXPECT--
different host: rejected
different port: rejected
protocol-relative different host: rejected
https upgrade to a non-default port: rejected
same origin: not rejected
same origin, host case differs: not rejected
https upgrade on the same host: not rejected
different host, follow_cross_origin_redirects: not rejected
Loading