Skip to content

Commit 23a7f02

Browse files
GromNaNnicolas-grekas
authored andcommitted
[HttpClient] Remove credentials from requests redirected to same host but different port
1 parent 768fb01 commit 23a7f02

File tree

4 files changed

+49
-8
lines changed

4 files changed

+49
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Allow yielding `Exception` from MockResponse's `$body` to mock transport errors
8+
* Remove credentials from requests redirected to same host but different port
89

910
5.4
1011
---

CurlHttpClient.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public function request(string $method, string $url, array $options = []): Respo
9090
$scheme = $url['scheme'];
9191
$authority = $url['authority'];
9292
$host = parse_url($authority, \PHP_URL_HOST);
93+
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
9394
$url = implode('', $url);
9495

9596
if (!isset($options['normalized_headers']['user-agent'])) {
@@ -163,7 +164,6 @@ public function request(string $method, string $url, array $options = []): Respo
163164
// First reset any old DNS cache entries then add the new ones
164165
$resolve = $this->multi->dnsCache->evictions;
165166
$this->multi->dnsCache->evictions = [];
166-
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
167167

168168
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
169169
// DNS cache removals require curl 7.42 or higher
@@ -293,7 +293,7 @@ public function request(string $method, string $url, array $options = []): Respo
293293
}
294294
}
295295

296-
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
296+
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number']);
297297
}
298298

299299
/**
@@ -373,11 +373,12 @@ private static function readRequestBody(int $length, \Closure $body, string &$bu
373373
*
374374
* Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
375375
*/
376-
private static function createRedirectResolver(array $options, string $host): \Closure
376+
private static function createRedirectResolver(array $options, string $host, int $port): \Closure
377377
{
378378
$redirectHeaders = [];
379379
if (0 < $options['max_redirects']) {
380380
$redirectHeaders['host'] = $host;
381+
$redirectHeaders['port'] = $port;
381382
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
382383
return 0 !== stripos($h, 'Host:');
383384
});
@@ -397,7 +398,8 @@ private static function createRedirectResolver(array $options, string $host): \C
397398
}
398399

399400
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
400-
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
401+
$port = parse_url('http:'.$location['authority'], \PHP_URL_PORT) ?: ('http:' === $location['scheme'] ? 80 : 443);
402+
$requestHeaders = $redirectHeaders['host'] === $host && $redirectHeaders['port'] === $port ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
401403
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
402404
}
403405

NativeHttpClient.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ public function request(string $method, string $url, array $options = []): Respo
237237
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
238238
}
239239

240-
return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
240+
return [self::createRedirectResolver($options, $host, $port, $proxy, $info, $onProgress), implode('', $url)];
241241
};
242242

243243
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
@@ -331,11 +331,11 @@ private static function dnsResolve(string $host, NativeClientState $multi, array
331331
/**
332332
* Handles redirects - the native logic is too buggy to be used.
333333
*/
334-
private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
334+
private static function createRedirectResolver(array $options, string $host, string $port, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
335335
{
336336
$redirectHeaders = [];
337337
if (0 < $maxRedirects = $options['max_redirects']) {
338-
$redirectHeaders = ['host' => $host];
338+
$redirectHeaders = ['host' => $host, 'port' => $port];
339339
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
340340
return 0 !== stripos($h, 'Host:');
341341
});
@@ -392,7 +392,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
392392

393393
if (false !== (parse_url($location, \PHP_URL_HOST) ?? false)) {
394394
// Authorization and Cookie headers MUST NOT follow except for the initial host name
395-
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
395+
$requestHeaders = $redirectHeaders['host'] === $host && $redirectHeaders['port'] === $port ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
396396
$requestHeaders[] = 'Host: '.$host.$port;
397397
$dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
398398
} else {

Tests/HttpClientTestCase.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
2222
use Symfony\Contracts\HttpClient\HttpClientInterface;
2323
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase as BaseHttpClientTestCase;
24+
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
25+
2426

2527
/*
2628
Tests for HTTP2 Push need a recent version of both PHP and curl. This docker command should run them:
@@ -406,4 +408,40 @@ public function testNullBody()
406408

407409
$this->expectNotToPerformAssertions();
408410
}
411+
412+
/**
413+
* @dataProvider getRedirectWithAuthTests
414+
*/
415+
public function testRedirectWithAuth(string $url, bool $redirectWithAuth)
416+
{
417+
$p = TestHttpServer::start(8067);
418+
419+
try {
420+
$client = $this->getHttpClient(__FUNCTION__);
421+
422+
$response = $client->request('GET', $url, [
423+
'headers' => [
424+
'cookie' => 'foo=bar',
425+
],
426+
]);
427+
$body = $response->toArray();
428+
} finally {
429+
$p->stop();
430+
}
431+
432+
if ($redirectWithAuth) {
433+
$this->assertArrayHasKey('HTTP_COOKIE', $body);
434+
} else {
435+
$this->assertArrayNotHasKey('HTTP_COOKIE', $body);
436+
}
437+
}
438+
439+
public function getRedirectWithAuthTests()
440+
{
441+
return [
442+
'same host and port' => ['url' => 'http://localhost:8057/302', 'redirectWithAuth' => true],
443+
'other port' => ['url' => 'http://localhost:8067/302', 'redirectWithAuth' => false],
444+
'other host' => ['url' => 'http://127.0.0.1:8057/302', 'redirectWithAuth' => false],
445+
];
446+
}
409447
}

0 commit comments

Comments
 (0)