8000 B/C break in http-kernel / EventListener / AbstractSessionListener in Symfony 3.4 TLS · Issue #29151 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content
8000

B/C break in http-kernel / EventListener / AbstractSessionListener in Symfony 3.4 TLS #29151

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
mice-sk opened this issue Nov 8, 2018 · 7 comments

Comments

@mice-sk
Copy link
mice-sk commented Nov 8, 2018

Symfony version(s) affected: 3.4.x

Description
Symfony 3.4 changed behaviour of AbstractSessionListener when a session cookie is present (regardless of authentication state).

Old behaviour was to respect Cache-Control headers set on response. New behaviour introduces a B/C break that overrides Cache-Control headers in a way that cannot be easily changed. This can have a non-trivial performance impact as it prevents websites to set proper cache headers even on user-agnostic content.

A fix for this was introduced in Symfony 4.1, but no solution is easily available for Symfony 3.4.

How to reproduce

  1. take symfony standard edition
  2. add controller method that sets Cache-Control: public, s-maxage=86400 on response
  3. load page, verify that cache header from step 2 is unmodified by Symfony
  4. add session cookie with random value via browser developer tools
  5. re-open page to see that Symfony rewrote headers to Cache-Control: max-age=0, must-revalidate, private, s-maxage=1234

Possible Solution

One of:

  1. Revert the change in Symfony 3.4 - it's an undocumented B/C break in minor version (and LTS version) that is in conflict with Symfony's backwards compatibility promise.
  2. Backport fix from Symfony 4.1.
  3. Refactor logic to not rely on session cookie presence only - cookie sent by user agent might be spoofed or session may have been invalidated by backend already. A check whether user is authenticated (remembered) should be added too. This would be reasonable change to make in Symfony 4.1+ too.

Additional context

Respective code:

@nicolas-grekas
Copy link
Member

Hi, thanks for reporting. This is not a BC break but a bug fix.
See eg #25826 #25736 or #25448 for discussions on the topic.
The code in 4.1 is a new feature, not a bug fix.

@mice-sk
Copy link
Author
mice-sk commented Nov 15, 2018

Hi @nicolas-grekas,
my apologies for the late reply.

Thank you for linking the issues, my google-fu apparently failed me while researching this initially. I agree that this is a good default, however let me explain why I do consider this a B/C break in 3.4/LTS.

As per comments in other tickets, HttpKernel is not aware of application-specific logic (hence the reasonable default), but consider a controller method as follows:

<?php

namespace AppBundle\Controller;

class HomepageController extends \Symfony\Bundle\FrameworkBundle\Controller\Controller
{
    public function indexAction()
    {
        $viewData = [];
        // some business logic

        $response = $this->render('index.html.twig', $viewData);

        // user-agnostic page (even for authenticated users) - we can set long-term cache headers here
        $response->setPublic();
        $response->setMaxAge(86400);
        $response->setSharedMaxAge(86400);

        return $response;
    }
}

The above scenario is perfectly valid, but is of course specific per application.

The change introduced to AbstractSessionListener in 3.4 overrides this with:

$event->getResponse()
    ->setPrivate()
    ->setMaxAge(0)
    ->headers->addCacheControlDirective('must-revalidate');

... thus breaking what was working fine prior to 3.4.

Note that I'm not arguing that the new default is incorrect (on the contrary). I'm arguing that we should not override headers set by the application - we should do so only when no values were set, i.e. modify AbstractSessionListener as in Symfony 4.1 (requiring applications to disable the automatic behaviour), or add checks that verify that the application did not set any custom headers itself:

$response = $event->getResponse();
if(!$reponse->headers->hasCacheControlDirective())
{
    $response
        ->setPrivate()
        ->setMaxAge(0)
        ->headers->addCacheControlDirective('must-revalidate');
}

Thank you

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Nov 15, 2018

Doing so can lead to severe security issues: a user-specific value can be put in a shared HTTP cache. That's why it shouldn't be that easy to override the behavior.

@mice-sk
Copy link
Author
mice-sk commented Nov 15, 2018

I don't disagree. Severity will vary depending on the application, i.e. in our case a loose flash message (with no PII) is the only thing I can image that could ever end there (and even that would be extremely rare).

So if we don't want to backport AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER from Symfony 4.1, is there a working/official mechanism to disable this behavior in Symfony 3.4 LTS, please?

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Nov 15, 2018

Using a decorating listener yes, see FriendsOfSymfony/FOSHttpCacheBundle#435

@mice-sk
Copy link
Author
mice-sk commented Nov 15, 2018

That's awesome! Thank you Nicolas.

@sustmi
Copy link
Contributor
sustmi commented Jan 16, 2019

Just a note for others: I have an action similar to the one presented in: #29151 (comment) .

The action itself does not use session but because of a firewall configuration in my security.yml (similar to this: https://symfony.com/doc/current/security/ldap.html#configuration-example-for-form-login ), Symfony's ContextListener was always accessing session variable with key _security_{firewallName} (https://github.com/symfony/symfony/blob/v3.4.20/src/Symfony/Component/Security/Http/Firewall/ContextListener.php#L89).

I made a workaround by disabling the security for the action:

firewalls:
    public:
        pattern: ^/path-that-should-be-public/
        security: false

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

No branches or pull requests

5 participants
0