diff --git a/CHANGELOG-3.4.md b/CHANGELOG-3.4.md index 4b307918d34b4..37f4a76efce85 100644 --- a/CHANGELOG-3.4.md +++ b/CHANGELOG-3.4.md @@ -7,6 +7,19 @@ in 3.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.4.0...v3.4.1 +* 3.4.11 (2018-05-25) + + * bug #27364 [DI] Fix bad exception on uninitialized references to non-shared services (nicolas-grekas) + * bug #27359 [HttpFoundation] Fix perf issue during MimeTypeGuesser intialization (nicolas-grekas) + * security #cve-2018-11408 [SecurityBundle] Fail if security.http_utils cannot be configured + * security #cve-2018-11406 clear CSRF tokens when the user is logged out + * security #cve-2018-11385 migrating session for UsernamePasswordJsonAuthenticationListener + * security #cve-2018-11385 Adding session authentication strategy to Guard to avoid session fixation + * security #cve-2018-11385 Adding session strategy to ALL listeners to avoid *any* possible fixation + * security #cve-2018-11386 [HttpFoundation] Break infinite loop in PdoSessionHandler when MySQL is in loose mode + * bug #27341 [WebProfilerBundle] Fixed validator/dump trace CSS (yceruto) + * bug #27337 [FrameworkBundle] fix typo in CacheClearCommand (emilielorenzo) + * 3.4.10 (2018-05-21) * bug #27264 [Validator] Use strict type in URL validator (mimol91) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bc4ebf6f78139..672246e7f9da8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,9 +17,9 @@ Symfony is the result of the work of many people who made the code better - Johannes S (johannes) - Jakub Zalas (jakubzalas) - Kris Wallsmith (kriswallsmith) + - Maxime Steinhausser (ogizanagi) - Ryan Weaver (weaverryan) - Javier Eguiluz (javier.eguiluz) - - Maxime Steinhausser (ogizanagi) - Grégoire Pineau (lyrixx) - Hugo Hamon (hhamon) - Abdellatif Ait boudad (aitboudad) @@ -35,17 +35,17 @@ Symfony is the result of the work of many people who made the code better - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) - Igor Wiedler (igorw) + - Samuel ROZE (sroze) - Jules Pietri (heah) - Eriksen Costa (eriksencosta) - Guilhem Niot (energetick) - Sarah Khalil (saro0h) - - Samuel ROZE (sroze) + - Yonel Ceruto (yonelceruto) - Jonathan Wage (jwage) - Hamza Amrouche (simperfit) - Diego Saint Esteben (dosten) - - Yonel Ceruto (yonelceruto) - - Alexandre Salomé (alexandresalome) - Iltar van der Berg (kjarli) + - Alexandre Salomé (alexandresalome) - William Durand (couac) - ornicar - Francis Besset (francisbesset) @@ -59,9 +59,9 @@ Symfony is the result of the work of many people who made the code better - Henrik Bjørnskov (henrikbjorn) - Dany Maillard (maidmaid) - Miha Vrhovnik + - Kevin Bond (kbond) - Tobias Nyholm (tobias) - Diego Saint Esteben (dii3g0) - - Kevin Bond (kbond) - Konstantin Kudryashov (everzet) - Alexander M. Turek (derrabus) - Bilal Amarni (bamarni) @@ -83,12 +83,12 @@ Symfony is the result of the work of many people who made the code better - Dariusz Górecki (canni) - Issei Murasawa (issei_m) - Douglas Greenshields (shieldo) + - David Maicher (dmaicher) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - Graham Campbell (graham) - Daniel Holmes (dholmes) - - David Maicher (dmaicher) - Dariusz Ruminski - Toni Uebernickel (havvg) - Bart van den Burg (burgov) @@ -103,9 +103,9 @@ Symfony is the result of the work of many people who made the code better - Maxime STEINHAUSSER - Michal Piotrowski (eventhorizon) - Tim Nagel (merk) + - Grégoire Paris (greg0ire) - Brice BERNARD (brikou) - Baptiste Clavié (talus) - - Grégoire Paris (greg0ire) - marc.weistroff - lenar - Alexander Schwenn (xelaris) @@ -139,21 +139,21 @@ Symfony is the result of the work of many people who made the code better - Sebastian Hörl (blogsh) - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) + - Jérôme Vasseur (jvasseur) + - Valentin Udaltsov (vudaltsov) + - gadelat (gadelat) - Guilherme Blanco (guilhermeblanco) - Pablo Godel (pgodel) - - Jérôme Vasseur (jvasseur) - Jérémie Augustin (jaugustin) - Andréia Bohner (andreia) - Philipp Wahala (hifi) - Julien Falque (julienfalque) - Rafael Dohms (rdohms) - Arnaud Kleinpeter (nanocom) - - gadelat (gadelat) - jwdeitch - Teoh Han Hui (teohhanhui) - Mikael Pajunen - Joel Wurtz (brouznouf) - - Valentin Udaltsov (vudaltsov) - Chris Wilkinson (thewilkybarkid) - Oleg Voronkovich - Vyacheslav Pavlov @@ -226,6 +226,7 @@ Symfony is the result of the work of many people who made the code better - Julien Brochet (mewt) - Leo Feyer - Tristan Darricau (nicofuma) + - Nikolay Labinskiy (e-moe) - Michaël Perrin (michael.perrin) - Marcel Beerta (mazen) - Loïc Faugeron @@ -260,6 +261,7 @@ Symfony is the result of the work of many people who made the code better - Kristen Gilden (kgilden) - Pierre-Yves LEBECQ (pylebecq) - Jordan Samouh (jordansamouh) + - Baptiste Lafontaine (magnetik) - Jakub Kucharovic (jkucharovic) - Uwe Jäger (uwej711) - Eugene Leonovich (rybakit) @@ -270,7 +272,6 @@ Symfony is the result of the work of many people who made the code better - Jan Sorgalla (jsor) - Ray - Tyson Andre - - Nikolay Labinskiy (e-moe) - Chekote - Thomas Adam - Albert Casademont (acasademont) @@ -286,6 +287,7 @@ Symfony is the result of the work of many people who made the code better - Oskar Stark (oskarstark) - Thomas Lallement (raziel057) - Giorgio Premi + - Christian Schmidt - Beau Simensen (simensen) - Michael Hirschler (mvhirsch) - Robert Kiss (kepten) @@ -317,7 +319,6 @@ Symfony is the result of the work of many people who made the code better - Jerzy Zawadzki (jzawadzki) - Wouter J - Ismael Ambrosi (iambrosi) - - Baptiste Lafontaine - François Pluchino (francoispluchino) - Aurelijus Valeiša (aurelijus) - Jan Decavele (jandc) @@ -361,6 +362,7 @@ Symfony is the result of the work of many people who made the code better - Yaroslav Kiliba - Terje Bråten - Mathieu Lechat + - MatTheCat - Robbert Klarenbeek (robbertkl) - JhonnyL - David Badura (davidbadura) @@ -424,7 +426,6 @@ Symfony is the result of the work of many people who made the code better - Jeanmonod David (jeanmonod) - Christopher Davis (chrisguitarguy) - Jan Schumann - - Christian Schmidt - Niklas Fiekas - Markus Bachmann (baachi) - lancergr @@ -437,6 +438,7 @@ Symfony is the result of the work of many people who made the code better - Josip Kruslin - Asmir Mustafic (goetas) - vagrant + - Aurimas Niekis (gcds) - EdgarPE - Florian Pfitzer (marmelatze) - Asier Illarramendi (doup) @@ -514,6 +516,7 @@ Symfony is the result of the work of many people who made the code better - De Cock Xavier (xdecock) - Almog Baku (almogbaku) - Scott Arciszewski + - Xavier HAUSHERR - Norbert Orzechowicz (norzechowicz) - Denis Charrier (brucewouaigne) - Matthijs van den Bos (matthijs) @@ -529,7 +532,6 @@ Symfony is the result of the work of many people who made the code better - Dawid Pakuła (zulusx) - Florian Rey (nervo) - Rodrigo Borrego Bernabé (rodrigobb) - - MatTheCat - Denis Gorbachev (starfall) - Peter van Dommelen - Tim van Densen @@ -563,6 +565,7 @@ Symfony is the result of the work of many people who made the code better - Mantas Var (mvar) - Sebastian Krebs - Jean-Christophe Cuvelier [Artack] + - Simon DELICATA - alcaeus - Fred Cox - vitaliytv @@ -580,6 +583,7 @@ Symfony is the result of the work of many people who made the code better - James Johnston - Sinan Eldem - Alexandre Dupuy (satchette) + - Malte Blättermann - Andre Rømcke (andrerom) - Nahuel Cuesta (ncuesta) - Chris Boden (cboden) @@ -604,7 +608,6 @@ Symfony is the result of the work of many people who made the code better - Michal Trojanowski - David Fuhr - Kamil Kokot (pamil) - - Aurimas Niekis (gcds) - Max Grigorian (maxakawizard) - mcfedr (mcfedr) - Rostyslav Kinash @@ -721,6 +724,7 @@ Symfony is the result of the work of many people who made the code better - Adam Szaraniec (mimol) - Yosmany Garcia (yosmanyga) - Wouter de Wild + - Antoine M (amakdessi) - Degory Valentine - izzyp - Benoit Lévêque (benoit_leveque) @@ -730,6 +734,7 @@ Symfony is the result of the work of many people who made the code better - Xavier Lacot (xavier) - possum - Denis Zunke (donalberto) + - Philipp Cordes - Ahmed TAILOULOUTE (ahmedtai) - Olivier Maisonneuve (olineuve) - Masterklavi @@ -749,7 +754,6 @@ Symfony is the result of the work of many people who made the code better - Adrien Lucas (adrienlucas) - Zhuravlev Alexander (scif) - James Michael DuPont - - Xavier HAUSHERR - Tom Klingenberg - Christopher Hall (mythmakr) - Patrick Dawkins (pjcdawkins) @@ -802,6 +806,7 @@ Symfony is the result of the work of many people who made the code better - corphi - grizlik - Derek ROTH + - Ben Johnson - Dmytro Boiko (eagle) - Shin Ohno (ganchiku) - Geert De Deckere (geertdd) @@ -882,6 +887,7 @@ Symfony is the result of the work of many people who made the code better - Michael Tibben - Billie Thompson - Sander Marechal + - Icode4Food (icode4food) - Radosław Benkel - jean pasqualini (darkilliant) - Ross Motley (rossmotley) @@ -993,6 +999,7 @@ Symfony is the result of the work of many people who made the code better - DerManoMann - Olaf Klischat - orlovv + - Jonathan Hedstrom - Peter Smeets (darkspartan) - Jhonny Lidfors (jhonny) - Julien Bianchi (jubianchi) @@ -1005,7 +1012,6 @@ Symfony is the result of the work of many people who made the code better - Andrew Tch - Alexander Cheprasov - Rodrigo Díez Villamuera (rodrigodiez) - - Malte Blättermann - e-ivanov - Jochen Bayer (jocl) - Alex Bowers @@ -1092,9 +1098,11 @@ Symfony is the result of the work of many people who made the code better - Tobias Stöckler - Mario Young - Ilia (aliance) + - Chris McCafferty (cilefen) - Grégoire Penverne (gpenverne) - Mo Di (modi) - Pablo Schläpfer + - Gert de Pagter - Jelte Steijaert (jelte) - Quique Porta (quiqueporta) - stoccc @@ -1177,9 +1185,9 @@ Symfony is the result of the work of many people who made the code better - Andreas Frömer - Philip Frank - Lance McNearney - - Antoine M (amakdessi) - Gonzalo Vilaseca (gonzalovilaseca) - Giorgio Premi + - ncou - Ian Carroll - caponica - Matt Daum (daum) @@ -1197,7 +1205,6 @@ Symfony is the result of the work of many people who made the code better - Tadcka - Beth Binkovitz - Gonzalo Míguez - - Philipp Cordes - Pierre Rineau - Romain Geissler - Adrien Moiruad @@ -1352,6 +1359,7 @@ Symfony is the result of the work of many people who made the code better - Pablo Maria Martelletti (pmartelletti) - Yassine Guedidi (yguedidi) - Waqas Ahmed + - Bert Hekman - Luis Muñoz - Matthew Donadio - Houziaux mike @@ -1450,6 +1458,7 @@ Symfony is the result of the work of many people who made the code better - Yannick Warnier (ywarnier) - Kevin Decherf - Jason Woods + - Oleg Andreyev - klemens - dened - Dmitry Korotovsky @@ -1508,6 +1517,7 @@ Symfony is the result of the work of many people who made the code better - Pierre Rineau - Maxim Lovchikov - adenkejawen + - Florent SEVESTRE (aniki-taicho) - Ari Pringle (apringle) - Dan Ordille (dordille) - Jan Eichhorn (exeu) @@ -1759,7 +1769,6 @@ Symfony is the result of the work of many people who made the code better - Matt Janssen - Ben Miller - Peter Gribanov - - Ben Johnson - kwiateusz - David Soria Parra - Sergiy Sokolenko @@ -1889,6 +1898,7 @@ Symfony is the result of the work of many people who made the code better - Julien Sanchez (sumbobyboys) - Guillermo Gisinger (t3chn0r) - Markus Tacker (tacker) + - Tarmo Leppänen (tarlepp) - Tyler Stroud (tystr) - Moritz Kraft (userfriendly) - Víctor Mateo (victormateo) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 650f36dc15b71..25ba58f13fdf3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -148,7 +148,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ('/' === \DIRECTORY_SEPARATOR && $mounts = @file('/proc/mounts')) { foreach ($mounts as $mount) { $mount = array_slice(explode(' ', $mount), 1, -3); - if (!\in_array(array_pop($mount), array('vboxfs', 'nfs'))) { + if (!\in_array(array_pop($mount), array('vboxsf', 'nfs'))) { continue; } $mount = implode(' ', $mount).'/'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php index 3dd18944de9f3..ba523382b66ba 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php @@ -26,7 +26,7 @@ class AddSessionDomainConstraintPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - if (!$container->hasParameter('session.storage.options') || !$container->has('security.http_utils')) { + if (!$container->hasParameter('session.storage.options')) { return; } @@ -34,6 +34,7 @@ public function process(ContainerBuilder $container) $domainRegexp = empty($sessionOptions['cookie_domain']) ? '%s' : sprintf('(?:%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.'))); $domainRegexp = (empty($sessionOptions['cookie_secure']) ? 'https?://' : 'https://').$domainRegexp; + // if the service doesn't exist, an exception must be thrown - ignoring would put security at risk $container->findDefinition('security.http_utils')->addArgument(sprintf('{^%s$}i', $domainRegexp)); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php new file mode 100644 index 0000000000000..d4d28ecc4eb35 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Christian Flothmann + */ +class RegisterCsrfTokenClearingLogoutHandlerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->has('security.logout_listener') || !$container->has('security.csrf.token_storage')) { + return; + } + + $csrfTokenStorage = $container->findDefinition('security.csrf.token_storage'); + $csrfTokenStorageClass = $container->getParameterBag()->resolveValue($csrfTokenStorage->getClass()); + + if (!is_subclass_of($csrfTokenStorageClass, 'Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface')) { + return; + } + + $container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler') + ->addArgument(new Reference('security.csrf.token_storage')) + ->setPublic(false); + + $container->findDefinition('security.logout_listener')->addMethodCall('addHandler', array(new Reference('security.logout.handler.csrf_token_clearing'))); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 0304c165a4bca..9bed3b3be11d3 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory; use Symfony\Component\Console\Application; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -60,7 +61,8 @@ public function build(ContainerBuilder $container) $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); - $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING); + $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass()); } public function registerCommands(Application $application) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php index 5cce32ea9377a..5eae4388dc2f8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php @@ -18,7 +18,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Tests\Authorization\Stub\VoterWithoutInterface; class AddSecurityVotersPassTest extends TestCase { @@ -122,3 +121,10 @@ public function testVoterMissingInterfaceAndMethod() $compilerPass->process($container); } } + +class VoterWithoutInterface +{ + public function vote() + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php index 382bdebe018fa..a836ab136cd93 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php @@ -96,6 +96,19 @@ public function testNoSession() $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://pirate.com/foo')); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @expectedExceptionMessage You have requested a non-existent service "security.http_utils". + */ + public function testNoHttpUtils() + { + $container = new ContainerBuilder(); + $container->setParameter('session.storage.options', array()); + + $pass = new AddSessionDomainConstraintPass(); + $pass->process($container); + } + private function createContainer($sessionStorageOptions) { $container = new ContainerBuilder(); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php index 7eeb7c21171ce..ddea9dc1742fe 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php @@ -31,4 +31,22 @@ public function testSessionLessRememberMeLogout() $this->assertNull($cookieJar->get('REMEMBERME')); } + + public function testCsrfTokensAreClearedOnLogout() + { + $client = $this->createClient(array('test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml')); + static::$kernel->getContainer()->get('test.security.csrf.token_storage')->setToken('foo', 'bar'); + + $client->request('POST', '/login', array( + '_username' => 'johannes', + '_password' => 'test', + )); + + $this->assertTrue(static::$kernel->getContainer()->get('test.security.csrf.token_storage')->hasToken('foo')); + $this->assertSame('bar', static::$kernel->getContainer()->get('test.security.csrf.token_storage')->getToken('foo')); + + $client->request('GET', '/logout'); + + $this->assertFalse(static::$kernel->getContainer()->get('test.security.csrf.token_storage')->hasToken('foo')); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php new file mode 100644 index 0000000000000..d90f774abde2b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return array( + new FrameworkBundle(), + new SecurityBundle(), +); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml new file mode 100644 index 0000000000000..1a4ade82b9d4c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml @@ -0,0 +1,31 @@ +imports: + - { resource: ./../config/framework.yml } + +services: + test.security.csrf.token_storage: + alias: security.csrf.token_storage + public: true + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + remember_me: + always_remember_me: true + secret: secret + logout: + invalidate_session: false + anonymous: ~ + stateless: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/routing.yml new file mode 100644 index 0000000000000..1dddfca2f8154 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/routing.yml @@ -0,0 +1,5 @@ +login: + path: /login + +logout: + path: /logout diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 1d0858c2def97..d158443896f87 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^5.5.9|>=7.0.8", "ext-xml": "*", - "symfony/security": "~3.4.10|^4.0.10", + "symfony/security": "~3.4.11|^4.0.11", "symfony/dependency-injection": "^3.4.3|^4.0.3", "symfony/http-kernel": "~3.4|~4.0", "symfony/polyfill-php70": "~1.0" @@ -46,7 +46,7 @@ "twig/twig": "~1.34|~2.4" }, "conflict": { - "symfony/security": "4.1.0-beta1", + "symfony/security": "4.1.0-beta1|4.1.0-beta2", "symfony/var-dumper": "<3.3", "symfony/event-dispatcher": "<3.4", "symfony/framework-bundle": "<3.4", diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 96cd8878a8091..40138d07ffb17 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -898,6 +898,7 @@ table.logs .metadata { background: #FFF; padding: 10px; margin: 0.5em 0; + overflow: auto; } #collector-content .sf-validator .trace { font-size: 12px; @@ -929,6 +930,7 @@ table.logs .metadata { background: #FFF; padding: 10px; margin: 0.5em 0; + overflow: auto; } #collector-content pre.sf-dump, diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 7ffedd3dc0523..d33c376df1d9e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -11,7 +11,6 @@ namespace Symfony\Component\DependencyInjection\Compiler; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -31,9 +30,6 @@ protected function processValue($value, $isRoot = false) if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() && !$this->container->has($id = (string) $value)) { throw new ServiceNotFoundException($id, $this->currentId); } - if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() && $this->container->has($id = (string) $value) && !$this->container->findDefinition($id)->isShared()) { - throw new InvalidArgumentException(sprintf('Invalid ignore-on-uninitialized reference found in service "%s": target service "%s" is not shared.', $this->currentId, $id)); - } return $value; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index a79e78b6b2a46..69fca11bd2e07 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -138,6 +138,6 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe return false; } - return $this->container->getDefinition($ids[0])->isShared(); + return !$ids || $this->container->getDefinition($ids[0])->isShared(); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index a92e11e503c49..d640b9a2351d1 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1896,6 +1896,9 @@ private function getServiceCall($id, Reference $reference = null) if ($this->container->hasDefinition($id) && ($definition = $this->container->getDefinition($id)) && !$definition->isSynthetic()) { if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) { $code = 'null'; + if (!$definition->isShared()) { + return $code; + } } elseif ($this->isTrivialInstance($definition)) { $code = substr($this->addNewInstance($definition, '', '', $id), 8, -2); if ($definition->isShared()) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php index ac002d834d1c5..a3fbfcf10132f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -68,27 +68,6 @@ public function testProcessThrowsExceptionOnInvalidReferenceFromInlinedDefinitio $this->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid ignore-on-uninitialized reference found in service - */ - public function testProcessThrowsExceptionOnNonSharedUninitializedReference() - { - $container = new ContainerBuilder(); - - $container - ->register('a', 'stdClass') - ->addArgument(new Reference('b', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)) - ; - - $container - ->register('b', 'stdClass') - ->setShared(false) - ; - - $this->process($container); - } - public function testProcessDefinitionWithBindings() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/Form/Test/Traits/ValidatorExtensionTrait.php b/src/Symfony/Component/Form/Test/Traits/ValidatorExtensionTrait.php index 0724f697ba77a..d5bcb04a4df6d 100644 --- a/src/Symfony/Component/Form/Test/Traits/ValidatorExtensionTrait.php +++ b/src/Symfony/Component/Form/Test/Traits/ValidatorExtensionTrait.php @@ -31,7 +31,7 @@ protected function getValidatorExtension() } $this->validator = $this->getMockBuilder(ValidatorInterface::class)->getMock(); - $metadata = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock(); + $metadata = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->setMethods(array('addPropertyConstraint'))->getMock(); $this->validator->expects($this->any())->method('getMetadataFor')->will($this->returnValue($metadata)); $this->validator->expects($this->any())->method('validate')->will($this->returnValue(array())); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php index 1b77e1ff6accb..a2b4e2ef9fb6b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php @@ -22,6 +22,7 @@ public function test2Dot5ValidationApi() ->disableOriginalConstructor() ->getMock(); $metadata = $this->getMockBuilder('Symfony\Component\Validator\Mapping\ClassMetadata') + ->setMethods(array('addConstraint', 'addPropertyConstraint')) ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php b/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php index e3ef45ef672cf..d78c76068234f 100644 --- a/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php +++ b/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php @@ -80,13 +80,8 @@ public static function reset() */ private function __construct() { - if (FileBinaryMimeTypeGuesser::isSupported()) { - $this->register(new FileBinaryMimeTypeGuesser()); - } - - if (FileinfoMimeTypeGuesser::isSupported()) { - $this->register(new FileinfoMimeTypeGuesser()); - } + $this->register(new FileBinaryMimeTypeGuesser()); + $this->register(new FileinfoMimeTypeGuesser()); } /** @@ -125,18 +120,14 @@ public function guess($path) throw new AccessDeniedException($path); } - if (!$this->guessers) { - $msg = 'Unable to guess the mime type as no guessers are available'; - if (!FileinfoMimeTypeGuesser::isSupported()) { - $msg .= ' (Did you enable the php_fileinfo extension?)'; - } - throw new \LogicException($msg); - } - foreach ($this->guessers as $guesser) { if (null !== $mimeType = $guesser->guess($path)) { return $mimeType; } } + + if (2 === \count($this->guessers) && !FileBinaryMimeTypeGuesser::isSupported() && !FileinfoMimeTypeGuesser::isSupported()) { + throw new \LogicException('Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?)'); + } } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 19b636252e32c..37ec757149cd8 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -616,6 +616,7 @@ protected function doRead($sessionId) $selectSql = $this->getSelectSql(); $selectStmt = $this->pdo->prepare($selectSql); $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $insertStmt = null; do { $selectStmt->execute(); @@ -631,6 +632,11 @@ protected function doRead($sessionId) return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; } + if (null !== $insertStmt) { + $this->rollback(); + throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); + } + if (!ini_get('session.use_strict_mode') && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { // In strict mode, session fixation is not possible: new sessions always start with a unique // random id, so that concurrency is not possible and this code path can be skipped. diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index abd7f74a03c87..ca0f0b95c7a8c 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -67,11 +67,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '3.4.10'; - const VERSION_ID = 30410; + const VERSION = '3.4.11'; + const VERSION_ID = 30411; const MAJOR_VERSION = 3; const MINOR_VERSION = 4; - const RELEASE_VERSION = 10; + const RELEASE_VERSION = 11; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '11/2020'; diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php index 66197c95ac2d2..c8d31093ceff2 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php @@ -113,4 +113,32 @@ public function testRemoveExistingToken() $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); $this->assertFalse($this->storage->hasToken('token_id')); } + + public function testClearRemovesAllTokensFromTheConfiguredNamespace() + { + $this->storage->setToken('foo', 'bar'); + $this->storage->clear(); + + $this->assertFalse($this->storage->hasToken('foo')); + $this->assertArrayNotHasKey(self::SESSION_NAMESPACE, $_SESSION); + } + + public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces() + { + $_SESSION['foo']['bar'] = 'baz'; + $this->storage->clear(); + + $this->assertArrayHasKey('foo', $_SESSION); + $this->assertArrayHasKey('bar', $_SESSION['foo']); + $this->assertSame('baz', $_SESSION['foo']['bar']); + } + + public function testClearDoesNotRemoveNonNamespacedSessionValues() + { + $_SESSION['foo'] = 'baz'; + $this->storage->clear(); + + $this->assertArrayHasKey('foo', $_SESSION); + $this->assertSame('baz', $_SESSION['foo']); + } } diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php index 306e19ad91bb9..7539852f13f3f 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -129,4 +129,31 @@ public function testRemoveExistingTokenFromActiveSession() $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); } + + public function testClearRemovesAllTokensFromTheConfiguredNamespace() + { + $this->storage->setToken('foo', 'bar'); + $this->storage->clear(); + + $this->assertFalse($this->storage->hasToken('foo')); + $this->assertFalse($this->session->has(self::SESSION_NAMESPACE.'/foo')); + } + + public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces() + { + $this->session->set('foo/bar', 'baz'); + $this->storage->clear(); + + $this->assertTrue($this->session->has('foo/bar')); + $this->assertSame('baz', $this->session->get('foo/bar')); + } + + public function testClearDoesNotRemoveNonNamespacedSessionValues() + { + $this->session->set('foo', 'baz'); + $this->storage->clear(); + + $this->assertTrue($this->session->has('foo')); + $this->assertSame('baz', $this->session->get('foo')); + } } diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/ClearableTokenStorageInterface.php b/src/Symfony/Component/Security/Csrf/TokenStorage/ClearableTokenStorageInterface.php new file mode 100644 index 0000000000000..0d6f16b68d0b6 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/ClearableTokenStorageInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenStorage; + +/** + * @author Christian Flothmann + */ +interface ClearableTokenStorageInterface extends TokenStorageInterface +{ + /** + * Removes all CSRF tokens. + */ + public function clear(); +} diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php index d4866fd955917..82bb990cb2fcc 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php @@ -18,7 +18,7 @@ * * @author Bernhard Schussek */ -class NativeSessionTokenStorage implements TokenStorageInterface +class NativeSessionTokenStorage implements ClearableTokenStorageInterface { /** * The namespace used to store values in the session. @@ -102,6 +102,14 @@ public function removeToken($tokenId) return $token; } + /** + * {@inheritdoc} + */ + public function clear() + { + unset($_SESSION[$this->namespace]); + } + private function startSession() { if (PHP_SESSION_NONE === session_status()) { diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php index 7b00e3231b45a..d22b83e8d51de 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php @@ -19,7 +19,7 @@ * * @author Bernhard Schussek */ -class SessionTokenStorage implements TokenStorageInterface +class SessionTokenStorage implements ClearableTokenStorageInterface { /** * The namespace used to store values in the session. @@ -92,4 +92,16 @@ public function removeToken($tokenId) return $this->session->remove($this->namespace.'/'.$tokenId); } + + /** + * {@inheritdoc} + */ + public function clear() + { + foreach (array_keys($this->session->all()) as $key) { + if (0 === strpos($key, $this->namespace.'/')) { + $this->session->remove($key); + } + } + } } diff --git a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php index b388478f56d0b..1af407d236039 100644 --- a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -48,6 +48,7 @@ public function __construct(TokenStorageInterface $tokenStorage, EventDispatcher */ public function authenticateWithToken(TokenInterface $token, Request $request) { + $this->migrateSession($request); $this->tokenStorage->setToken($token); if (null !== $this->dispatcher) { @@ -129,4 +130,12 @@ public function handleAuthenticationFailure(AuthenticationException $authenticat is_object($response) ? get_class($response) : gettype($response) )); } + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php index 0065fe8237c3e..6286b6cf87cd6 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php @@ -82,6 +82,9 @@ final public function handle(GetResponseEvent $event) if (null !== $this->logger) { $this->logger->info('Pre-authentication successful.', array('token' => (string) $token)); } + + $this->migrateSession($request); + $this->tokenStorage->setToken($token); if (null !== $this->dispatcher) { @@ -114,4 +117,12 @@ private function clearToken(AuthenticationException $exception) * @return array An array composed of the user and the credentials */ abstract protected function getPreAuthenticatedData(Request $request); + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } diff --git a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php index 1ddc41643448e..0d32f9a3d2bac 100644 --- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; @@ -70,6 +71,9 @@ public function handle(GetResponseEvent $event) try { $token = $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $request->headers->get('PHP_AUTH_PW'), $this->providerKey)); + + $this->migrateSession($request); + $this->tokenStorage->setToken($token); } catch (AuthenticationException $e) { $token = $this->tokenStorage->getToken(); @@ -88,4 +92,12 @@ public function handle(GetResponseEvent $event) $event->setResponse($this->authenticationEntryPoint->start($request, $e)); } } + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } diff --git a/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php index 92f96c6034e13..bcdd0d168ece2 100644 --- a/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php @@ -121,6 +121,8 @@ public function handle(GetResponseEvent $event) $this->logger->info('Digest authentication successful.', array('username' => $digestAuth->getUsername(), 'received' => $digestAuth->getResponse())); } + $this->migrateSession($request); + $this->tokenStorage->setToken(new UsernamePasswordToken($user, $user->getPassword(), $this->providerKey)); } @@ -137,6 +139,14 @@ private function fail(GetResponseEvent $event, Request $request, AuthenticationE $event->setResponse($this->authenticationEntryPoint->start($request, $authException)); } + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } /** diff --git a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php index dd90712408615..7325658c8d902 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -85,6 +86,9 @@ public function handle(GetResponseEvent $event) } $token = $this->authenticationManager->authenticate($token); + + $this->migrateSession($request); + $this->tokenStorage->setToken($token); if (null !== $this->dispatcher) { @@ -119,4 +123,12 @@ public function handle(GetResponseEvent $event) } } } + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php index 955288c23c375..8bde1e00151e8 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php @@ -139,6 +139,8 @@ private function onSuccess(Request $request, TokenInterface $token) $this->logger->info('User has been authenticated successfully.', array('username' => $token->getUsername())); } + $this->migrateSession($request); + $this->tokenStorage->setToken($token); if (null !== $this->eventDispatcher) { @@ -182,4 +184,12 @@ private function onFailure(Request $request, AuthenticationException $failed) return $response; } + + private function migrateSession(Request $request) + { + if (!$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + $request->getSession()->migrate(true); + } } diff --git a/src/Symfony/Component/Security/Http/Logout/CsrfTokenClearingLogoutHandler.php b/src/Symfony/Component/Security/Http/Logout/CsrfTokenClearingLogoutHandler.php new file mode 100644 index 0000000000000..ad6b888aad562 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Logout/CsrfTokenClearingLogoutHandler.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Logout; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; + +/** + * @author Christian Flothmann + */ +class CsrfTokenClearingLogoutHandler implements LogoutHandlerInterface +{ + private $csrfTokenStorage; + + public function __construct(ClearableTokenStorageInterface $csrfTokenStorage) + { + $this->csrfTokenStorage = $csrfTokenStorage; + } + + public function logout(Request $request, Response $response, TokenInterface $token) + { + $this->csrfTokenStorage->clear(); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php index dd258a086f1f0..9c1faa922c010 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php @@ -47,6 +47,8 @@ public function onAuthentication(Request $request, TokenInterface $token) return; case self::MIGRATE: + // Note: this logic is duplicated in several authentication listeners + // until Symfony 5.0 due to a security fix with BC compat $request->getSession()->migrate(true); return; diff --git a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategyInterface.php b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategyInterface.php index 9b05f151340ee..8de89b1868d16 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategyInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategyInterface.php @@ -27,8 +27,8 @@ interface SessionAuthenticationStrategyInterface /** * This performs any necessary changes to the session. * - * This method is called before the TokenStorage is populated with a - * Token, and only by classes inheriting from AbstractAuthenticationListener. + * This method should be called before the TokenStorage is populated with a + * Token. It should be used by authentication listeners when a session is used. */ public function onAuthentication(Request $request, TokenInterface $token); } diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/CsrfTokenClearingLogoutHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/CsrfTokenClearingLogoutHandlerTest.php new file mode 100644 index 0000000000000..fe34eaa6e5da3 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Logout/CsrfTokenClearingLogoutHandlerTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Logout; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler; + +class CsrfTokenClearingLogoutHandlerTest extends TestCase +{ + private $session; + private $csrfTokenStorage; + private $csrfTokenClearingLogoutHandler; + + protected function setUp() + { + $this->session = new Session(new MockArraySessionStorage()); + $this->csrfTokenStorage = new SessionTokenStorage($this->session, 'foo'); + $this->csrfTokenStorage->setToken('foo', 'bar'); + $this->csrfTokenStorage->setToken('foobar', 'baz'); + $this->csrfTokenClearingLogoutHandler = new CsrfTokenClearingLogoutHandler($this->csrfTokenStorage); + } + + public function testCsrfTokenCookieWithSameNamespaceIsRemoved() + { + $this->assertSame('bar', $this->session->get('foo/foo')); + $this->assertSame('baz', $this->session->get('foo/foobar')); + + $this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()); + + $this->assertFalse($this->csrfTokenStorage->hasToken('foo')); + $this->assertFalse($this->csrfTokenStorage->hasToken('foobar')); + + $this->assertFalse($this->session->has('foo/foo')); + $this->assertFalse($this->session->has('foo/foobar')); + } + + public function testCsrfTokenCookieWithDifferentNamespaceIsNotRemoved() + { + $barNamespaceCsrfSessionStorage = new SessionTokenStorage($this->session, 'bar'); + $barNamespaceCsrfSessionStorage->setToken('foo', 'bar'); + $barNamespaceCsrfSessionStorage->setToken('foobar', 'baz'); + + $this->assertSame('bar', $this->session->get('foo/foo')); + $this->assertSame('baz', $this->session->get('foo/foobar')); + $this->assertSame('bar', $this->session->get('bar/foo')); + $this->assertSame('baz', $this->session->get('bar/foobar')); + + $this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()); + + $this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foo')); + $this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foobar')); + $this->assertSame('bar', $barNamespaceCsrfSessionStorage->getToken('foo')); + $this->assertSame('baz', $barNamespaceCsrfSessionStorage->getToken('foobar')); + $this->assertFalse($this->csrfTokenStorage->hasToken('foo')); + $this->assertFalse($this->csrfTokenStorage->hasToken('foobar')); + + $this->assertFalse($this->session->has('foo/foo')); + $this->assertFalse($this->session->has('foo/foobar')); + $this->assertSame('bar', $this->session->get('bar/foo')); + $this->assertSame('baz', $this->session->get('bar/foobar')); + } +} diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 1ad433be87e27..f6758b6372021 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -27,9 +27,12 @@ }, "require-dev": { "symfony/routing": "~2.8|~3.0|~4.0", - "symfony/security-csrf": "^2.8.31|^3.3.13|~4.0", + "symfony/security-csrf": "^2.8.41|^3.3.17|^3.4.11|^4.0.11", "psr/log": "~1.0" }, + "conflict": { + "symfony/security-csrf": "<2.8.41|~3.0,<3.3.17|~3.4,<3.4.11|~4.0,<4.0.11|~4.1,<=4.1.0-beta2" + }, "suggest": { "symfony/security-csrf": "For using tokens to protect authentication/logout attempts", "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs" diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php index d9a15d5ac0de5..944a7b3133f35 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -60,5 +60,5 @@ public function getMaxDepth(); /** * Merges an {@see AttributeMetadataInterface} with in the current one. */ - public function merge(self $attributeMetadata); + public function merge(AttributeMetadataInterface $attributeMetadata); }