feature #36129 [HttpFoundation][HttpKernel][Security] Improve UnexpectedSessionUsageException backtrace (mtarld)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[HttpFoundation][HttpKernel][Security] Improve UnexpectedSessionUsageException backtrace

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       |
| License       | MIT
| Doc PR        |

Improve `UnexceptedSessionUsageException` backtrace so that it leads to the place in the userland  where it was told to use session.

Commits
-------

1e1d332c7c Improve UnexcpectedSessionUsageException backtrace
This commit is contained in:
Nicolas Grekas 2020-03-31 21:27:28 +02:00
commit 2130465899
10 changed files with 142 additions and 4 deletions

View File

@ -13,6 +13,12 @@
<service id="session" class="Symfony\Component\HttpFoundation\Session\Session" public="true"> <service id="session" class="Symfony\Component\HttpFoundation\Session\Session" public="true">
<argument type="service" id="session.storage" /> <argument type="service" id="session.storage" />
<argument>null</argument> <!-- AttributeBagInterface -->
<argument>null</argument> <!-- FlashBagInterface -->
<argument type="collection">
<argument type="service" id="session_listener" />
<argument>onSessionUsage</argument>
</argument>
</service> </service>
<service id="Symfony\Component\HttpFoundation\Session\SessionInterface" alias="session" /> <service id="Symfony\Component\HttpFoundation\Session\SessionInterface" alias="session" />

View File

@ -14,6 +14,7 @@ CHANGELOG
according to [RFC 8674](https://tools.ietf.org/html/rfc8674) according to [RFC 8674](https://tools.ietf.org/html/rfc8674)
* made the Mime component an optional dependency * made the Mime component an optional dependency
* added `MarshallingSessionHandler`, `IdentityMarshaller` * added `MarshallingSessionHandler`, `IdentityMarshaller`
* made `Session` accept a callback to report when the session is being used
5.0.0 5.0.0
----- -----

View File

@ -35,10 +35,12 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
private $attributeName; private $attributeName;
private $data = []; private $data = [];
private $usageIndex = 0; private $usageIndex = 0;
private $usageReporter;
public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null) public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null)
{ {
$this->storage = $storage ?: new NativeSessionStorage(); $this->storage = $storage ?: new NativeSessionStorage();
$this->usageReporter = $usageReporter;
$attributes = $attributes ?: new AttributeBag(); $attributes = $attributes ?: new AttributeBag();
$this->attributeName = $attributes->getName(); $this->attributeName = $attributes->getName();
@ -153,6 +155,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
{ {
if ($this->isStarted()) { if ($this->isStarted()) {
++$this->usageIndex; ++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
} }
foreach ($this->data as &$data) { foreach ($this->data as &$data) {
if (!empty($data)) { if (!empty($data)) {
@ -229,6 +234,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
public function getMetadataBag() public function getMetadataBag()
{ {
++$this->usageIndex; ++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->storage->getMetadataBag(); return $this->storage->getMetadataBag();
} }
@ -238,7 +246,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function registerBag(SessionBagInterface $bag) public function registerBag(SessionBagInterface $bag)
{ {
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex)); $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter));
} }
/** /**

View File

@ -21,17 +21,22 @@ final class SessionBagProxy implements SessionBagInterface
private $bag; private $bag;
private $data; private $data;
private $usageIndex; private $usageIndex;
private $usageReporter;
public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex) public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter)
{ {
$this->bag = $bag; $this->bag = $bag;
$this->data = &$data; $this->data = &$data;
$this->usageIndex = &$usageIndex; $this->usageIndex = &$usageIndex;
$this->usageReporter = $usageReporter;
} }
public function getBag(): SessionBagInterface public function getBag(): SessionBagInterface
{ {
++$this->usageIndex; ++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->bag; return $this->bag;
} }
@ -42,6 +47,9 @@ final class SessionBagProxy implements SessionBagInterface
return true; return true;
} }
++$this->usageIndex; ++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return empty($this->data[$this->bag->getStorageKey()]); return empty($this->data[$this->bag->getStorageKey()]);
} }
@ -60,6 +68,10 @@ final class SessionBagProxy implements SessionBagInterface
public function initialize(array &$array): void public function initialize(array &$array): void
{ {
++$this->usageIndex; ++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
$this->data[$this->bag->getStorageKey()] = &$array; $this->data[$this->bag->getStorageKey()] = &$array;
$this->bag->initialize($array); $this->bag->initialize($array);

View File

@ -281,7 +281,7 @@ class SessionTest extends TestCase
$bag->setName('foo'); $bag->setName('foo');
$storage = new MockArraySessionStorage(); $storage = new MockArraySessionStorage();
$storage->registerBag(new SessionBagProxy($bag, $data, $usageIndex)); $storage->registerBag(new SessionBagProxy($bag, $data, $usageIndex, null));
$this->assertSame($bag, (new Session($storage))->getBag('foo')); $this->assertSame($bag, (new Session($storage))->getBag('foo'));
} }

View File

@ -6,6 +6,7 @@ CHANGELOG
* allowed using public aliases to reference controllers * allowed using public aliases to reference controllers
* added session usage reporting when the `_stateless` attribute of the request is set to `true` * added session usage reporting when the `_stateless` attribute of the request is set to `true`
* added `AbstractSessionListener::onSessionUsage()` to report when the session is used while a request is stateless
5.0.0 5.0.0
----- -----

View File

@ -146,6 +146,37 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
} }
} }
public function onSessionUsage(): void
{
if (!$this->debug) {
return;
}
if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) {
return;
}
$stateless = false;
$clonedRequestStack = clone $requestStack;
while (null !== ($request = $clonedRequestStack->pop()) && !$stateless) {
$stateless = $request->attributes->get('_stateless');
}
if (!$stateless) {
return;
}
if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $requestStack->getCurrentRequest()->getSession()) {
return;
}
if ($session->isStarted()) {
$session->save();
}
throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
}
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array
{ {
return [ return [

View File

@ -244,4 +244,63 @@ class SessionListenerTest extends TestCase
$this->expectException(UnexpectedSessionUsageException::class); $this->expectException(UnexpectedSessionUsageException::class);
$listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response)); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
} }
public function testSessionUsageCallbackWhenDebugAndStateless()
{
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
$session->method('isStarted')->willReturn(true);
$session->expects($this->exactly(1))->method('save');
$requestStack = new RequestStack();
$request = new Request();
$request->attributes->set('_stateless', true);
$requestStack->push(new Request());
$requestStack->push($request);
$requestStack->push(new Request());
$container = new Container();
$container->set('initialized_session', $session);
$container->set('request_stack', $requestStack);
$this->expectException(UnexpectedSessionUsageException::class);
(new SessionListener($container, true))->onSessionUsage();
}
public function testSessionUsageCallbackWhenNoDebug()
{
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
$session->method('isStarted')->willReturn(true);
$session->expects($this->exactly(0))->method('save');
$request = new Request();
$request->attributes->set('_stateless', true);
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
$requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request);
$container = new Container();
$container->set('initialized_session', $session);
$container->set('request_stack', $requestStack);
(new SessionListener($container))->onSessionUsage();
}
public function testSessionUsageCallbackWhenNoStateless()
{
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
$session->method('isStarted')->willReturn(true);
$session->expects($this->never())->method('save');
$requestStack = new RequestStack();
$requestStack->push(new Request());
$requestStack->push(new Request());
$container = new Container();
$container->set('initialized_session', $session);
$container->set('request_stack', $requestStack);
(new SessionListener($container, true))->onSessionUsage();
}
} }

View File

@ -96,11 +96,14 @@ class ContextListener extends AbstractListener
if (null !== $session) { if (null !== $session) {
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0; $usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0;
$usageIndexReference = PHP_INT_MIN;
$sessionId = $request->cookies->get($session->getName()); $sessionId = $request->cookies->get($session->getName());
$token = $session->get($this->sessionKey); $token = $session->get($this->sessionKey);
if ($this->sessionTrackerEnabler && \in_array($sessionId, [true, $session->getId()], true)) { if ($this->sessionTrackerEnabler && \in_array($sessionId, [true, $session->getId()], true)) {
$usageIndexReference = $usageIndexValue; $usageIndexReference = $usageIndexValue;
} else {
$usageIndexReference = $usageIndexReference - PHP_INT_MIN + $usageIndexValue;
} }
} }

View File

@ -361,6 +361,23 @@ class ContextListenerTest extends TestCase
$this->assertSame($usageIndex, $session->getUsageIndex()); $this->assertSame($usageIndex, $session->getUsageIndex());
} }
public function testSessionIsNotReported()
{
$usageReporter = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
$usageReporter->expects($this->never())->method('__invoke');
$session = new Session(new MockArraySessionStorage(), null, null, $usageReporter);
$request = new Request();
$request->setSession($session);
$request->cookies->set('MOCKSESSID', true);
$tokenStorage = new TokenStorage();
$listener = new ContextListener($tokenStorage, [], 'context_key', null, null, null, [$tokenStorage, 'getToken']);
$listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST));
}
protected function runSessionOnKernelResponse($newToken, $original = null) protected function runSessionOnKernelResponse($newToken, $original = null)
{ {
$session = new Session(new MockArraySessionStorage()); $session = new Session(new MockArraySessionStorage());