Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

Commit 89929cb

Browse filesBrowse the repository at this point in the historyBrowse files
Merge branch '7.4' into 8.0
* 7.4: [JsonPath] Add limits for filter expression length and depth in `JsonCrawler` [Serializer] Check the denormalized class is a Mime part in MimeMessageNormalizer [JsonStreamer] Drop the unbounded process-global key cache in Splitter [SecurityBundle] Restrict redirections to the current host when sessions are disabled [HttpFoundation] Fix TypeError in UriSigner when the hash parameter is not a string [HttpFoundation] Reject reserved characters in the cookie path and domain [HttpClient] Reject https:// proxies that curl would connect to in cleartext [HttpFoundation] Fix newline handling in server event fields [HttpClient] Strip Proxy-Authorization on cross-authority redirects [Messenger] Fix keepalive test with DBAL 3
2 parents 7a58195 + 4063da6 commit 89929cb
Copy full SHA for 89929cb

25 files changed

+631-51Lines changed: 631 additions & 51 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ class AddSessionDomainConstraintPass implements CompilerPassInterface
2323
{
2424
public function process(ContainerBuilder $container): void
2525
{
26-
if (!$container->hasParameter('session.storage.options') || !$container->has('security.http_utils')) {
26+
if (!$container->has('security.http_utils')) {
2727
return;
2828
}
2929

30-
$sessionOptions = $container->getParameter('session.storage.options');
30+
// Without sessions, fall back to restricting redirections to the current host
31+
$sessionOptions = $container->hasParameter('session.storage.options') ? $container->getParameter('session.storage.options') : [];
3132
$domainRegexp = empty($sessionOptions['cookie_domain']) ? '%%s' : \sprintf('(?:%%%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.')));
3233

3334
if ('auto' === ($sessionOptions['cookie_secure'] ?? null)) {
Collapse file

‎src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php
+17-5Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\ContainerBuilder;
2222
use Symfony\Component\DependencyInjection\Definition;
2323
use Symfony\Component\HttpFoundation\Request;
24+
use Symfony\Component\Security\Http\HttpUtils;
2425

2526
class AddSessionDomainConstraintPassTest extends TestCase
2627
{
@@ -92,12 +93,23 @@ public function testNoSession()
9293
$utils = $container->get('security.http_utils');
9394
$request = Request::create('/', 'get');
9495

95-
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog'));
96-
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog'));
96+
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('http://localhost/'));
97+
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('http://localhost/'));
9798
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
98-
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('https://www.localhost/foo'));
99-
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://symfony.com/blog'));
100-
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://pirate.com/foo'));
99+
$this->assertTrue($utils->createRedirectResponse($request, 'http://localhost/foo')->isRedirect('http://localhost/foo'));
100+
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/'));
101+
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/'));
102+
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/'));
103+
}
104+
105+
public function testNoSessionInjectsCurrentHostConstraint()
106+
{
107+
$container = new ContainerBuilder();
108+
$container->register('security.http_utils', HttpUtils::class);
109+
110+
(new AddSessionDomainConstraintPass())->process($container);
111+
112+
$this->assertSame(['{^https?://%%s$}i', null], $container->getDefinition('security.http_utils')->getArguments());
101113
}
102114

103115
public function testSessionAutoSecure()
Collapse file

‎src/Symfony/Component/HttpClient/CurlHttpClient.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/CurlHttpClient.php
+42-10Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public function request(string $method, string $url, array $options = []): Respo
9494
$host = parse_url($authority, \PHP_URL_HOST);
9595
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
9696
$proxy = self::getProxyUrl($options['proxy'], $url);
97+
$noProxy = $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
98+
self::checkHttpsProxySupport($proxy, $url, $noProxy);
9799
$url = implode('', $url);
98100

99101
if (!isset($options['normalized_headers']['user-agent'])) {
@@ -109,8 +111,9 @@ public function request(string $method, string $url, array $options = []): Respo
109111
\CURLOPT_MAXREDIRS => max(0, $options['max_redirects']),
110112
\CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
111113
\CURLOPT_TIMEOUT => 0,
112-
\CURLOPT_PROXY => $proxy,
113-
\CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
114+
// Always set, so that curl doesn't resolve the proxy from its own environment
115+
\CURLOPT_PROXY => $proxy ?? '',
116+
\CURLOPT_NOPROXY => $noProxy,
114117
\CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
115118
\CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
116119
\CURLOPT_CAINFO => $options['cafile'],
@@ -330,7 +333,7 @@ public function request(string $method, string $url, array $options = []): Respo
330333
}
331334
}
332335

333-
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $authority), CurlClientState::$curlVersion['version_number'], $url, $ntlmOriginKey);
336+
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $authority, $noProxy), CurlClientState::$curlVersion['version_number'], $url, $ntlmOriginKey);
334337
}
335338

336339
public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface
@@ -407,21 +410,21 @@ private static function readRequestBody(int $length, \Closure $body, string &$bu
407410
/**
408411
* Resolves relative URLs on redirects and deals with authentication headers.
409412
*
410-
* Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
413+
* Work around CVE-2018-1000007: Authorization, Cookie and Proxy-Authorization headers should not follow redirects - fixed in Curl 7.64
411414
*/
412-
private static function createRedirectResolver(array $options, string $authority): \Closure
415+
private static function createRedirectResolver(array $options, string $authority, string $noProxy): \Closure
413416
{
414417
$redirectHeaders = [];
415418
if (0 < $options['max_redirects']) {
416419
$redirectHeaders['authority'] = $authority;
417420
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static fn ($h) => 0 !== stripos($h, 'Host:'));
418421

419-
if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
420-
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'));
422+
if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0]) || isset($options['normalized_headers']['proxy-authorization'][0])) {
423+
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:') && 0 !== stripos($h, 'Proxy-Authorization:'));
421424
}
422425
}
423426

424-
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
427+
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options, $noProxy) {
425428
try {
426429
$location = self::parseUrl($location);
427430
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
@@ -444,16 +447,45 @@ private static function createRedirectResolver(array $options, string $authority
444447
}
445448

446449
$proxy = self::getProxyUrl($options['proxy'], $url);
447-
curl_setopt($ch, \CURLOPT_PROXY, $proxy);
450+
self::checkHttpsProxySupport($proxy, $url, $noProxy);
451+
curl_setopt($ch, \CURLOPT_PROXY, $proxy ?? '');
448452

449-
if (\defined('CURL_HTTP_VERSION_3') && \CURL_HTTP_VERSION_3 === curl_getinfo($ch, \CURLINFO_HTTP_VERSION) && self::willUseProxy($proxy, $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '', parse_url($url['authority'], \PHP_URL_HOST))) {
453+
if (\defined('CURL_HTTP_VERSION_3') && \CURL_HTTP_VERSION_3 === curl_getinfo($ch, \CURLINFO_HTTP_VERSION) && self::willUseProxy($proxy, $noProxy, parse_url($url['authority'], \PHP_URL_HOST))) {
450454
curl_setopt($ch, \CURLOPT_HTTP_VERSION, \defined('CURL_HTTP_VERSION_2_0') ? \CURL_HTTP_VERSION_2_0 : \CURL_HTTP_VERSION_1_1);
451455
}
452456

453457
return implode('', $url);
454458
};
455459
}
456460

461+
/**
462+
* Rejects "https://" proxies that curl cannot connect to over TLS.
463+
*
464+
* Curl older than 7.50.2 connects to them in cleartext instead, leaking the proxy
465+
* credentials and the CONNECT metadata on the wire.
466+
*/
467+
private static function checkHttpsProxySupport(?string $proxy, array $url, string $noProxy): void
468+
{
469+
if (null === $proxy || 0 !== stripos($proxy, 'https://')) {
470+
return;
471+
}
472+
473+
if (CurlClientState::$curlVersion['features'] & (\defined('CURL_VERSION_HTTPS_PROXY') ? \CURL_VERSION_HTTPS_PROXY : 1 << 21)) {
474+
return;
475+
}
476+
477+
$host = parse_url($url['authority'], \PHP_URL_HOST);
478+
479+
// Matching "no_proxy" should follow the behavior of curl
480+
foreach (preg_split('/[\s,]+/', $noProxy, -1, \PREG_SPLIT_NO_EMPTY) as $rule) {
481+
if ('*' === $rule || $host === $rule || str_ends_with($host, '.'.ltrim($rule, '.'))) {
482+
return;
483+
}
484+
}
485+
486+
throw new TransportException('Cannot use an "https://" proxy: the installed curl does not support HTTPS proxies and could connect to it in cleartext; curl 7.52 or higher is required.');
487+
}
488+
457489
private function findConstantName(int $opt): ?string
458490
{
459491
$constants = array_filter(get_defined_constants(), static fn ($v, $k) => $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_')), \ARRAY_FILTER_USE_BOTH);
Collapse file

‎src/Symfony/Component/HttpClient/NativeHttpClient.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/NativeHttpClient.php
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ private static function createRedirectResolver(array $options, string $authority
385385
$redirectHeaders = ['authority' => $authority];
386386
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static fn ($h) => 0 !== stripos($h, 'Host:'));
387387

388-
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
389-
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'));
388+
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie']) || isset($options['normalized_headers']['proxy-authorization'])) {
389+
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:') && 0 !== stripos($h, 'Proxy-Authorization:'));
390390
}
391391
}
392392

@@ -436,7 +436,7 @@ private static function createRedirectResolver(array $options, string $authority
436436
[$host, $port] = self::parseHostPort($url, $info);
437437

438438
if ($locationHasHost) {
439-
// Authorization and Cookie headers MUST NOT follow except for the initial authority name
439+
// Authorization, Cookie and Proxy-Authorization headers MUST NOT follow except for the initial authority name
440440
$requestHeaders = $redirectHeaders['authority'] === $url['authority'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
441441
$requestHeaders[] = 'Host: '.$host.$port;
442442
$dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
Collapse file

‎src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ public function request(string $method, string $url, array $options = []): Respo
100100
$options['max_redirects'] = 0;
101101
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];
102102

103-
if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
104-
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'));
103+
if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie']) || isset($options['normalized_headers']['proxy-authorization'])) {
104+
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static fn ($h) => 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:') && 0 !== stripos($h, 'Proxy-Authorization:'));
105105
}
106106

107107
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator {
@@ -138,7 +138,7 @@ public function request(string $method, string $url, array $options = []): Respo
138138
}
139139
}
140140

141-
// Authorization and Cookie headers MUST NOT follow except for the initial host name
141+
// Authorization, Cookie and Proxy-Authorization headers MUST NOT follow except for the initial host name
142142
$port = parse_url($url, \PHP_URL_PORT);
143143
$options['headers'] = $redirectHeaders['host'] === $host && ($redirectHeaders['port'] ?? null) === $port ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
144144

Collapse file

‎src/Symfony/Component/HttpClient/Response/AmpResponse.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Response/AmpResponse.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ private static function followRedirects(Request $originRequest, AmpClientState $
354354
if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
355355
$request->removeHeader('authorization');
356356
$request->removeHeader('cookie');
357+
$request->removeHeader('proxy-authorization');
357358
$request->removeHeader('host');
358359
}
359360

Collapse file

‎src/Symfony/Component/HttpClient/Response/CurlResponse.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Response/CurlResponse.php
+11-1Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,17 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
442442
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
443443
}
444444

445-
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
445+
try {
446+
$info['redirect_url'] = $resolveRedirect($ch, $location, $noContent);
447+
} catch (TransportException $e) {
448+
// Exceptions must be reported through the response, they cannot escape a curl callback
449+
$multi->handlesActivity[$id][] = null;
450+
$multi->handlesActivity[$id][] = $e;
451+
452+
return 0;
453+
}
454+
455+
if (null === $info['redirect_url']) {
446456
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
447457
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
448458
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
Collapse file

‎src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\HttpClient\Tests;
1313

14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\Attributes\Group;
1516
use Symfony\Component\HttpClient\AmpHttpClient;
1617
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -51,4 +52,14 @@ public function testProxy()
5152
{
5253
$this->markTestSkipped('A real proxy server would be needed.');
5354
}
55+
56+
#[DataProvider('getRedirectWithAuthTests')]
57+
public function testRedirectWithProxyAuthorization(string $url, bool $redirectWithAuth)
58+
{
59+
if ($redirectWithAuth) {
60+
$this->markTestSkipped('AmpHttpClient never forwards Proxy-Authorization to the target host.');
61+
}
62+
63+
parent::testRedirectWithProxyAuthorization($url, $redirectWithAuth);
64+
}
5465
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.