From a52f0440530b54fa079ce76e8c5d196a42cad981 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 20 Jun 2022 23:16:07 +0100 Subject: [PATCH] Release 6.5.8 (#3042) * Release 6.5.8 * Update README.md * Update RedirectMiddleware.php --- CHANGELOG.md | 5 + README.md | 22 ++-- composer.json | 9 +- src/RedirectMiddleware.php | 37 ++----- tests/RedirectMiddlewareTest.php | 177 ++++++++++++++++++++++++++++--- 5 files changed, 199 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3db22d8..b053017a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 6.5.8 - 2022-06-20 + +* Fix change in port should be considered a change in origin +* Fix `CURLOPT_HTTPAUTH` option not cleared on change of origin + ## 6.5.7 - 2022-06-09 * Fix failure to strip Authorization header on HTTP downgrade diff --git a/README.md b/README.md index 00d2066e9..bc98e1a10 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -Guzzle, PHP HTTP client -======================= +![Guzzle](.github/logo.png?raw=true) + +# Guzzle, PHP HTTP client [![Latest Version](https://img.shields.io/github/release/guzzle/guzzle.svg?style=flat-square)](https://github.com/guzzle/guzzle/releases) [![Build Status](https://img.shields.io/github/workflow/status/guzzle/guzzle/CI?label=ci%20build&style=flat-square)](https://github.com/guzzle/guzzle/actions?query=workflow%3ACI) @@ -38,15 +39,18 @@ $promise->wait(); ## Help and docs -- [Documentation](http://guzzlephp.org/) -- [Stack Overflow](http://stackoverflow.com/questions/tagged/guzzle) +We use GitHub issues only to discuss bugs and new features. For support please refer to: + +- [Documentation](https://docs.guzzlephp.org) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle) +- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/) - [Gitter](https://gitter.im/guzzle/guzzle) ## Installing Guzzle The recommended way to install Guzzle is through -[Composer](http://getcomposer.org). +[Composer](https://getcomposer.org/). ```bash # Install Composer @@ -87,7 +91,7 @@ composer update [guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3 [guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5 [guzzle-7-repo]: https://github.com/guzzle/guzzle -[guzzle-3-docs]: http://guzzle3.readthedocs.org -[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/ -[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/ -[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/ +[guzzle-3-docs]: https://guzzle3.readthedocs.io/ +[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/ +[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/ +[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/ diff --git a/composer.json b/composer.json index b9cb386a6..a57d78f60 100644 --- a/composer.json +++ b/composer.json @@ -53,9 +53,9 @@ "require": { "php": ">=5.5", "ext-json": "*", - "symfony/polyfill-intl-idn": "^1.17.0", + "symfony/polyfill-intl-idn": "^1.17", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1" + "guzzlehttp/psr7": "^1.9" }, "require-dev": { "ext-curl": "*", @@ -66,7 +66,10 @@ "psr/log": "Required for using the Log middleware" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } }, "extra": { "branch-alias": { diff --git a/src/RedirectMiddleware.php b/src/RedirectMiddleware.php index fd86c60a7..008a29b8c 100644 --- a/src/RedirectMiddleware.php +++ b/src/RedirectMiddleware.php @@ -94,6 +94,14 @@ public function checkRedirect( $this->guardMax($request, $options); $nextRequest = $this->modifyRequest($request, $options, $response); + // If authorization is handled by curl, unset it if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) { + unset( + $options['curl'][\CURLOPT_HTTPAUTH], + $options['curl'][\CURLOPT_USERPWD] + ); + } + if (isset($options['allow_redirects']['on_redirect'])) { call_user_func( $options['allow_redirects']['on_redirect'], @@ -210,8 +218,8 @@ public function modifyRequest( $modify['remove_headers'][] = 'Referer'; } - // Remove Authorization and Cookie headers if required. - if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) { + // Remove Authorization and Cookie headers if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) { $modify['remove_headers'][] = 'Authorization'; $modify['remove_headers'][] = 'Cookie'; } @@ -219,31 +227,6 @@ public function modifyRequest( return Psr7\modify_request($request, $modify); } - /** - * Determine if we should strip sensitive headers from the request. - * - * We return true if either of the following conditions are true: - * - * 1. the host is different; - * 2. the scheme has changed, and now is non-https. - * - * @return bool - */ - private static function shouldStripSensitiveHeaders( - UriInterface $originalUri, - UriInterface $modifiedUri - ) { - if (strcasecmp($originalUri->getHost(), $modifiedUri->getHost()) !== 0) { - return true; - } - - if ($originalUri->getScheme() !== $modifiedUri->getScheme() && 'https' !== $modifiedUri->getScheme()) { - return true; - } - - return false; - } - /** * Set the appropriate URL on the request based on the location header. * diff --git a/tests/RedirectMiddlewareTest.php b/tests/RedirectMiddlewareTest.php index fc830bacb..73f333a27 100644 --- a/tests/RedirectMiddlewareTest.php +++ b/tests/RedirectMiddlewareTest.php @@ -251,30 +251,183 @@ public function testInvokesOnRedirectForRedirects() self::assertTrue($call); } + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossHost($auth) + { + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://test.com']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + } + + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossPort($auth) + { + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com:81/']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + } + + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossScheme($auth) + { + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com?a=b']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + } + + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossSchemeSamePort($auth) + { + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com:80?a=b']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + } + + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testNotRemoveCurlAuthorizationOptionsOnRedirect($auth) + { + if (!defined('\CURLOPT_HTTPAUTH') || !defined('\CURLOPT_USERPWD')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com/2']), + static function (RequestInterface $request, $options) { + self::assertTrue( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options does not contain expected CURLOPT_HTTPAUTH entry' + ); + self::assertTrue( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options does not contain expected CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + } + public function crossOriginRedirectProvider() { return [ - ['http://example.com?a=b', 'http://test.com/', false], - ['https://example.com?a=b', 'https://test.com/', false], - ['http://example.com?a=b', 'https://test.com/', false], - ['https://example.com?a=b', 'http://test.com/', false], - ['http://example.com?a=b', 'http://example.com/', true], - ['https://example.com?a=b', 'https://example.com/', true], - ['http://example.com?a=b', 'https://example.com/', true], - ['https://example.com?a=b', 'http://example.com/', false], + ['http://example.com/123', 'http://example.com/', false], + ['http://example.com/123', 'http://example.com:80/', false], + ['http://example.com:80/123', 'http://example.com/', false], + ['http://example.com:80/123', 'http://example.com:80/', false], + ['http://example.com/123', 'https://example.com/', true], + ['http://example.com/123', 'http://www.example.com/', true], + ['http://example.com/123', 'http://example.com:81/', true], + ['http://example.com:80/123', 'http://example.com:81/', true], + ['https://example.com/123', 'https://example.com/', false], + ['https://example.com/123', 'https://example.com:443/', false], + ['https://example.com:443/123', 'https://example.com/', false], + ['https://example.com:443/123', 'https://example.com:443/', false], + ['https://example.com/123', 'http://example.com/', true], + ['https://example.com/123', 'https://www.example.com/', true], + ['https://example.com/123', 'https://example.com:444/', true], + ['https://example.com:443/123', 'https://example.com:444/', true], ]; } /** * @dataProvider crossOriginRedirectProvider */ - public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $shouldBePresent) + public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $isCrossOrigin) { $mock = new MockHandler([ new Response(302, ['Location' => $targetUri]), - function (RequestInterface $request) use ($shouldBePresent) { - self::assertSame($shouldBePresent, $request->hasHeader('Authorization')); - self::assertSame($shouldBePresent, $request->hasHeader('Cookie')); + function (RequestInterface $request) use ($isCrossOrigin) { + self::assertSame(!$isCrossOrigin, $request->hasHeader('Authorization')); + self::assertSame(!$isCrossOrigin, $request->hasHeader('Cookie')); return new Response(200); }