Version in base suite: 5.4.23+dfsg-1+deb12u2 Version in overlay suite: 5.4.23+dfsg-1+deb12u3 Base version: symfony_5.4.23+dfsg-1+deb12u3 Target version: symfony_5.4.23+dfsg-1+deb12u4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/s/symfony/symfony_5.4.23+dfsg-1+deb12u3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/s/symfony/symfony_5.4.23+dfsg-1+deb12u4.dsc changelog | 10 patches/HttpClient-Filter-private-IPs-before-connecting-when-Host.patch | 2 patches/HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch | 285 ++++++++++ patches/security-http-Check-owner-of-persisted-remember-me-cookie.patch | 110 +++ patches/series | 2 5 files changed, 408 insertions(+), 1 deletion(-) diff -Nru symfony-5.4.23+dfsg/debian/changelog symfony-5.4.23+dfsg/debian/changelog --- symfony-5.4.23+dfsg/debian/changelog 2024-11-09 09:22:58.000000000 +0000 +++ symfony-5.4.23+dfsg/debian/changelog 2024-11-14 11:16:18.000000000 +0000 @@ -1,3 +1,13 @@ +symfony (5.4.23+dfsg-1+deb12u4) bookworm-security; urgency=medium + + * Backport security fixes from Symfony 5.4.47 + - [security-http] Check owner of persisted remember-me cookie + [CVE-2024-51996] + - [HttpClient] Resolve hostnames in NoPrivateNetworkHttpClient + [CVE-2024-50342] + + -- David Prévot Thu, 14 Nov 2024 12:16:18 +0100 + symfony (5.4.23+dfsg-1+deb12u3) bookworm-security; urgency=medium * Backport security fixes from Symfony 5.4.46 diff -Nru symfony-5.4.23+dfsg/debian/patches/HttpClient-Filter-private-IPs-before-connecting-when-Host.patch symfony-5.4.23+dfsg/debian/patches/HttpClient-Filter-private-IPs-before-connecting-when-Host.patch --- symfony-5.4.23+dfsg/debian/patches/HttpClient-Filter-private-IPs-before-connecting-when-Host.patch 2024-11-09 09:22:58.000000000 +0000 +++ symfony-5.4.23+dfsg/debian/patches/HttpClient-Filter-private-IPs-before-connecting-when-Host.patch 2024-11-14 11:16:18.000000000 +0000 @@ -2,7 +2,7 @@ Date: Fri, 25 Oct 2024 10:13:01 +0200 Subject: [HttpClient] Filter private IPs before connecting when Host == IP -Origin: upstream, https://symfony.com/blog/cve-2024-50342-internal-address-and-port-enumeration-allowed-by-noprivatenetworkhttpclient +Origin: upstream, https://github.com/symfony/symfony/commit/b4bf5afdbdcb2fd03da513ee03beeabeb551e5fa Bug: https://github.com/symfony/symfony/security/advisories/GHSA-9c3x-r3wp-mgxm Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-50342 --- diff -Nru symfony-5.4.23+dfsg/debian/patches/HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch symfony-5.4.23+dfsg/debian/patches/HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch --- symfony-5.4.23+dfsg/debian/patches/HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch 1970-01-01 00:00:00.000000000 +0000 +++ symfony-5.4.23+dfsg/debian/patches/HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch 2024-11-14 11:16:18.000000000 +0000 @@ -0,0 +1,285 @@ +From: Nicolas Grekas +Date: Fri, 8 Nov 2024 09:23:38 +0100 +Subject: [HttpClient] Resolve hostnames in NoPrivateNetworkHttpClient + +Origin: upstream, https://github.com/symfony/symfony/commit/b4bf5afdbdcb2fd03da513ee03beeabeb551e5fa +Bug: https://github.com/symfony/symfony/security/advisories/GHSA-9c3x-r3wp-mgxm +Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-50342 +--- + src/Symfony/Component/HttpClient/HttpOptions.php | 2 ++ + .../Component/HttpClient/NativeHttpClient.php | 12 ++++++-- + .../HttpClient/NoPrivateNetworkHttpClient.php | 17 +++++++++-- + .../Component/HttpClient/Response/AmpResponse.php | 11 ++++++-- + .../Component/HttpClient/Response/AsyncContext.php | 4 +-- + .../HttpClient/Response/AsyncResponse.php | 4 +-- + .../Component/HttpClient/Response/CurlResponse.php | 11 ++++++-- + .../HttpClient/Tests/HttpClientTestCase.php | 33 ++++++++++++++++++++++ + .../HttpClient/Tests/MockHttpClientTest.php | 5 +++- + .../Component/HttpClient/TraceableHttpClient.php | 4 +-- + .../Contracts/HttpClient/HttpClientInterface.php | 8 ++++-- + 11 files changed, 93 insertions(+), 18 deletions(-) + +diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php +index da55f99..5a178dd 100644 +--- a/src/Symfony/Component/HttpClient/HttpOptions.php ++++ b/src/Symfony/Component/HttpClient/HttpOptions.php +@@ -148,6 +148,8 @@ class HttpOptions + } + + /** ++ * @param callable(int, int, array, \Closure|null=):void $callback ++ * + * @return $this + */ + public function setOnProgress(callable $callback) +diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php +index 63fcc1c..caeacf1 100644 +--- a/src/Symfony/Component/HttpClient/NativeHttpClient.php ++++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php +@@ -138,7 +138,15 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac + // Memoize the last progress to ease calling the callback periodically when no network transfer happens + $lastProgress = [0, 0]; + $maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF; +- $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) { ++ $multi = $this->multi; ++ $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { ++ if (null !== $ip) { ++ $multi->dnsCache[$host] = $ip; ++ } ++ ++ return $multi->dnsCache[$host] ?? null; ++ }; ++ $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration, $resolve) { + if ($info['total_time'] >= $maxDuration) { + throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url']))); + } +@@ -154,7 +162,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac + $lastProgress = $progress ?: $lastProgress; + } + +- $onProgress($lastProgress[0], $lastProgress[1], $progressInfo); ++ $onProgress($lastProgress[0], $lastProgress[1], $progressInfo, $resolve); + }; + } elseif (0 < $options['max_duration']) { + $maxDuration = $options['max_duration']; +diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php +index fcbf833..c7d982f 100644 +--- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php ++++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php +@@ -80,11 +80,24 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa + $lastUrl = ''; + $lastPrimaryIp = ''; + +- $options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void { ++ $options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void { + if ($info['url'] !== $lastUrl) { + $host = trim(parse_url($info['url'], PHP_URL_HOST) ?: '', '[]'); ++ $resolve ??= static fn () => null; ++ ++ if (($ip = $host) ++ && !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) ++ && !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) ++ && !$ip = $resolve($host) ++ ) { ++ if ($ip = @(dns_get_record($host, \DNS_A)[0]['ip'] ?? null)) { ++ $resolve($host, $ip); ++ } elseif ($ip = @(dns_get_record($host, \DNS_AAAA)[0]['ipv6'] ?? null)) { ++ $resolve($host, '['.$ip.']'); ++ } ++ } + +- if ($host && IpUtils::checkIp($host, $subnets ?? self::PRIVATE_SUBNETS)) { ++ if ($ip && IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) { + throw new TransportException(sprintf('Host "%s" is blocked for "%s".', $host, $info['url'])); + } + +diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php +index 6d0ce6e..fdb9533 100644 +--- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php ++++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php +@@ -90,10 +90,17 @@ final class AmpResponse implements ResponseInterface, StreamableInterface + $info['max_duration'] = $options['max_duration']; + $info['debug'] = ''; + ++ $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { ++ if (null !== $ip) { ++ $multi->dnsCache[$host] = $ip; ++ } ++ ++ return $multi->dnsCache[$host] ?? null; ++ }; + $onProgress = $options['on_progress'] ?? static function () {}; +- $onProgress = $this->onProgress = static function () use (&$info, $onProgress) { ++ $onProgress = $this->onProgress = static function () use (&$info, $onProgress, $resolve) { + $info['total_time'] = microtime(true) - $info['start_time']; +- $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info); ++ $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info, $resolve); + }; + + $pauseDeferred = new Deferred(); +diff --git a/src/Symfony/Component/HttpClient/Response/AsyncContext.php b/src/Symfony/Component/HttpClient/Response/AsyncContext.php +index 646458e..952d4b6 100644 +--- a/src/Symfony/Component/HttpClient/Response/AsyncContext.php ++++ b/src/Symfony/Component/HttpClient/Response/AsyncContext.php +@@ -156,8 +156,8 @@ final class AsyncContext + $this->info['previous_info'][] = $info = $this->response->getInfo(); + if (null !== $onProgress = $options['on_progress'] ?? null) { + $thisInfo = &$this->info; +- $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) { +- $onProgress($dlNow, $dlSize, $thisInfo + $info); ++ $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) { ++ $onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve); + }; + } + if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) { +diff --git a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php +index 80c9f7d..a3d006d 100644 +--- a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php ++++ b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php +@@ -51,8 +51,8 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface + + if (null !== $onProgress = $options['on_progress'] ?? null) { + $thisInfo = &$this->info; +- $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) { +- $onProgress($dlNow, $dlSize, $thisInfo + $info); ++ $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) { ++ $onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve); + }; + } + $this->response = $client->request($method, $url, ['buffer' => false] + $options); +diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php +index 7cfad58..3804505 100644 +--- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php ++++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php +@@ -126,13 +126,20 @@ final class CurlResponse implements ResponseInterface, StreamableInterface + curl_pause($ch, \CURLPAUSE_CONT); + + if ($onProgress = $options['on_progress']) { ++ $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { ++ if (null !== $ip) { ++ $multi->dnsCache->hostnames[$host] = $ip; ++ } ++ ++ return $multi->dnsCache->hostnames[$host] ?? null; ++ }; + $url = isset($info['url']) ? ['url' => $info['url']] : []; + curl_setopt($ch, \CURLOPT_NOPROGRESS, false); +- curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) { ++ curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer, $resolve) { + try { + rewind($debugBuffer); + $debug = ['debug' => stream_get_contents($debugBuffer)]; +- $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug); ++ $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug, $resolve); + } catch (\Throwable $e) { + $multi->handlesActivity[(int) $ch][] = null; + $multi->handlesActivity[(int) $ch][] = $e; +diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +index f096e65..00398d9 100644 +--- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php ++++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +@@ -15,6 +15,7 @@ use PHPUnit\Framework\SkippedTestSuiteError; + use Symfony\Component\HttpClient\Exception\ClientException; + use Symfony\Component\HttpClient\Exception\TransportException; + use Symfony\Component\HttpClient\Internal\ClientState; ++use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; + use Symfony\Component\HttpClient\Response\StreamWrapper; + use Symfony\Component\Process\Exception\ProcessFailedException; + use Symfony\Component\Process\Process; +@@ -462,4 +463,36 @@ abstract class HttpClientTestCase extends BaseHttpClientTestCase + + $this->expectNotToPerformAssertions(); + } ++ ++ public function testMisspelledScheme() ++ { ++ $httpClient = $this->getHttpClient(__FUNCTION__); ++ ++ $this->expectException(InvalidArgumentException::class); ++ $this->expectExceptionMessage('Invalid URL: host is missing in "http:/localhost:8057/".'); ++ ++ $httpClient->request('GET', 'http:/localhost:8057/'); ++ } ++ ++ public function testNoPrivateNetwork() ++ { ++ $client = $this->getHttpClient(__FUNCTION__); ++ $client = new NoPrivateNetworkHttpClient($client); ++ ++ $this->expectException(TransportException::class); ++ $this->expectExceptionMessage('Host "localhost" is blocked'); ++ ++ $client->request('GET', 'http://localhost:8888'); ++ } ++ ++ public function testNoPrivateNetworkWithResolve() ++ { ++ $client = $this->getHttpClient(__FUNCTION__); ++ $client = new NoPrivateNetworkHttpClient($client); ++ ++ $this->expectException(TransportException::class); ++ $this->expectExceptionMessage('Host "symfony.com" is blocked'); ++ ++ $client->request('GET', 'http://symfony.com', ['resolve' => ['symfony.com' => '127.0.0.1']]); ++ } + } +diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +index e244c32..9f38940 100644 +--- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php ++++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +@@ -304,7 +304,7 @@ class MockHttpClientTest extends HttpClientTestCase + + switch ($testCase) { + default: +- return new MockHttpClient(function (string $method, string $url, array $options) use ($client) { ++ return new MockHttpClient(function (string $method, string $url, array $options) use ($client, $testCase) { + try { + // force the request to be completed so that we don't test side effects of the transport + $response = $client->request($method, $url, ['buffer' => false] + $options); +@@ -312,6 +312,9 @@ class MockHttpClientTest extends HttpClientTestCase + + return new MockResponse($content, $response->getInfo()); + } catch (\Throwable $e) { ++ if (str_starts_with($testCase, 'testNoPrivateNetwork')) { ++ throw $e; ++ } + $this->fail($e->getMessage()); + } + }); +diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php +index 76c9282..5efe559 100644 +--- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php ++++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php +@@ -58,11 +58,11 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface, + $content = false; + } + +- $options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) { ++ $options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$traceInfo, $onProgress) { + $traceInfo = $info; + + if (null !== $onProgress) { +- $onProgress($dlNow, $dlSize, $info); ++ $onProgress($dlNow, $dlSize, $info, $resolve); + } + }; + +diff --git a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php +index 9c96629..6e5515b 100644 +--- a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php ++++ b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php +@@ -48,9 +48,11 @@ interface HttpClientInterface + 'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not, + // or a stream resource where the response body should be written, + // or a closure telling if/where the response should be buffered based on its headers +- 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort +- // the request; it MUST be called on DNS resolution, on arrival of headers and on +- // completion; it SHOULD be called on upload/download of data and at least 1/s ++ 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info, ?Closure $resolve = null) - throwing any ++ // exceptions MUST abort the request; it MUST be called on connection, on headers and on ++ // completion; it SHOULD be called on upload/download of data and at least 1/s; ++ // if passed, $resolve($host) / $resolve($host, $ip) can be called to read / populate ++ // the DNS cache respectively + 'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution + 'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored + 'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached diff -Nru symfony-5.4.23+dfsg/debian/patches/security-http-Check-owner-of-persisted-remember-me-cookie.patch symfony-5.4.23+dfsg/debian/patches/security-http-Check-owner-of-persisted-remember-me-cookie.patch --- symfony-5.4.23+dfsg/debian/patches/security-http-Check-owner-of-persisted-remember-me-cookie.patch 1970-01-01 00:00:00.000000000 +0000 +++ symfony-5.4.23+dfsg/debian/patches/security-http-Check-owner-of-persisted-remember-me-cookie.patch 2024-11-14 11:16:18.000000000 +0000 @@ -0,0 +1,110 @@ +From: =?utf-8?b?SsOpcsOpbXkgRGVydXNzw6k=?= +Date: Thu, 7 Nov 2024 09:29:25 +0100 +Subject: [security-http] Check owner of persisted remember-me cookie + +Origin: upstream, https://github.com/symfony/symfony/commit/81354d392c5f0b7a52bcbd729d6f82501e94135a +Bug: https://github.com/symfony/symfony/security/advisories/GHSA-cg23-qf8f-62rr +Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-51996 +--- + .../RememberMe/PersistentRememberMeHandler.php | 19 ++++++++++-- + .../RememberMe/PersistentRememberMeHandlerTest.php | 34 ++++++++++++++++++++-- + 2 files changed, 48 insertions(+), 5 deletions(-) + +diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php +index 6e43dbf..486b7bf 100644 +--- a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php ++++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php +@@ -66,9 +66,16 @@ final class PersistentRememberMeHandler extends AbstractRememberMeHandler + throw new AuthenticationException('The cookie is incorrectly formatted.'); + } + +- [$series, $tokenValue] = explode(':', $rememberMeDetails->getValue()); ++ [$series, $tokenValue] = explode(':', $rememberMeDetails->getValue(), 2); + $persistentToken = $this->tokenProvider->loadTokenBySeries($series); + ++ if ($persistentToken->getUserIdentifier() !== $rememberMeDetails->getUserIdentifier() || $persistentToken->getClass() !== $rememberMeDetails->getUserFqcn()) { ++ throw new AuthenticationException('The cookie\'s hash is invalid.'); ++ } ++ ++ // content of $rememberMeDetails is not trustable. this prevents use of this class ++ unset($rememberMeDetails); ++ + if ($this->tokenVerifier) { + $isTokenValid = $this->tokenVerifier->verifyToken($persistentToken, $tokenValue); + } else { +@@ -78,11 +85,17 @@ final class PersistentRememberMeHandler extends AbstractRememberMeHandler + throw new CookieTheftException('This token was already used. The account is possibly compromised.'); + } + +- if ($persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime'] < time()) { ++ $expires = $persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime']; ++ if ($expires < time()) { + throw new AuthenticationException('The cookie has expired.'); + } + +- return parent::consumeRememberMeCookie($rememberMeDetails->withValue($persistentToken->getLastUsed()->getTimestamp().':'.$rememberMeDetails->getValue().':'.$persistentToken->getClass())); ++ return parent::consumeRememberMeCookie(new RememberMeDetails( ++ $persistentToken->getClass(), ++ $persistentToken->getUserIdentifier(), ++ $expires, ++ $persistentToken->getLastUsed()->getTimestamp().':'.$series.':'.$tokenValue.':'.$persistentToken->getClass() ++ )); + } + + public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void +diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php +index 76472b1..33ea98f 100644 +--- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php ++++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php +@@ -80,7 +80,7 @@ class PersistentRememberMeHandlerTest extends TestCase + $this->tokenProvider->expects($this->any()) + ->method('loadTokenBySeries') + ->with('series1') +- ->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min'))) ++ ->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', $lastUsed = new \DateTime('-10 min'))) + ; + + $this->tokenProvider->expects($this->once())->method('updateToken')->with('series1'); +@@ -98,11 +98,41 @@ class PersistentRememberMeHandlerTest extends TestCase + + $this->assertSame($rememberParts[0], $cookieParts[0]); // class + $this->assertSame($rememberParts[1], $cookieParts[1]); // identifier +- $this->assertSame($rememberParts[2], $cookieParts[2]); // expire ++ $this->assertEqualsWithDelta($lastUsed->getTimestamp() + 31536000, (int) $cookieParts[2], 2); // expire + $this->assertNotSame($rememberParts[3], $cookieParts[3]); // value + $this->assertSame(explode(':', $rememberParts[3])[0], explode(':', $cookieParts[3])[0]); // series + } + ++ public function testConsumeRememberMeCookieInvalidOwner() ++ { ++ $this->tokenProvider->expects($this->any()) ++ ->method('loadTokenBySeries') ++ ->with('series1') ++ ->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min'))) ++ ; ++ ++ $rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'jeremy', 360, 'series1:tokenvalue'); ++ ++ $this->expectException(AuthenticationException::class); ++ $this->expectExceptionMessage('The cookie\'s hash is invalid.'); ++ $this->handler->consumeRememberMeCookie($rememberMeDetails); ++ } ++ ++ public function testConsumeRememberMeCookieInvalidValue() ++ { ++ $this->tokenProvider->expects($this->any()) ++ ->method('loadTokenBySeries') ++ ->with('series1') ++ ->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min'))) ++ ; ++ ++ $rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue:somethingelse'); ++ ++ $this->expectException(AuthenticationException::class); ++ $this->expectExceptionMessage('This token was already used. The account is possibly compromised.'); ++ $this->handler->consumeRememberMeCookie($rememberMeDetails); ++ } ++ + public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate() + { + $verifier = $this->createMock(TokenVerifierInterface::class); diff -Nru symfony-5.4.23+dfsg/debian/patches/series symfony-5.4.23+dfsg/debian/patches/series --- symfony-5.4.23+dfsg/debian/patches/series 2024-11-09 09:22:58.000000000 +0000 +++ symfony-5.4.23+dfsg/debian/patches/series 2024-11-14 11:16:18.000000000 +0000 @@ -44,4 +44,6 @@ fix-tests.patch Runtime-fix-tests.patch ErrorHandler-Extend-test-expectation.patch +HttpClient-Resolve-hostnames-in-NoPrivateNetworkHttpClien.patch +security-http-Check-owner-of-persisted-remember-me-cookie.patch HttpClient-Temporary-test-hack.patch