Description
Symfony version(s) affected: 4.4.19
Description
Recently I stumbled upon an issue that PHP isn't writing my cookie as "secure" in the output even when I
- am accessing the website through HTTPS;
- have
session.cookie_secure
explicitly set to "On" inphp.ini
, and - have
cookie_secure
set toauto
in Symfony 4.4 (which is the default).
I found this weird, because
- this seems to only affect the PHP session ID cookie but not those written by the
sendHeaders()
method in Symfony'sResponse
object, so obviously Symfony knows it is a secure connection, - this doesn't happens all time - in my setup it only affects the frontend but not the backend, and
- the problem goes away if I explicitly set
cookie_secure
totrue
inframework.yaml
.
After several hours of debugging, I have tracked down the cause. Here's how it happens:
- When
NativeSessionStorage
is created, it calls ini_set() to replace the session-related configurations with those defined in the framework config. The values are written as-is, so whencookie_secure
is set toauto
inframework.yaml
, thisauto
will be passed toini_set
. Here's the problem -auto
is not a valid value for this configuration, and will probably be treated asOff
orFalse
. Which is why havingsession.cookie_secure
set to "On" inphp.ini
won't help in my situation. - Fortunately, this invalid value
auto
normally won't stay forever. The problem is fixed bySymfony\Component\HttpKernel\EventListener\SessionListener
, which is activated when the framework'scookie_secure
is set toauto
.SessionListener
checks if the request is a secure one and if yes, it sets thecookie_secure
PHP option to true, thus overriding the previous incorrect value that will be intepretated asfalse
.
Here is the tricky part. The reason the session ID cookie isn't properly written as secure
even when the website is accessed through HTTPS is because in the frontend, I have an Authenticator that checks for a specific session key in the supports
method. This method is executed so early in the request cycle such that the session is started before SessionListener
is run. When this happens, it is too late for SessionListener
to set the cookie_secure
PHP option to true
because NativeSessionStorage's setOption() will exit immediately if the session has been started. Now the auto
value will stay till the end, which I believe will be treated as false
by PHP, so the PHPSESSID's cookie will not have the secure
attribute.
I don't see any warning that one should not access the session in the guard authenticator's supports()
method. In fact I can find suggestion of "only return true in supports() if the user is on the correct URL and if that session key exists" in the comment section of a SymfonyCasts page, which is exactly what I was doing (the website I work on contains a frontend and backend which operate with different authenticators, and I need a bridge between them, thus the need for checking a particular session value). So I believe I am not doing something unwise.
I understand that I may have stumbled upon an edge case, and probably not much people will be affected by this. Also it's not difficult to work around this problem, I just need to defer the session checking until getUser()
is called. Or I can just explicitly set cookie_secure
to true
to avoid this glitch. But since the consequence is security related (cookie_secure
flag is not turned on even when it should), I think it would be better for me to report this issue anyway and mark it as a bug report first and let your team decide what to do with it.
Thank you!
Update 19 Feb
After some more tests, it looks like the problem is probably a more wide-spreaded one. session.cookie_secure
isn't set properly (i.e. stays in "auto") even for the following controller code in a newly set up Symfony 4.4 and 5.2 project:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
class MainController
{
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
/**
* @Route("/")
*/
public function index(): Response
{
$this->session->set('foo', 'bar');
return new Response(ini_get('session.cookie_secure'));
}
}