diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index 08b6faac0e7e9..ef0aa721d3904 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -140,11 +140,12 @@ public function onKernelResponse(ResponseEvent $event) */ $sessionName = $session->getName(); $sessionId = $session->getId(); - $sessionCookiePath = $this->sessionOptions['cookie_path'] ?? '/'; - $sessionCookieDomain = $this->sessionOptions['cookie_domain'] ?? null; - $sessionCookieSecure = $this->sessionOptions['cookie_secure'] ?? false; - $sessionCookieHttpOnly = $this->sessionOptions['cookie_httponly'] ?? true; - $sessionCookieSameSite = $this->sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; + $sessionOptions = $this->getSessionOptions($this->sessionOptions); + $sessionCookiePath = $sessionOptions['cookie_path'] ?? '/'; + $sessionCookieDomain = $sessionOptions['cookie_domain'] ?? null; + $sessionCookieSecure = $sessionOptions['cookie_secure'] ?? false; + $sessionCookieHttpOnly = $sessionOptions['cookie_httponly'] ?? true; + $sessionCookieSameSite = $sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; SessionUtils::popSessionCookie($sessionName, $sessionId); @@ -162,7 +163,7 @@ public function onKernelResponse(ResponseEvent $event) ); } elseif ($sessionId !== $requestSessionCookieId) { $expire = 0; - $lifetime = $this->sessionOptions['cookie_lifetime'] ?? null; + $lifetime = $sessionOptions['cookie_lifetime'] ?? null; if ($lifetime) { $expire = time() + $lifetime; } @@ -280,4 +281,23 @@ public function reset(): void * @return SessionInterface|null */ abstract protected function getSession(); + + private function getSessionOptions(array $sessionOptions): array + { + $mergedSessionOptions = []; + + foreach (session_get_cookie_params() as $key => $value) { + $mergedSessionOptions['cookie_'.$key] = $value; + } + + foreach ($sessionOptions as $key => $value) { + // do the same logic as in the NativeSessionStorage + if ('cookie_secure' === $key && 'auto' === $value) { + continue; + } + $mergedSessionOptions[$key] = $value; + } + + return $mergedSessionOptions; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index 9924c27d11af9..27aaf98301de8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -33,6 +34,99 @@ class SessionListenerTest extends TestCase { + /** + * @dataProvider provideSessionOptions + * @runInSeparateProcess + */ + public function testSessionCookieOptions(array $phpSessionOptions, array $sessionOptions, array $expectedSessionOptions) + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); + $session->expects($this->exactly(1))->method('save'); + $session->expects($this->exactly(1))->method('isStarted')->willReturn(true); + + if (isset($phpSessionOptions['samesite'])) { + ini_set('session.cookie_samesite', $phpSessionOptions['samesite']); + } + session_set_cookie_params(0, $phpSessionOptions['path'] ?? null, $phpSessionOptions['domain'] ?? null, $phpSessionOptions['secure'] ?? null, $phpSessionOptions['httponly'] ?? null); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container, false, $sessionOptions); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertSame('PHPSESSID', $cookies[0]->getName()); + $this->assertSame('123456', $cookies[0]->getValue()); + $this->assertSame($expectedSessionOptions['cookie_path'], $cookies[0]->getPath()); + $this->assertSame($expectedSessionOptions['cookie_domain'], $cookies[0]->getDomain()); + $this->assertSame($expectedSessionOptions['cookie_secure'], $cookies[0]->isSecure()); + $this->assertSame($expectedSessionOptions['cookie_httponly'], $cookies[0]->isHttpOnly()); + $this->assertSame($expectedSessionOptions['cookie_samesite'], $cookies[0]->getSameSite()); + } + + public function provideSessionOptions(): \Generator + { + if (\PHP_VERSION_ID > 70300) { + yield 'set_samesite_by_php' => [ + 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_STRICT], + ]; + } + + yield 'set_cookie_path_by_php' => [ + 'phpSessionOptions' => ['path' => '/prod/'], + 'sessionOptions' => ['cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/prod/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_secure_by_php' => [ + 'phpSessionOptions' => ['secure' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookiesecure_auto_by_symfony_false_by_php' => [ + 'phpSessionOptions' => ['secure' => false], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => false, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookiesecure_auto_by_symfony_true_by_php' => [ + 'phpSessionOptions' => ['secure' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_httponly_by_php' => [ + 'phpSessionOptions' => ['httponly' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_domain_by_php' => [ + 'phpSessionOptions' => ['domain' => 'test.symfony'], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => 'test.symfony', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_samesite_by_symfony' => [ + 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + } + public function testOnlyTriggeredOnMainRequest() { $listener = $this->getMockForAbstractClass(AbstractSessionListener::class); @@ -160,10 +254,10 @@ public function testSessionSaveAndResponseHasSessionCookie() $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); $request = new Request(); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); - $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertSame('PHPSESSID', $cookies[0]->getName());