CVE-2022-31091 (PHP, published 2022-06-27T22:15Z)
Description: value: Guzzle, an extensible PHP HTTP client. `Authorization` and `Cookie` headers on requests are sensitive information. In affected versions on making a request which responds with a redirect to a URI with a different port, if we choose to follow it, we should remove the `Authorization` and `Cookie` headers from the request, before containing. Previously, we would only consider a change in host or scheme. Affected Guzzle 7 users should upgrade to Guzzle 7.4.5 as soon as possible. Affected users using any earlier series of Guzzle should upgrade to Guzzle 6.5.8 or 7.4.5. Note that a partial fix was implemented in Guzzle 7.4.2, where a change in host would trigger removal of the curl-added Authorization header, however this earlier fix did not cover change in scheme or change in port. An alternative approach would be to use your own redirect middleware, rather than ours, if you are unable to upgrade. If you do not require or expect redirects to be followed, one should simply disable redirects all together.
Severity: MEDIUM (CVSS v3 base score: 7.7)
Weakness: CWE-200 \u2014 Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
Repository: https://github.com/guzzle/guzzle
Files changed:
CHANGELOG.md
README.md
composer.json
src/RedirectMiddleware.php
tests/RedirectMiddlewareTest.php
Commit message: Release 7.4.5 (#3043)



Vulnerable code:
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/)
[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/
        "guzzlehttp/psr7": "^1.8.3 || ^2.1",
        // If authorization is handled by curl, unset it if host is different.
        if ($request->getUri()->getHost() !== $nextRequest->getUri()->getHost()
            && defined('\CURLOPT_HTTPAUTH')
        ) {
        // Remove Authorization and Cookie headers if required.
        if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) {
    /**
     * 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.
     */
    private static function shouldStripSensitiveHeaders(
        UriInterface $originalUri,
        UriInterface $modifiedUri
    ): bool {
        if (\strcasecmp($originalUri->getHost(), $modifiedUri->getHost()) !== 0) {
            return true;
        }

        if ($originalUri->getScheme() !== $modifiedUri->getScheme() && 'https' !== $modifiedUri->getScheme()) {
            return true;
        }

        return false;
    }

    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],
        ];
     * @dataProvider crossOriginRedirectProvider
    public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $shouldBePresent)
        $mock = new MockHandler([
            new Response(302, ['Location' => $targetUri]),
            static function (RequestInterface $request) use ($shouldBePresent) {
                self::assertSame($shouldBePresent, $request->hasHeader('Authorization'));
                self::assertSame($shouldBePresent, $request->hasHeader('Cookie'));
        $client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
    public function testNotRemoveAuthorizationHeaderOnRedirect()
            new Response(302, ['Location' => 'http://example.com/2']),
            static function (RequestInterface $request) {
                self::assertTrue($request->hasHeader('Authorization'));
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
    public function testRemoveCurlAuthorizationOptionsOnRedirect($auth)
            new Response(302, ['Location' => 'http://test.com']),
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);

Fixed code:
## 7.4.5 - 2022-06-20

* Fix change in port should be considered a change in origin
* Fix `CURLOPT_HTTPAUTH` option not cleared on change of origin

- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
[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/
        "guzzlehttp/psr7": "^1.9 || ^2.4",
        // If authorization is handled by curl, unset it if URI is cross-origin.
        if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) {
        // Remove Authorization and Cookie headers if URI is cross-origin.
        if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) {
    /**
     * @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'
                );
        $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');
        }

            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'
                );
        $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
    public function testRemoveCurlAuthorizationOptionsOnRedirectCrossSchemeSamePort($auth)
            new Response(302, ['Location' => 'http://example.com:80?a=b']),
        $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
    public function crossOriginRedirectProvider()
    {
        return [
            ['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, $isCrossOrigin)
    {
        $mock = new MockHandler([
            new Response(302, ['Location' => $targetUri]),
            static function (RequestInterface $request) use ($isCrossOrigin) {
                self::assertSame(!$isCrossOrigin, $request->hasHeader('Authorization'));
                self::assertSame(!$isCrossOrigin, $request->hasHeader('Cookie'));

                return new Response(200);
            }
        ]);
        $handler = HandlerStack::create($mock);
        $client = new Client(['handler' => $handler]);
        $client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
    }

    public function testNotRemoveAuthorizationHeaderOnRedirect()
    {
        $mock = new MockHandler([
            new Response(302, ['Location' => 'http://example.com/2']),
            static function (RequestInterface $request) {
                self::assertTrue($request->hasHeader('Authorization'));
                return new Response(200);
            }
        ]);
        $handler = HandlerStack::create($mock);
        $client = new Client(['handler' => $handler]);
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
    }