Skip to content

Commit 1dd98b0

Browse files
Release 7.4.5 (#3043)
1 parent 8d2ce4e commit 1dd98b0

File tree

5 files changed

+137
-62
lines changed

5 files changed

+137
-62
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
44

5+
## 7.4.5 - 2022-06-20
6+
7+
* Fix change in port should be considered a change in origin
8+
* Fix `CURLOPT_HTTPAUTH` option not cleared on change of origin
9+
510
## 7.4.4 - 2022-06-09
611

712
* Fix failure to strip Authorization header on HTTP downgrade

Diff for: README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ We use GitHub issues only to discuss bugs and new features. For support please r
4444

4545
- [Documentation](https://docs.guzzlephp.org)
4646
- [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
47-
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/)
47+
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
4848
- [Gitter](https://gitter.im/guzzle/guzzle)
4949

5050

@@ -73,10 +73,10 @@ composer require guzzlehttp/guzzle
7373
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
7474
[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
7575
[guzzle-7-repo]: https://github.com/guzzle/guzzle
76-
[guzzle-3-docs]: http://guzzle3.readthedocs.org
77-
[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/
78-
[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/
79-
[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/
76+
[guzzle-3-docs]: https://guzzle3.readthedocs.io/
77+
[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/
78+
[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/
79+
[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/
8080

8181

8282
## Security

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"php": "^7.2.5 || ^8.0",
5555
"ext-json": "*",
5656
"guzzlehttp/promises": "^1.5",
57-
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
57+
"guzzlehttp/psr7": "^1.9 || ^2.4",
5858
"psr/http-client": "^1.0",
5959
"symfony/deprecation-contracts": "^2.2 || ^3.0"
6060
},

Diff for: src/RedirectMiddleware.php

+4-29
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,8 @@ public function checkRedirect(RequestInterface $request, array $options, Respons
8888
$this->guardMax($request, $response, $options);
8989
$nextRequest = $this->modifyRequest($request, $options, $response);
9090

91-
// If authorization is handled by curl, unset it if host is different.
92-
if ($request->getUri()->getHost() !== $nextRequest->getUri()->getHost()
93-
&& defined('\CURLOPT_HTTPAUTH')
94-
) {
91+
// If authorization is handled by curl, unset it if URI is cross-origin.
92+
if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) {
9593
unset(
9694
$options['curl'][\CURLOPT_HTTPAUTH],
9795
$options['curl'][\CURLOPT_USERPWD]
@@ -198,38 +196,15 @@ public function modifyRequest(RequestInterface $request, array $options, Respons
198196
$modify['remove_headers'][] = 'Referer';
199197
}
200198

201-
// Remove Authorization and Cookie headers if required.
202-
if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) {
199+
// Remove Authorization and Cookie headers if URI is cross-origin.
200+
if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) {
203201
$modify['remove_headers'][] = 'Authorization';
204202
$modify['remove_headers'][] = 'Cookie';
205203
}
206204

207205
return Psr7\Utils::modifyRequest($request, $modify);
208206
}
209207

210-
/**
211-
* Determine if we should strip sensitive headers from the request.
212-
*
213-
* We return true if either of the following conditions are true:
214-
*
215-
* 1. the host is different;
216-
* 2. the scheme has changed, and now is non-https.
217-
*/
218-
private static function shouldStripSensitiveHeaders(
219-
UriInterface $originalUri,
220-
UriInterface $modifiedUri
221-
): bool {
222-
if (\strcasecmp($originalUri->getHost(), $modifiedUri->getHost()) !== 0) {
223-
return true;
224-
}
225-
226-
if ($originalUri->getScheme() !== $modifiedUri->getScheme() && 'https' !== $modifiedUri->getScheme()) {
227-
return true;
228-
}
229-
230-
return false;
231-
}
232-
233208
/**
234209
* Set the appropriate URL on the request based on the location header.
235210
*/

Diff for: tests/RedirectMiddlewareTest.php

+122-27
Original file line numberDiff line numberDiff line change
@@ -272,65 +272,105 @@ public function testInvokesOnRedirectForRedirects()
272272
self::assertTrue($call);
273273
}
274274

275-
public function crossOriginRedirectProvider()
275+
/**
276+
* @testWith ["digest"]
277+
* ["ntlm"]
278+
*/
279+
public function testRemoveCurlAuthorizationOptionsOnRedirectCrossHost($auth)
276280
{
277-
return [
278-
['http://example.com?a=b', 'http://test.com/', false],
279-
['https://example.com?a=b', 'https://test.com/', false],
280-
['http://example.com?a=b', 'https://test.com/', false],
281-
['https://example.com?a=b', 'http://test.com/', false],
282-
['http://example.com?a=b', 'http://example.com/', true],
283-
['https://example.com?a=b', 'https://example.com/', true],
284-
['http://example.com?a=b', 'https://example.com/', true],
285-
['https://example.com?a=b', 'http://example.com/', false],
286-
];
281+
if (!defined('\CURLOPT_HTTPAUTH')) {
282+
self::markTestSkipped('ext-curl is required for this test');
283+
}
284+
285+
$mock = new MockHandler([
286+
new Response(302, ['Location' => 'http://test.com']),
287+
static function (RequestInterface $request, $options) {
288+
self::assertFalse(
289+
isset($options['curl'][\CURLOPT_HTTPAUTH]),
290+
'curl options still contain CURLOPT_HTTPAUTH entry'
291+
);
292+
self::assertFalse(
293+
isset($options['curl'][\CURLOPT_USERPWD]),
294+
'curl options still contain CURLOPT_USERPWD entry'
295+
);
296+
return new Response(200);
297+
}
298+
]);
299+
$handler = HandlerStack::create($mock);
300+
$client = new Client(['handler' => $handler]);
301+
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
287302
}
288303

289304
/**
290-
* @dataProvider crossOriginRedirectProvider
305+
* @testWith ["digest"]
306+
* ["ntlm"]
291307
*/
292-
public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $shouldBePresent)
308+
public function testRemoveCurlAuthorizationOptionsOnRedirectCrossPort($auth)
293309
{
294-
$mock = new MockHandler([
295-
new Response(302, ['Location' => $targetUri]),
296-
static function (RequestInterface $request) use ($shouldBePresent) {
297-
self::assertSame($shouldBePresent, $request->hasHeader('Authorization'));
298-
self::assertSame($shouldBePresent, $request->hasHeader('Cookie'));
310+
if (!defined('\CURLOPT_HTTPAUTH')) {
311+
self::markTestSkipped('ext-curl is required for this test');
312+
}
299313

314+
$mock = new MockHandler([
315+
new Response(302, ['Location' => 'http://example.com:81/']),
316+
static function (RequestInterface $request, $options) {
317+
self::assertFalse(
318+
isset($options['curl'][\CURLOPT_HTTPAUTH]),
319+
'curl options still contain CURLOPT_HTTPAUTH entry'
320+
);
321+
self::assertFalse(
322+
isset($options['curl'][\CURLOPT_USERPWD]),
323+
'curl options still contain CURLOPT_USERPWD entry'
324+
);
300325
return new Response(200);
301326
}
302327
]);
303328
$handler = HandlerStack::create($mock);
304329
$client = new Client(['handler' => $handler]);
305-
$client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
330+
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
306331
}
307332

308-
public function testNotRemoveAuthorizationHeaderOnRedirect()
333+
/**
334+
* @testWith ["digest"]
335+
* ["ntlm"]
336+
*/
337+
public function testRemoveCurlAuthorizationOptionsOnRedirectCrossScheme($auth)
309338
{
339+
if (!defined('\CURLOPT_HTTPAUTH')) {
340+
self::markTestSkipped('ext-curl is required for this test');
341+
}
342+
310343
$mock = new MockHandler([
311-
new Response(302, ['Location' => 'http://example.com/2']),
312-
static function (RequestInterface $request) {
313-
self::assertTrue($request->hasHeader('Authorization'));
344+
new Response(302, ['Location' => 'http://example.com?a=b']),
345+
static function (RequestInterface $request, $options) {
346+
self::assertFalse(
347+
isset($options['curl'][\CURLOPT_HTTPAUTH]),
348+
'curl options still contain CURLOPT_HTTPAUTH entry'
349+
);
350+
self::assertFalse(
351+
isset($options['curl'][\CURLOPT_USERPWD]),
352+
'curl options still contain CURLOPT_USERPWD entry'
353+
);
314354
return new Response(200);
315355
}
316356
]);
317357
$handler = HandlerStack::create($mock);
318358
$client = new Client(['handler' => $handler]);
319-
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
359+
$client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
320360
}
321361

322362
/**
323363
* @testWith ["digest"]
324364
* ["ntlm"]
325365
*/
326-
public function testRemoveCurlAuthorizationOptionsOnRedirect($auth)
366+
public function testRemoveCurlAuthorizationOptionsOnRedirectCrossSchemeSamePort($auth)
327367
{
328368
if (!defined('\CURLOPT_HTTPAUTH')) {
329369
self::markTestSkipped('ext-curl is required for this test');
330370
}
331371

332372
$mock = new MockHandler([
333-
new Response(302, ['Location' => 'http://test.com']),
373+
new Response(302, ['Location' => 'http://example.com:80?a=b']),
334374
static function (RequestInterface $request, $options) {
335375
self::assertFalse(
336376
isset($options['curl'][\CURLOPT_HTTPAUTH]),
@@ -345,7 +385,7 @@ static function (RequestInterface $request, $options) {
345385
]);
346386
$handler = HandlerStack::create($mock);
347387
$client = new Client(['handler' => $handler]);
348-
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
388+
$client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
349389
}
350390

351391
/**
@@ -377,6 +417,61 @@ static function (RequestInterface $request, $options) {
377417
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
378418
}
379419

420+
public function crossOriginRedirectProvider()
421+
{
422+
return [
423+
['http://example.com/123', 'http://example.com/', false],
424+
['http://example.com/123', 'http://example.com:80/', false],
425+
['http://example.com:80/123', 'http://example.com/', false],
426+
['http://example.com:80/123', 'http://example.com:80/', false],
427+
['http://example.com/123', 'https://example.com/', true],
428+
['http://example.com/123', 'http://www.example.com/', true],
429+
['http://example.com/123', 'http://example.com:81/', true],
430+
['http://example.com:80/123', 'http://example.com:81/', true],
431+
['https://example.com/123', 'https://example.com/', false],
432+
['https://example.com/123', 'https://example.com:443/', false],
433+
['https://example.com:443/123', 'https://example.com/', false],
434+
['https://example.com:443/123', 'https://example.com:443/', false],
435+
['https://example.com/123', 'http://example.com/', true],
436+
['https://example.com/123', 'https://www.example.com/', true],
437+
['https://example.com/123', 'https://example.com:444/', true],
438+
['https://example.com:443/123', 'https://example.com:444/', true],
439+
];
440+
}
441+
442+
/**
443+
* @dataProvider crossOriginRedirectProvider
444+
*/
445+
public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $isCrossOrigin)
446+
{
447+
$mock = new MockHandler([
448+
new Response(302, ['Location' => $targetUri]),
449+
static function (RequestInterface $request) use ($isCrossOrigin) {
450+
self::assertSame(!$isCrossOrigin, $request->hasHeader('Authorization'));
451+
self::assertSame(!$isCrossOrigin, $request->hasHeader('Cookie'));
452+
453+
return new Response(200);
454+
}
455+
]);
456+
$handler = HandlerStack::create($mock);
457+
$client = new Client(['handler' => $handler]);
458+
$client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
459+
}
460+
461+
public function testNotRemoveAuthorizationHeaderOnRedirect()
462+
{
463+
$mock = new MockHandler([
464+
new Response(302, ['Location' => 'http://example.com/2']),
465+
static function (RequestInterface $request) {
466+
self::assertTrue($request->hasHeader('Authorization'));
467+
return new Response(200);
468+
}
469+
]);
470+
$handler = HandlerStack::create($mock);
471+
$client = new Client(['handler' => $handler]);
472+
$client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
473+
}
474+
380475
/**
381476
* Verifies how RedirectMiddleware::modifyRequest() modifies the method and body of a request issued when
382477
* encountering a redirect response.

0 commit comments

Comments
 (0)