Merge branch '3.3' into 3.4

* 3.3:
  fixed CS
  fixed CS
  [Security] Namespace generated CSRF tokens depending of the current scheme
  ensure that submitted data are uploaded files
  [Console] remove dead code
  bumped Symfony version to 3.3.13
  updated VERSION for 3.3.12
  updated CHANGELOG for 3.3.12
  bumped Symfony version to 2.8.31
  updated VERSION for 2.8.30
  updated CHANGELOG for 2.8.30
  bumped Symfony version to 2.7.38
  updated VERSION for 2.7.37
  updated CHANGELOG for 2.7.37
  [Security] Validate redirect targets using the session cookie domain
  prevent bundle readers from breaking out of paths
This commit is contained in:
Nicolas Grekas 2017-11-16 17:25:26 +02:00
commit caa10ae038
25 changed files with 603 additions and 137 deletions

View File

@ -7,6 +7,12 @@ in 3.3 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.3.0...v3.3.1
* 3.3.12 (2017-11-13)
* bug #24954 [DI] Fix dumping with custom base class (nicolas-grekas)
* bug #24952 [HttpFoundation] Fix session-related BC break (nicolas-grekas, sroze)
* bug #24929 [Console] Fix traversable autocomplete values (ro0NL)
* 3.3.11 (2017-11-10)
* bug #24888 [FrameworkBundle] Specifically inject the debug dispatcher in the collector (ogizanagi)

View File

@ -18,6 +18,7 @@
<service id="security.csrf.token_manager" class="Symfony\Component\Security\Csrf\CsrfTokenManager" public="true">
<argument type="service" id="security.csrf.token_generator" />
<argument type="service" id="security.csrf.token_storage" />
<argument type="service" id="request_stack" on-invalid="ignore" />
</service>
<service id="Symfony\Component\Security\Csrf\CsrfTokenManagerInterface" alias="security.csrf.token_manager" />
</services>

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Uses the session domain to restrict allowed redirection targets.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class AddSessionDomainConstraintPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasParameter('session.storage.options') || !$container->has('security.http_utils')) {
return;
}
$sessionOptions = $container->getParameter('session.storage.options');
$domainRegexp = empty($sessionOptions['cookie_domain']) ? '%s' : sprintf('(?:%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.')));
$domainRegexp = (empty($sessionOptions['cookie_secure']) ? 'https?://' : 'https://').$domainRegexp;
$container->findDefinition('security.http_utils')->addArgument(sprintf('{^%s$}i', $domainRegexp));
}
}

View File

@ -14,8 +14,10 @@ namespace Symfony\Bundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory;
@ -58,6 +60,7 @@ class SecurityBundle extends Bundle
$extension->addUserProviderFactory(new InMemoryFactory());
$extension->addUserProviderFactory(new LdapFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
}
public function registerCommands(Application $application)

View File

@ -0,0 +1,131 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\Request;
class AddSessionDomainConstraintPassTest extends TestCase
{
public function testSessionCookie()
{
$container = $this->createContainer(array('cookie_domain' => '.symfony.com.', 'cookie_secure' => true));
$utils = $container->get('security.http_utils');
$request = Request::create('/', 'get');
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/'));
}
public function testSessionNoDomain()
{
$container = $this->createContainer(array('cookie_secure' => true));
$utils = $container->get('security.http_utils');
$request = Request::create('/', 'get');
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/'));
}
public function testSessionNoSecure()
{
$container = $this->createContainer(array('cookie_domain' => '.symfony.com.'));
$utils = $container->get('security.http_utils');
$request = Request::create('/', 'get');
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/'));
}
public function testSessionNoSecureAndNoDomain()
{
$container = $this->createContainer(array());
$utils = $container->get('security.http_utils');
$request = Request::create('/', 'get');
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://localhost/foo')->isRedirect('http://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/'));
}
public function testNoSession()
{
$container = $this->createContainer(null);
$utils = $container->get('security.http_utils');
$request = Request::create('/', 'get');
$this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('https://www.localhost/foo'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://symfony.com/blog'));
$this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://pirate.com/foo'));
}
private function createContainer($sessionStorageOptions)
{
$container = new ContainerBuilder();
$container->setParameter('kernel.cache_dir', __DIR__);
$container->setParameter('kernel.charset', 'UTF-8');
$container->setParameter('kernel.container_class', 'cc');
$container->setParameter('kernel.debug', true);
$container->setParameter('kernel.root_dir', __DIR__);
$container->setParameter('kernel.secret', __DIR__);
if (null !== $sessionStorageOptions) {
$container->setParameter('session.storage.options', $sessionStorageOptions);
}
$container->setParameter('request_listener.http_port', 80);
$container->setParameter('request_listener.https_port', 443);
$config = array(
'security' => array(
'providers' => array('some_provider' => array('id' => 'foo')),
'firewalls' => array('some_firewall' => array('security' => false)),
),
);
$ext = new FrameworkExtension();
$ext->load(array(), $container);
$ext = new SecurityExtension();
$ext->load($config, $container);
(new AddSessionDomainConstraintPass())->process($container);
return $container;
}
}

View File

@ -127,15 +127,13 @@ class Application
try {
$e = null;
$exitCode = $this->doRun($input, $output);
} catch (\Exception $x) {
$e = $x;
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (null !== $e) {
if (!$this->catchExceptions || !$x instanceof \Exception) {
throw $x;
if (!$this->catchExceptions || !$e instanceof \Exception) {
throw $e;
}
if ($output instanceof ConsoleOutputInterface) {

View File

@ -57,6 +57,11 @@ CHANGELOG
* moved data trimming logic of TrimListener into StringUtil
* [BC BREAK] When registering a type extension through the DI extension, the tag alias has to match the actual extended type.
2.7.38
------
* [BC BREAK] the `isFileUpload()` method was added to the `RequestHandlerInterface`
2.7.0
-----

View File

@ -27,20 +27,35 @@ class FileType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$form = $event->getForm();
$requestHandler = $form->getConfig()->getRequestHandler();
$data = null;
if ($options['multiple']) {
$data = array();
foreach ($event->getData() as $file) {
if ($requestHandler->isFileUpload($file)) {
$data[] = $file;
}
}
// submitted data for an input file (not required) without choosing any file
if (array(null) === $data) {
if (array(null) === $data || array() === $data) {
$emptyData = $form->getConfig()->getEmptyData();
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
$event->setData($data);
}
});
}
$event->setData($data);
} elseif (!$requestHandler->isFileUpload($event->getData())) {
$emptyData = $form->getConfig()->getEmptyData();
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
$event->setData($data);
}
});
}
/**

View File

@ -16,6 +16,7 @@ use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
/**
@ -28,9 +29,6 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
{
private $serverParams;
/**
* {@inheritdoc}
*/
public function __construct(ServerParams $serverParams = null)
{
$this->serverParams = $serverParams ?: new ServerParams();
@ -109,4 +107,9 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$form->submit($data, 'PATCH' !== $method);
}
public function isFileUpload($data)
{
return $data instanceof File;
}
}

View File

@ -23,14 +23,6 @@ class NativeRequestHandler implements RequestHandlerInterface
{
private $serverParams;
/**
* {@inheritdoc}
*/
public function __construct(ServerParams $params = null)
{
$this->serverParams = $params ?: new ServerParams();
}
/**
* The allowed keys of the $_FILES array.
*/
@ -42,6 +34,11 @@ class NativeRequestHandler implements RequestHandlerInterface
'type',
);
public function __construct(ServerParams $params = null)
{
$this->serverParams = $params ?: new ServerParams();
}
/**
* {@inheritdoc}
*/
@ -121,6 +118,14 @@ class NativeRequestHandler implements RequestHandlerInterface
$form->submit($data, 'PATCH' !== $method);
}
public function isFileUpload($data)
{
// POST data will always be strings or arrays of strings. Thus, we can be sure
// that the submitted data is a file upload if the "error" value is an integer
// (this value must have been injected by PHP itself).
return is_array($data) && isset($data['error']) && is_int($data['error']);
}
/**
* Returns the method used to submit the request to the server.
*

View File

@ -25,4 +25,11 @@ interface RequestHandlerInterface
* @param mixed $request The current request
*/
public function handleRequest(FormInterface $form, $request = null);
/**
* @param mixed $data
*
* @return bool
*/
public function isFileUpload($data);
}

View File

@ -353,12 +353,24 @@ abstract class AbstractRequestHandlerTest extends TestCase
);
}
public function testUploadedFilesAreAccepted()
{
$this->assertTrue($this->requestHandler->isFileUpload($this->getMockFile()));
}
public function testInvalidFilesAreRejected()
{
$this->assertFalse($this->requestHandler->isFileUpload($this->getInvalidFile()));
}
abstract protected function setRequestData($method, $data, $files = array());
abstract protected function getRequestHandler();
abstract protected function getMockFile($suffix = '');
abstract protected function getInvalidFile();
protected function getMockForm($name, $method = null, $compound = true)
{
$config = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')->getMock();

View File

@ -11,6 +11,11 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\NativeRequestHandler;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class FileTypeTest extends BaseTypeTest
{
const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\FileType';
@ -29,40 +34,49 @@ class FileTypeTest extends BaseTypeTest
$this->assertSame($data, $form->getData());
}
public function testSubmit()
/**
* @dataProvider requestHandlerProvider
*/
public function testSubmit(RequestHandlerInterface $requestHandler)
{
$form = $this->factory->createBuilder(static::TESTED_TYPE)->getForm();
$data = $this->createUploadedFileMock('abcdef', 'original.jpg', true);
$form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm();
$data = $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg');
$form->submit($data);
$this->assertSame($data, $form->getData());
}
public function testSetDataMultiple()
/**
* @dataProvider requestHandlerProvider
*/
public function testSetDataMultiple(RequestHandlerInterface $requestHandler)
{
$form = $this->factory->createBuilder(static::TESTED_TYPE, null, array(
'multiple' => true,
))->getForm();
))->setRequestHandler($requestHandler)->getForm();
$data = array(
$this->createUploadedFileMock('abcdef', 'first.jpg', true),
$this->createUploadedFileMock('zyxwvu', 'second.jpg', true),
$this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'),
$this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'),
);
$form->setData($data);
$this->assertSame($data, $form->getData());
}
public function testSubmitMultiple()
/**
* @dataProvider requestHandlerProvider
*/
public function testSubmitMultiple(RequestHandlerInterface $requestHandler)
{
$form = $this->factory->createBuilder(static::TESTED_TYPE, null, array(
'multiple' => true,
))->getForm();
))->setRequestHandler($requestHandler)->getForm();
$data = array(
$this->createUploadedFileMock('abcdef', 'first.jpg', true),
$this->createUploadedFileMock('zyxwvu', 'second.jpg', true),
$this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'),
$this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'),
);
$form->submit($data);
@ -73,11 +87,14 @@ class FileTypeTest extends BaseTypeTest
$this->assertArrayHasKey('multiple', $view->vars['attr']);
}
public function testDontPassValueToView()
/**
* @dataProvider requestHandlerProvider
*/
public function testDontPassValueToView(RequestHandlerInterface $requestHandler)
{
$form = $this->factory->create(static::TESTED_TYPE);
$form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm();
$form->submit(array(
'file' => $this->createUploadedFileMock('abcdef', 'original.jpg', true),
'file' => $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'),
));
$this->assertEquals('', $form->createView()->vars['value']);
@ -109,29 +126,59 @@ class FileTypeTest extends BaseTypeTest
$this->assertSame(array(), $form->getViewData());
}
private function createUploadedFileMock($name, $originalName, $valid)
/**
* @dataProvider requestHandlerProvider
*/
public function testSubmittedFilePathsAreDropped(RequestHandlerInterface $requestHandler)
{
$file = $this
->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile')
->setConstructorArgs(array(__DIR__.'/../../../Fixtures/foo', 'foo'))
->getMock()
;
$file
->expects($this->any())
->method('getBasename')
->will($this->returnValue($name))
;
$file
->expects($this->any())
->method('getClientOriginalName')
->will($this->returnValue($originalName))
;
$file
->expects($this->any())
->method('isValid')
->will($this->returnValue($valid))
;
$form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm();
$form->submit('file:///etc/passwd');
return $file;
$this->assertNull($form->getData());
$this->assertNull($form->getNormData());
$this->assertSame('', $form->getViewData());
}
/**
* @dataProvider requestHandlerProvider
*/
public function testMultipleSubmittedFilePathsAreDropped(RequestHandlerInterface $requestHandler)
{
$form = $this->factory
->createBuilder(static::TESTED_TYPE, null, array(
'multiple' => true,
))
->setRequestHandler($requestHandler)
->getForm();
$form->submit(array(
'file:///etc/passwd',
$this->createUploadedFileMock(new HttpFoundationRequestHandler(), __DIR__.'/../../../Fixtures/foo', 'foo.jpg'),
$this->createUploadedFileMock(new NativeRequestHandler(), __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'),
));
$this->assertCount(1, $form->getData());
}
public function requestHandlerProvider()
{
return array(
array(new HttpFoundationRequestHandler()),
array(new NativeRequestHandler()),
);
}
private function createUploadedFileMock(RequestHandlerInterface $requestHandler, $path, $originalName)
{
if ($requestHandler instanceof HttpFoundationRequestHandler) {
return new UploadedFile($path, $originalName, null, 10, null, true);
}
return array(
'name' => $originalName,
'error' => 0,
'type' => 'text/plain',
'tmp_name' => $path,
'size' => 10,
);
}
}

View File

@ -51,4 +51,9 @@ class HttpFoundationRequestHandlerTest extends AbstractRequestHandlerTest
{
return new UploadedFile(__DIR__.'/../../Fixtures/foo'.$suffix, 'foo'.$suffix);
}
protected function getInvalidFile()
{
return 'file:///etc/passwd';
}
}

View File

@ -216,4 +216,15 @@ class NativeRequestHandlerTest extends AbstractRequestHandlerTest
'size' => 100,
);
}
protected function getInvalidFile()
{
return array(
'name' => 'upload.txt',
'type' => 'text/plain',
'tmp_name' => 'owfdskjasdfsa',
'error' => '0',
'size' => '100',
);
}
}

View File

@ -30,6 +30,11 @@ class JsonBundleReader implements BundleReaderInterface
{
$fileName = $path.'/'.$locale.'.json';
// prevent directory traversal attacks
if (dirname($fileName) !== $path) {
throw new ResourceBundleNotFoundException(sprintf('The resource bundle "%s" does not exist.', $fileName));
}
if (!file_exists($fileName)) {
throw new ResourceBundleNotFoundException(sprintf(
'The resource bundle "%s" does not exist.',

View File

@ -30,6 +30,11 @@ class PhpBundleReader implements BundleReaderInterface
{
$fileName = $path.'/'.$locale.'.php';
// prevent directory traversal attacks
if (dirname($fileName) !== $path) {
throw new ResourceBundleNotFoundException(sprintf('The resource bundle "%s" does not exist.', $fileName));
}
if (!file_exists($fileName)) {
throw new ResourceBundleNotFoundException(sprintf(
'The resource bundle "%s/%s.php" does not exist.',

View File

@ -0,0 +1,14 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return array(
'Foo' => 'Bar',
);

View File

@ -69,4 +69,12 @@ class JsonBundleReaderTest extends TestCase
{
$this->reader->read(__DIR__.'/Fixtures/json', 'en_Invalid');
}
/**
* @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException
*/
public function testReaderDoesNotBreakOutOfGivenPath()
{
$this->reader->read(__DIR__.'/Fixtures/json', '../invalid_directory/en');
}
}

View File

@ -61,4 +61,12 @@ class PhpBundleReaderTest extends TestCase
{
$this->reader->read(__DIR__.'/Fixtures/NotAFile', 'en');
}
/**
* @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException
*/
public function testReaderDoesNotBreakOutOfGivenPath()
{
$this->reader->read(__DIR__.'/Fixtures/php', '../invalid_directory/en');
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Csrf;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
@ -20,16 +22,45 @@ use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
* Default implementation of {@link CsrfTokenManagerInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CsrfTokenManager implements CsrfTokenManagerInterface
{
private $generator;
private $storage;
private $namespace;
public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null)
/**
* @param null|string|RequestStack|callable $namespace
* * null: generates a namespace using $_SERVER['HTTPS']
* * string: uses the given string
* * RequestStack: generates a namespace using the current master request
* * callable: uses the result of this callable (must return a string)
*/
public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null, $namespace = null)
{
$this->generator = $generator ?: new UriSafeTokenGenerator();
$this->storage = $storage ?: new NativeSessionTokenStorage();
$superGlobalNamespaceGenerator = function () {
return !empty($_SERVER['HTTPS']) && 'off' !== strtolower($_SERVER['HTTPS']) ? 'https-' : '';
};
if (null === $namespace) {
$this->namespace = $superGlobalNamespaceGenerator;
} elseif ($namespace instanceof RequestStack) {
$this->namespace = function () use ($namespace, $superGlobalNamespaceGenerator) {
if ($request = $namespace->getMasterRequest()) {
return $request->isSecure() ? 'https-' : '';
}
return $superGlobalNamespaceGenerator();
};
} elseif (is_callable($namespace) || is_string($namespace)) {
$this->namespace = $namespace;
} else {
throw new InvalidArgumentException(sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', gettype($namespace)));
}
}
/**
@ -37,12 +68,13 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
*/
public function getToken($tokenId)
{
if ($this->storage->hasToken($tokenId)) {
$value = $this->storage->getToken($tokenId);
$namespacedId = $this->getNamespace().$tokenId;
if ($this->storage->hasToken($namespacedId)) {
$value = $this->storage->getToken($namespacedId);
} else {
$value = $this->generator->generateToken();
$this->storage->setToken($tokenId, $value);
$this->storage->setToken($namespacedId, $value);
}
return new CsrfToken($tokenId, $value);
@ -53,9 +85,10 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
*/
public function refreshToken($tokenId)
{
$namespacedId = $this->getNamespace().$tokenId;
$value = $this->generator->generateToken();
$this->storage->setToken($tokenId, $value);
$this->storage->setToken($namespacedId, $value);
return new CsrfToken($tokenId, $value);
}
@ -65,7 +98,7 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
*/
public function removeToken($tokenId)
{
return $this->storage->removeToken($tokenId);
return $this->storage->removeToken($this->getNamespace().$tokenId);
}
/**
@ -73,10 +106,16 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
*/
public function isTokenValid(CsrfToken $token)
{
if (!$this->storage->hasToken($token->getId())) {
$namespacedId = $this->getNamespace().$token->getId();
if (!$this->storage->hasToken($namespacedId)) {
return false;
}
return hash_equals($this->storage->getToken($token->getId()), $token->getValue());
return hash_equals($this->storage->getToken($namespacedId), $token->getValue());
}
private function getNamespace()
{
return is_callable($ns = $this->namespace) ? $ns() : $ns;
}
}

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Security\Csrf\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManager;
@ -21,145 +23,202 @@ use Symfony\Component\Security\Csrf\CsrfTokenManager;
class CsrfTokenManagerTest extends TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
* @dataProvider getManagerGeneratorAndStorage
*/
private $generator;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $storage;
/**
* @var CsrfTokenManager
*/
private $manager;
protected function setUp()
public function testGetNonExistingToken($namespace, $manager, $storage, $generator)
{
$this->generator = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock();
$this->storage = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock();
$this->manager = new CsrfTokenManager($this->generator, $this->storage);
}
protected function tearDown()
{
$this->generator = null;
$this->storage = null;
$this->manager = null;
}
public function testGetNonExistingToken()
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('hasToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue(false));
$this->generator->expects($this->once())
$generator->expects($this->once())
->method('generateToken')
->will($this->returnValue('TOKEN'));
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('setToken')
->with('token_id', 'TOKEN');
->with($namespace.'token_id', 'TOKEN');
$token = $this->manager->getToken('token_id');
$token = $manager->getToken('token_id');
$this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token);
$this->assertSame('token_id', $token->getId());
$this->assertSame('TOKEN', $token->getValue());
}
public function testUseExistingTokenIfAvailable()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testUseExistingTokenIfAvailable($namespace, $manager, $storage)
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('hasToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('getToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue('TOKEN'));
$token = $this->manager->getToken('token_id');
$token = $manager->getToken('token_id');
$this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token);
$this->assertSame('token_id', $token->getId());
$this->assertSame('TOKEN', $token->getValue());
}
public function testRefreshTokenAlwaysReturnsNewToken()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testRefreshTokenAlwaysReturnsNewToken($namespace, $manager, $storage, $generator)
{
$this->storage->expects($this->never())
$storage->expects($this->never())
->method('hasToken');
$this->generator->expects($this->once())
$generator->expects($this->once())
->method('generateToken')
->will($this->returnValue('TOKEN'));
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('setToken')
->with('token_id', 'TOKEN');
->with($namespace.'token_id', 'TOKEN');
$token = $this->manager->refreshToken('token_id');
$token = $manager->refreshToken('token_id');
$this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token);
$this->assertSame('token_id', $token->getId());
$this->assertSame('TOKEN', $token->getValue());
}
public function testMatchingTokenIsValid()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testMatchingTokenIsValid($namespace, $manager, $storage)
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('hasToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('getToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue('TOKEN'));
$this->assertTrue($this->manager->isTokenValid(new CsrfToken('token_id', 'TOKEN')));
$this->assertTrue($manager->isTokenValid(new CsrfToken('token_id', 'TOKEN')));
}
public function testNonMatchingTokenIsNotValid()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testNonMatchingTokenIsNotValid($namespace, $manager, $storage)
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('hasToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('getToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue('TOKEN'));
$this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR')));
$this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR')));
}
public function testNonExistingTokenIsNotValid()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testNonExistingTokenIsNotValid($namespace, $manager, $storage)
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('hasToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue(false));
$this->storage->expects($this->never())
$storage->expects($this->never())
->method('getToken');
$this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR')));
$this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR')));
}
public function testRemoveToken()
/**
* @dataProvider getManagerGeneratorAndStorage
*/
public function testRemoveToken($namespace, $manager, $storage)
{
$this->storage->expects($this->once())
$storage->expects($this->once())
->method('removeToken')
->with('token_id')
->with($namespace.'token_id')
->will($this->returnValue('REMOVED_TOKEN'));
$this->assertSame('REMOVED_TOKEN', $this->manager->removeToken('token_id'));
$this->assertSame('REMOVED_TOKEN', $manager->removeToken('token_id'));
}
public function testNamespaced()
{
$generator = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock();
$storage = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock();
$requestStack = new RequestStack();
$requestStack->push(new Request(array(), array(), array(), array(), array(), array('HTTPS' => 'on')));
$manager = new CsrfTokenManager($generator, $storage, null, $requestStack);
$token = $manager->getToken('foo');
$this->assertSame('foo', $token->getId());
}
public function getManagerGeneratorAndStorage()
{
$data = array();
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('', new CsrfTokenManager($generator, $storage, ''), $storage, $generator);
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('https-', new CsrfTokenManager($generator, $storage), $storage, $generator);
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('aNamespace-', new CsrfTokenManager($generator, $storage, 'aNamespace-'), $storage, $generator);
$requestStack = new RequestStack();
$requestStack->push(new Request(array(), array(), array(), array(), array(), array('HTTPS' => 'on')));
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('https-', new CsrfTokenManager($generator, $storage, $requestStack), $storage, $generator);
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('generated-', new CsrfTokenManager($generator, $storage, function () {
return 'generated-';
}), $storage, $generator);
$requestStack = new RequestStack();
$requestStack->push(new Request());
list($generator, $storage) = $this->getGeneratorAndStorage();
$data[] = array('', new CsrfTokenManager($generator, $storage, $requestStack), $storage, $generator);
return $data;
}
private function getGeneratorAndStorage()
{
return array(
$this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock(),
$this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock(),
);
}
public function setUp()
{
$_SERVER['HTTPS'] = 'on';
}
public function tearDown()
{
parent::tearDown();
unset($_SERVER['HTTPS']);
}
}

View File

@ -29,20 +29,23 @@ class HttpUtils
{
private $urlGenerator;
private $urlMatcher;
private $domainRegexp;
/**
* @param UrlGeneratorInterface $urlGenerator A UrlGeneratorInterface instance
* @param UrlMatcherInterface|RequestMatcherInterface $urlMatcher The URL or Request matcher
* @param string|null $domainRegexp A regexp that the target of HTTP redirections must match, scheme included
*
* @throws \InvalidArgumentException
*/
public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatcher = null)
public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatcher = null, $domainRegexp = null)
{
$this->urlGenerator = $urlGenerator;
if (null !== $urlMatcher && !$urlMatcher instanceof UrlMatcherInterface && !$urlMatcher instanceof RequestMatcherInterface) {
throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.');
}
$this->urlMatcher = $urlMatcher;
$this->domainRegexp = $domainRegexp;
}
/**
@ -56,6 +59,10 @@ class HttpUtils
*/
public function createRedirectResponse(Request $request, $path, $status = 302)
{
if (null !== $this->domainRegexp && preg_match('#^https?://[^/]++#i', $path, $host) && !preg_match(sprintf($this->domainRegexp, preg_quote($request->getHttpHost())), $host[0])) {
$path = '/';
}
return new RedirectResponse($this->generateUri($request, $path), $status);
}

View File

@ -38,6 +38,38 @@ class HttpUtilsTest extends TestCase
$this->assertTrue($response->isRedirect('http://symfony.com/'));
}
public function testCreateRedirectResponseWithDomainRegexp()
{
$utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://symfony\.com$#i');
$response = $utils->createRedirectResponse($this->getRequest(), 'http://symfony.com/blog');
$this->assertTrue($response->isRedirect('http://symfony.com/blog'));
}
public function testCreateRedirectResponseWithRequestsDomain()
{
$utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i');
$response = $utils->createRedirectResponse($this->getRequest(), 'http://localhost/blog');
$this->assertTrue($response->isRedirect('http://localhost/blog'));
}
public function testCreateRedirectResponseWithBadRequestsDomain()
{
$utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i');
$response = $utils->createRedirectResponse($this->getRequest(), 'http://pirate.net/foo');
$this->assertTrue($response->isRedirect('http://localhost/'));
}
public function testCreateRedirectResponseWithProtocolRelativeTarget()
{
$utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i');
$response = $utils->createRedirectResponse($this->getRequest(), '//evil.com/do-bad-things');
$this->assertTrue($response->isRedirect('http://localhost//evil.com/do-bad-things'), 'Protocol-relative redirection should not be supported for security reasons');
}
public function testCreateRedirectResponseWithRouteName()
{
$utils = new HttpUtils($urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock());