Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c77a645

Browse files
committed
bug #47273 [HttpFoundation] Do not send Set-Cookie header twice for deleted session cookie (X-Coder264)
This PR was merged into the 5.4 branch. Discussion ---------- [HttpFoundation] Do not send Set-Cookie header twice for deleted session cookie | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #47228 | License | MIT | Doc PR | - In a user logout flow `\Symfony\Component\Security\Http\EventListener\SessionLogoutListener` gets triggered which invalidates the user session by calling `session_regenerate_id(true)` which calls `\Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy` which calls `setcookie($this->sessionName, '', $params);`. Calling `setcookie` with the second argument (the value) being an empty string signals to PHP that we want to send a `Set-Cookie` HTTP header to the client so that the client deletes the cookie (aka the expiry time of the cookie gets set to some time in the past) and PHP also sets the value of that cookie to `deleted` -> https://github.com/php/php-src/blob/php-8.1.9/ext/standard/head.c#L124 The `\Symfony\Component\HttpKernel\EventListener\AbstractSessionListener` class did not handle this case as `\Symfony\Component\HttpKernel\EventListener\AbstractSessionListener::onKernelResponse` method would call `$response->headers->clearCookie()` without calling the `popSessionCookie` method which would then add this same cookie again which would then result in two `Set-Cookie` headers being sent to the client for the same cookie. Commits ------- b08025d Do not send deleted session cookie twice in the response
2 parents 91b7bdd + b08025d commit c77a645

File tree

4 files changed

+78
-0
lines changed

4 files changed

+78
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
Array
3+
(
4+
[0] => Content-Type: text/plain; charset=utf-8
5+
[1] => Cache-Control: max-age=0, private, must-revalidate
6+
[2] => Cache-Control: max-age=0, must-revalidate, private
7+
[3] => Date: Sat, 12 Nov 1955 20:04:00 GMT
8+
[4] => Expires: %s, %d %s %d %d:%d:%d GMT
9+
[5] => Set-Cookie: PHPSESSID=deleted; expires=%s, %d-%s-%d %d:%d:%d GMT; Max-Age=%d; %s
10+
)
11+
shutdown
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Container;
4+
use Symfony\Component\HttpFoundation\Request;
5+
use Symfony\Component\HttpFoundation\RequestStack;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\HttpFoundation\Session\SessionFactory;
8+
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorageFactory;
9+
use Symfony\Component\HttpKernel\Event\RequestEvent;
10+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
11+
use Symfony\Component\HttpKernel\EventListener\SessionListener;
12+
use Symfony\Component\HttpKernel\HttpKernelInterface;
13+
14+
/** @var Response $r */
15+
$r = require __DIR__.'/common.inc';
16+
17+
$sessionId = 'vqd4dpbtst3af0k4sdl18nebkn';
18+
session_id($sessionId);
19+
$sessionName = session_name();
20+
$_COOKIE[$sessionName] = $sessionId;
21+
22+
$request = new Request();
23+
$request->cookies->set($sessionName, $sessionId);
24+
25+
$requestStack = new RequestStack();
26+
$requestStack->push($request);
27+
28+
$sessionFactory = new SessionFactory($requestStack, new NativeSessionStorageFactory());
29+
30+
$container = new Container();
31+
$container->set('request_stack', $requestStack);
32+
$container->set('session_factory', $sessionFactory);
33+
34+
$listener = new SessionListener($container);
35+
36+
$kernel = new class($r) implements HttpKernelInterface {
37+
/**
38+
* @var Response
39+
*/
40+
private $response;
41+
42+
public function __construct(Response $response)
43+
{
44+
$this->response = $response;
45+
}
46+
47+
public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
48+
{
49+
return $this->response;
50+
}
51+
};
52+
53+
$listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST));
54+
$session = $request->getSession();
55+
$session->set('foo', 'bar');
56+
$session->invalidate();
57+
58+
$listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $r));
59+
60+
$r->sendHeaders();

src/Symfony/Component/HttpFoundation/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"require-dev": {
2525
"predis/predis": "~1.0",
2626
"symfony/cache": "^4.4|^5.0|^6.0",
27+
"symfony/dependency-injection": "^5.4|^6.0",
28+
"symfony/http-kernel": "^5.4.12|^6.1.4",
2729
"symfony/mime": "^4.4|^5.0|^6.0",
2830
"symfony/expression-language": "^4.4|^5.0|^6.0"
2931
},

src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ public function onKernelResponse(ResponseEvent $event)
158158

159159
$isSessionEmpty = $session->isEmpty() && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions
160160
if ($requestSessionCookieId && $isSessionEmpty) {
161+
// PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument
162+
// which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy
163+
// when the session gets invalidated (for example on logout) so we must handle this case here too
164+
// otherwise we would send two Set-Cookie headers back with the response
165+
SessionUtils::popSessionCookie($sessionName, 'deleted');
161166
$response->headers->clearCookie(
162167
$sessionName,
163168
$sessionCookiePath,

0 commit comments

Comments
 (0)