diff --git a/CHANGELOG-4.2.md b/CHANGELOG-4.2.md index 7ff4976cca8e9..c8b1ea6dc20b9 100644 --- a/CHANGELOG-4.2.md +++ b/CHANGELOG-4.2.md @@ -7,6 +7,14 @@ in 4.2 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/v4.2.0...v4.2.1 +* 4.2.12 (2019-11-13) + + * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) + * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) + * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) + * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) + * 4.2.11 (2019-07-28) * bug #32760 [HttpKernel] clarify error handler restoring process (xabbuh) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php index 31f99da2a08f2..0ccacf583fe57 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php @@ -68,7 +68,7 @@ public function getTestParameters() return [ 'unauthorized_user_cannot_switch' => ['user_cannot_switch_1', 'user_cannot_switch_1', 'user_cannot_switch_1', 403], 'authorized_user_can_switch' => ['user_can_switch', 'user_cannot_switch_1', 'user_cannot_switch_1', 200], - 'authorized_user_cannot_switch_to_non_existent' => ['user_can_switch', 'user_does_not_exist', 'user_can_switch', 500], + 'authorized_user_cannot_switch_to_non_existent' => ['user_can_switch', 'user_does_not_exist', 'user_can_switch', 403], 'authorized_user_can_switch_to_himself' => ['user_can_switch', 'user_can_switch', 'user_can_switch', 200], ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 5a03f8f7fa846..a839853ba4808 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -24,7 +24,7 @@ "symfony/security-core": "~4.2", "symfony/security-csrf": "~4.2", "symfony/security-guard": "~4.2", - "symfony/security-http": "~4.2" + "symfony/security-http": "^4.2.12" }, "require-dev": { "symfony/asset": "~3.4|~4.0", diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 51b2bbd6c560e..fbdce32d94609 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -277,6 +277,16 @@ public function commit() return $ok; } + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + public function __destruct() { if ($this->deferred) { diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 5b08418fccf34..f992832848236 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -277,6 +277,16 @@ public function commit() return $this->invalidateTags([]); } + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + public function __destruct() { $this->commit(); diff --git a/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php b/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php index 313000195d516..d5a9a10bedfac 100644 --- a/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php +++ b/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -31,7 +31,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface * * @param string $cmd The command to run to get the mime type of a file */ - public function __construct(string $cmd = 'file -b --mime %s 2>/dev/null') + public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') { $this->cmd = $cmd; } @@ -80,7 +80,7 @@ public function guess($path) ob_start(); // need to use --mime instead of -i. see #6641 - passthru(sprintf($this->cmd, escapeshellarg($path)), $return); + passthru(sprintf($this->cmd, escapeshellarg((0 === strpos($path, '-') ? './' : '').$path)), $return); if ($return > 0) { ob_end_clean(); diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/-test b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/-test new file mode 100644 index 0000000000000..b636f4b8df536 Binary files /dev/null and b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/-test differ diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php b/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php index bb88807ab0519..ff2aa892eebac 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php @@ -20,7 +20,16 @@ */ class MimeTypeTest extends TestCase { - protected $path; + public function testGuessWithLeadingDash() + { + $cwd = getcwd(); + chdir(__DIR__.'/../Fixtures'); + try { + $this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess('-test')); + } finally { + chdir($cwd); + } + } public function testGuessImageWithoutExtension() { diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 2019fc92fd8a6..999e86836a1ca 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.2.11'; - const VERSION_ID = 40211; + const VERSION = '4.2.12'; + const VERSION_ID = 40212; const MAJOR_VERSION = 4; const MINOR_VERSION = 2; - const RELEASE_VERSION = 11; + const RELEASE_VERSION = 12; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '07/2019'; diff --git a/src/Symfony/Component/HttpKernel/UriSigner.php b/src/Symfony/Component/HttpKernel/UriSigner.php index 1dd56ffd76fff..af8a421371afe 100644 --- a/src/Symfony/Component/HttpKernel/UriSigner.php +++ b/src/Symfony/Component/HttpKernel/UriSigner.php @@ -79,7 +79,7 @@ public function check($uri) $hash = $params[$this->parameter]; unset($params[$this->parameter]); - return $this->computeHash($this->buildUrl($url, $params)) === $hash; + return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); } private function computeHash($uri) diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 1e5684d579901..6906bc7b8049c 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -22,6 +22,7 @@ "symfony/http-foundation": "^4.1.1", "symfony/debug": "~3.4|~4.0", "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php56": "~1.8", "psr/log": "~1.0" }, "require-dev": { diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index 4aff3b45811fa..dac5e02e2ea32 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -93,7 +93,8 @@ public function handle(GetResponseEvent $event) try { $this->tokenStorage->setToken($this->attemptSwitchUser($request, $username)); } catch (AuthenticationException $e) { - throw new \LogicException(sprintf('Switch User failed: "%s"', $e->getMessage())); + // Generate 403 in any conditions to prevent user enumeration vulnerabilities + throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e); } } @@ -130,7 +131,24 @@ private function attemptSwitchUser(Request $request, $username) throw new \LogicException(sprintf('You are already switched to "%s" user.', $token->getUsername())); } - $user = $this->provider->loadUserByUsername($username); + $currentUsername = $token->getUsername(); + $nonExistentUsername = '_'.md5(random_bytes(8).$username); + + // To protect against user enumeration via timing measurements + // we always load both successfully and unsuccessfully + try { + $user = $this->provider->loadUserByUsername($username); + + try { + $this->provider->loadUserByUsername($nonExistentUsername); + throw new \LogicException('AuthenticationException expected'); + } catch (AuthenticationException $e) { + } + } catch (AuthenticationException $e) { + $this->provider->loadUserByUsername($currentUsername); + + throw $e; + } if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) { $exception = new AccessDeniedException(); diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index 6e3b0e30f81d7..31cd458095296 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Event\SwitchUserEvent; @@ -161,6 +162,7 @@ public function testExitUserDoesNotDispatchEventWithStringUser() public function testSwitchUserIsDisallowed() { $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']); + $user = new User('username', 'password', []); $this->tokenStorage->setToken($token); $this->request->query->set('_switch_user', 'kuba'); @@ -169,6 +171,33 @@ public function testSwitchUserIsDisallowed() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH']) ->willReturn(false); + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba']) + ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); + + $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + $listener->handle($this->event); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException + */ + public function testSwitchUserTurnsAuthenticationExceptionTo403() + { + $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_ALLOWED_TO_SWITCH']); + + $this->tokenStorage->setToken($token); + $this->request->query->set('_switch_user', 'kuba'); + + $this->accessDecisionManager->expects($this->never()) + ->method('decide'); + + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba'], ['username']) + ->will($this->onConsecutiveCalls($this->throwException(new UsernameNotFoundException()))); + $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener->handle($this->event); } @@ -185,9 +214,10 @@ public function testSwitchUser() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) ->willReturn(true); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername')->with('kuba') - ->willReturn($user); + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba']) + ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) ->method('checkPostAuth')->with($user); @@ -215,9 +245,10 @@ public function testSwitchUserKeepsOtherQueryStringParameters() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) ->willReturn(true); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername')->with('kuba') - ->willReturn($user); + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba']) + ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) ->method('checkPostAuth')->with($user); @@ -243,9 +274,10 @@ public function testSwitchUserWithReplacedToken() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) ->willReturn(true); - $this->userProvider->expects($this->any()) - ->method('loadUserByUsername')->with('kuba') - ->willReturn($user); + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba']) + ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); $dispatcher @@ -290,9 +322,10 @@ public function testSwitchUserStateless() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) ->willReturn(true); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername')->with('kuba') - ->willReturn($user); + $this->userProvider->expects($this->exactly(2)) + ->method('loadUserByUsername') + ->withConsecutive(['kuba']) + ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) ->method('checkPostAuth')->with($user); diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index 87b3156d41efd..8919ec578fe1f 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -212,27 +212,28 @@ public static function export($value, $indent = '') $subIndent = $indent.' '; if (\is_string($value)) { - $code = var_export($value, true); - - if (false !== strpos($value, "\n") || false !== strpos($value, "\r")) { - $code = strtr($code, [ - "\r\n" => "'.\"\\r\\n\"\n".$subIndent.".'", - "\r" => "'.\"\\r\"\n".$subIndent.".'", - "\n" => "'.\"\\n\"\n".$subIndent.".'", - ]); - } + $code = sprintf("'%s'", addcslashes($value, "'\\")); - if (false !== strpos($value, "\0")) { - $code = str_replace('\' . "\0" . \'', '\'."\0".\'', $code); - $code = str_replace('".\'\'."', '', $code); - } + $code = preg_replace_callback('/([\0\r\n]++)(.)/', function ($m) use ($subIndent) { + $m[1] = sprintf('\'."%s".\'', str_replace( + ["\0", "\r", "\n", '\n\\'], + ['\0', '\r', '\n', '\n"'."\n".$subIndent.'."\\'], + $m[1] + )); - if (false !== strpos($code, "''.")) { - $code = str_replace("''.", '', $code); - } + if ("'" === $m[2]) { + return substr($m[1], 0, -2); + } + + if ('n".\'' === substr($m[1], -4)) { + return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); + } + + return $m[1].$m[2]; + }, $code, -1, $count); - if (".''" === substr($code, -3)) { - $code = rtrim(substr($code, 0, -3)); + if ($count && 0 === strpos($code, "''.")) { + $code = substr($code, 3); } return $code; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/lf-ending-string.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/lf-ending-string.php new file mode 100644 index 0000000000000..f6bcf84976344 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/lf-ending-string.php @@ -0,0 +1,4 @@ + 'B'."\r" - .'C'."\n" + .'A' => 'B'."\r".'C'."\n" ."\n", ]; diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index 6f54d761064ec..3117bd51dc5cc 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -109,6 +109,7 @@ public function testExport(string $testName, $value, bool $staticValueExpected = public function provideExport() { yield ['multiline-string', ["\0\0\r\nA" => "B\rC\n\n"], true]; + yield ['lf-ending-string', "'BOOM'\n.var_dump(123)//'", true]; yield ['bool', true, true]; yield ['simple-array', [123, ['abc']], true];