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

Skip to content

[Security] Do not remove existing session on stateless requests #57854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

[Security] Do not remove existing session on stateless requests #57854

wants to merge 1 commit into from

Conversation

micheh
Copy link
Contributor

@micheh micheh commented Jul 28, 2024

Q A
Branch? 5.4
Bug fix? yes
New feature? no
Deprecations? no
Issues Fix #57851
License MIT

This fixes the bug where an existing session was removed on stateless requests, forcing the user to reauthenticate after visiting a route with stateless: true.

@micheh micheh changed the base branch from 6.4 to 5.4 July 28, 2024 09:29
@MatTheCat
Copy link
Contributor

It always seemed weird to me #57372 added a stateless check in the ContextListener because it would be an error to have it running in a stateless context, but now this error is hidden.

Wouldn’t it be better to remove said check?

$session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null;

Ping @VincentLanglet

@micheh
Copy link
Contributor Author

micheh commented Jul 28, 2024

@MatTheCat Before the change in #57372, Symfony will try to fetch the token from the session and refresh the user (even if the request is stateless). The problem now is that the change only updated the kernel.request event listener and left the kernel.response event listener as is. Due to the absence of the kernel.request event listener, no token is set and the kernel.response event listener therefore removes the session.

Maybe another option would be to revert the change made to the ContextListener in #57372 and instead return false in the supports method for stateless requests? This would have the same result that both event listeners would no longer do anything.

@VincentLanglet
Copy link
Contributor

It always seemed weird to me #57372 added a stateless check in the ContextListener because it would be an error to have it running in a stateless context, but now this error is hidden.

When I tried to debug #57372, I got error because the session was used and it wasn't voluntary. I don't remember having done anything to use the ContextListener.

Wouldn’t it be better to remove said check?

$session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null;

Either the check should be kept and some others fixes are required, either something else need to be find in order to not trigger this listener on stateless request.

Maybe another option would be to revert the change made to the ContextListener in #57372 and instead return false in the supports method for stateless requests? This would have the same result that both event listeners would no longer do anything.

I like this idea @micheh.

@MatTheCat
Copy link
Contributor

I don't remember having done anything to use the ContextListener.

Your firewall was stateful, which didn't match the route configured as stateless.

AFAIU the point of stateless requests was to warn you if you were using the session, not to stop Symfony from using it.

@VincentLanglet
Copy link
Contributor

Your firewall was stateful, which didn't match the route configured as stateless.

I don't think so, I never declared a route as stateless, I just had firewall stateful (and route stateful) or firewall stateless (and route stateless).

AFAIU the point of stateless requests was to warn you if you were using the session, not to stop Symfony from using it.

I don't think so, the check done in the AbstractSessionListener

if ($this->debug) {
throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
}
if ($this->container->has('logger')) {
$this->container->get('logger')->warning('Session was used while the request was declared stateless.');
}

doesn't check if the session was use by Symfony or the developer.

That's why there was already previous fix as #54742 or #51350.

When the request is stateless, IMHO the session is supposed to never be used. Which lead some questions like #57502 (comment) about throwing an exception in request::getSession for stateless requests...

@MatTheCat
Copy link
Contributor

MatTheCat commented Jul 28, 2024

I never declared a route as stateless, I just had firewall stateful (and route stateful) or firewall stateless (and route stateless).

Then I don’t see how you could get an error coming from the ContextListener 🤔

the check done in the AbstractSessionListener doesn't check if the session was use by Symfony or the developer.

I don’t think there is a need for that. But, as a user, you can trigger session usage by Symfony: like when you have a stateful firewall, the ContextListener will always run. On the other hand, as a user you cannot do anything against the WebProfilerBundle using the session or not.

In the first case I’d prefer to be warned because the session was used because of me.
In the second, the WebProfilerBundle should be updated to work, the request being configured stateless or not.

Considering every case as the second means you end up hiding developers’ mistakes, which I don’t think is wise 🤷‍♂️

@VincentLanglet
Copy link
Contributor

VincentLanglet commented Jul 28, 2024

I never declared a route as stateless, I just had firewall stateful (and route stateful) or firewall stateless (and route stateless).

Then I don’t see how you could get an error coming from the ContextListener 🤔

Honestly I dunno either, I just tried and didn't reproduce but I don't remember about the time I debug this and I surely had a reason because I didn't add the stateless check on every hasSession call. But still the usecase of micheh should be valid no ? A stateless request is possible on a stateful firewall.

Then, wouldn't the best to have the following method supports in the ContextListener

     public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_stateless') ? false : null;
    }

WDYT about updating your PR @micheh ?

@MatTheCat
Copy link
Contributor

A stateless request is possible on a stateful firewall.

Well, no 😅

A stateful firewall will (de)serialize the current user using the session. If you're saying a request matching this firewall is stateless, you either misconfigured the request, or the firewall.

@VincentLanglet
Copy link
Contributor

A stateless request is possible on a stateful firewall.

Well, no 😅

A stateful firewall will (de)serialize the current user using the session. If you're saying a request matching this firewall is stateless, you either misconfigured the request, or the firewall.

Indeed, my bad.

Anyway, I agree the stateless check should be reverted and then I'll see if have session usage.
If not it's perfect, if yes and it's coming from symfony, the support method might need to be updated.

Should this PR be updated or do you want to open another PR @MatTheCat ?

@MatTheCat
Copy link
Contributor

I guess this PR can be updated since its goal stays the same?

@micheh
Copy link
Contributor Author

micheh commented Jul 29, 2024

A stateful firewall will (de)serialize the current user using the session. If you're saying a request matching this firewall is stateless, you either misconfigured the request, or the firewall.

I disagree. It should be possible to have some stateless routes in a stateful firewall. Routes can configure a stateless boolean option in order to declare that the session shouldn't be used when matching a request. This includes fetching the user from the session, which the ContextListener does. The option exists for this reason on the route itself to allow stateless routes in a stateful firewall.

If you revert the changes in #57372 it will throw an exception, because the listener will try to fetch the user from the session, which is not allowed in a stateless route. With the change in #57372 and in this PR, it will prevent the ContextListener to use the session, which means there is no more exception. But it will still throw an exception if you use the session yourself, because this logic is in the AbstractSessionListener that remains unchanged. Therefore it wont hide developer mistakes, but update Symfony to respect the stateless setting correctly in the ContextListener.

@VincentLanglet
Copy link
Contributor

VincentLanglet commented Jul 29, 2024

If you revert the changes in #57372 it will throw an exception, because the listener will try to fetch the user from the session, which is not allowed in a stateless route. With the change in #57372 and in this PR, it will prevent the ContextListener to use the session, which means there is no more exception. But it will still throw an exception if you use the session yourself, because this logic is in the AbstractSessionListener that remains unchanged. Therefore it wont hide developer mistakes, but update Symfony to respect the stateless setting correctly in the ContextListener.

Then wouldn't it better to return false in the ContextListener::support method when the request is stateless ?

@MatTheCat
Copy link
Contributor

MatTheCat commented Jul 29, 2024

Well it all boils down if you consider stateless as a check or as a directive.

The blog post which introduced it reads

Routes can now configure a stateless boolean option. If set to true, they declare that session won't be used during the handling of the request.

Note that it is written declare, not force.

Following your suggestion, there wouldn’t be any difference between a stateful and stateless firewall. Feels weird to me (also, it probably would be a new feature more than a bugfix 🤔).

(I’m unsubscribing from this thread because we’re running in circles.)

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 13, 2024

I agree with @MatTheCat about the intention of the _stateless attribute. The changes in this PR and in #57372 are changing the semantics of the attribute.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 13, 2024

Stateful and Lazy firewalls should be compatible with _stateless routes BTW.

@VincentLanglet
Copy link
Contributor

VincentLanglet commented Aug 13, 2024

I agree with @MatTheCat about the intention of the _stateless attribute. The changes in this PR and in #57372 are changing the semantics of the attribute.

Then should #57372 be reverted @nicolas-grekas ?

@nicolas-grekas
Copy link
Member

Yes, we should consider reverting indeed. And reconsidering the original issue from this more-accurate angle.

@stof
Copy link
Member

stof commented Aug 13, 2024

-1 for this PR. If a firewall is configured as being stateful, the ContextListener should always run (as that's exactly what a stateful firewall is about).
The right fix is to revert the change done in the ContextListener in https://github.com/symfony/symfony/pull/57372/files (other changes in the profiler code are fine, as the web debug toolbar needs to be compatible with stateless requests as it is a debug tool running on all pages)

If a route is explicitly configured as being stateless, it must either not perform any security checks (to never trigger lazy authentication) or perform it using a stateless firewall. Otherwise it cannot be stateless.

@VincentLanglet
Copy link
Contributor

#58002 @nicolas-grekas @stof

(other changes in the profiler code are fine, as the web debug toolbar needs to be compatible with stateless requests as it is a debug tool running on all pages)

If a route is explicitly configured as being stateless, it must either not perform any security checks (to never trigger lazy authentication) or perform it using a stateless firewall. Otherwise it cannot be stateless.

Then #57852 would still be useful IMHO

@stof
Copy link
Member

stof commented Aug 13, 2024

Then #57852 would still be useful IMHO

I don't think so.

Removing the security check in your controller because you marked the request as stateless does not make any sense. If your controller needs a security check, it should perform it. And then, if the security check involved using the session, you will get an exception (in debug mode) or a warning in logs telling you that you marked a request as stateless while it is not, which means a mistake in your project (and then, the right fix is either to change your firewall to be stateless or to allow the request to be stateful).

I don't see a lot of use cases for explicit checking on $request->isStateless() outside debug tools like the profiler (and custom collectors have no reason to read the session as we already cover this with the core collector), the dedicated check in the session listener and the special logic we have (in the core as well) for the smart default of that state for stateless firewalls.
cases where you mark a request as stateless or stateful explicitly will likely need to know it is done through an attribute anyway as the standard way to do that is by putting the information in the route definition (relying on the route defaults to declare the attribute).

I would even say that introducing Request::isStateless() could push developers towards the wrong path for all cases where they are not expected to check it.

@VincentLanglet
Copy link
Contributor

Then #57852 would still be useful IMHO

I don't think so.

Removing the security check in your controller because you marked the request as stateless does not make any sense. [...]

I don't see a lot of use cases for explicit checking on $request->isStateless() outside debug tools like the profiler [...]

I would even say that introducing Request::isStateless() could push developers towards the wrong path for all cases where they are not expected to check it.

I personally have multiple use-cases in our code base:

  • When looking at the flash messages doc (cf https://symfony.com/doc/current/session.html#flash-messages), call to {% for message in app.flashes('notice') %} is done in the base.html.twig template. When used by a stateless request it add a usage of the session. Since having two base templates is not really convenient, it's better to have a stateless check inside the twig template before looking for flashes. (Or better symfony could add this check inside his code maybe)

  • We're having a service which add the session id to the Monolog\LogRecord and this should be skipped when the request is stateless or it will add a session usage.

  • We're using some listeners onKernelResponse/onKernelRequest which are doing multiple things and one of these things is setting/getting a value in the session. So we've added a stateless check to skip this step for stateless requests but keep the rest of the logic.

@micheh
Copy link
Contributor Author

micheh commented Aug 13, 2024

Stateful and Lazy firewalls should be compatible with _stateless routes BTW.

Currently they are not compatible because TokenStorage::getToken() is called in some event listeners, which will instruct the ContextListener to try to fetch the user from the session (resulting in an exception if the user is available in the session). When the debug mode is disabled, this will also refresh the user and if the session is not valid anymore for some reason (e.g. timeout, roles changed etc.), the ContextListener will remove the session and turn an otherwise stateless request/response into a stateful one.

-1 for this PR. If a firewall is configured as being stateful, the ContextListener should always run (as that's exactly what a stateful firewall is about). The right fix is to revert the change done in the ContextListener in https://github.com/symfony/symfony/pull/57372/files (other changes in the profiler code are fine, as the web debug toolbar needs to be compatible with stateless requests as it is a debug tool running on all pages)

If a route is explicitly configured as being stateless, it must either not perform any security checks (to never trigger lazy authentication) or perform it using a stateless firewall. Otherwise it cannot be stateless.

TokenStorage::getToken() is not necessarily used only in security checks. For example, the SecurityDataCollector will try to get the token.

} elseif (null === $token = $this->tokenStorage->getToken()) {

Third party event listeners like Sentry also check if a token is available in order to enrich the logs with user information (if it is avaialble). The event listener of the Blameable Doctrine Extension also fetches the token on each request.

In those event listeners it would be possible to only call getToken() if the request is not stateless. But then again, authenticators in stateless firewalls may also set the token. Should every consumer of TokenStorage::getToken() have the knowledge when this method can be called without an exception/warning?

@nicolas-grekas
Copy link
Member

There is a service called security.untracked_token_storage for use cases like Sentry's. I guess this is what should be used in most if not all these situations. To be confirmed of course.

fabpot added a commit that referenced this pull request Aug 14, 2024
…centLanglet)

This PR was merged into the 5.4 branch.

Discussion
----------

[Security] Revert stateless check for ContextListener

This reverts commit 40341a1.

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix ##57851
| License       | MIT

Closes #57854

Cf #57854 (comment)

Commits
-------

188e2d2 Revert stateless check
@fabpot
Copy link
Member

fabpot commented Aug 14, 2024

Fixed by #58002

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants