CVE-2022-31090 (PHP, published 2022-06-27T22:15Z)
Description: value: Guzzle, an extensible PHP HTTP client. `Authorization` headers on requests are sensitive information. In affected versions when using our Curl handler, it is possible to use the `CURLOPT_HTTPAUTH` option to specify an `Authorization` header. On making a request which responds with a redirect to a URI with a different origin (change in host, scheme or port), if we choose to follow it, we should remove the `CURLOPT_HTTPAUTH` option before continuing, stopping curl from appending the `Authorization` header to the new request. 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. If you do not require or expect redirects to be followed, one should simply disable redirects all together. Alternatively, one can specify to use the Guzzle steam handler backend, rather than curl.
Severity: MEDIUM (CVSS v3 base score: 7.7)
Weakness: CWE-212 \u2014 Improper Removal of Sensitive Information Before Storage or Transfer
The product stores, transfers, or shares a resource that contains sensitive information, but it does not properly remove that information before the product makes the resource available to unauthorized actors.
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']]);
    }