Compare commits

...
This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.

9 Commits
5.4 ... 3.2

Author SHA1 Message Date
Fabien Potencier fc9f9470e8
Merge pull request #25001 from fabpot/release-3.2.14
released v3.2.14
2017-11-16 19:00:05 +01:00
Fabien Potencier 48b0d9b9e4 updated CHANGELOG for 3.2.14 2017-11-16 18:59:41 +01:00
Nicolas Grekas 611c272f2a fix deps=low 2017-11-16 19:55:54 +02:00
Fabien Potencier 1c86f91c23 fixed CS 2017-11-16 19:55:54 +02:00
Nicolas Grekas 435b2174d8 [Security] Validate redirect targets using the session cookie domain 2017-11-16 19:55:54 +02:00
Christian Flothmann 3d45e100c6 prevent bundle readers from breaking out of paths 2017-11-16 19:55:54 +02:00
Christian Flothmann 7e93eaaad6 ensure that submitted data are uploaded files 2017-11-16 19:55:54 +02:00
Fabien Potencier f25616d6f6 fixed CS 2017-11-16 19:55:54 +02:00
Kévin Dunglas 4f0e5afa2b [Security] Namespace generated CSRF tokens depending of the current scheme 2017-11-16 19:55:54 +02:00
26 changed files with 600 additions and 134 deletions

View File

@ -7,6 +7,13 @@ in 3.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/v3.2.0...v3.2.1
* 3.2.14 (2017-11-16)
* security #24995 Validate redirect targets using the session cookie domain (nicolas-grekas)
* security #24994 Prevent bundle readers from breaking out of paths (xabbuh)
* security #24993 Ensure that submitted data are uploaded files (xabbuh)
* security #24992 Namespace generated CSRF tokens depending of the current scheme (dunglas)
* 3.2.13 (2017-08-01)
* bug #22244 [Console] Fix passing options with defaultCommand (Jakub Sacha)

View File

@ -14,6 +14,7 @@
<service id="security.csrf.token_manager" class="Symfony\Component\Security\Csrf\CsrfTokenManager">
<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>
</services>
</container>

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

@ -12,8 +12,10 @@
namespace Symfony\Bundle\SecurityBundle;
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;
@ -55,5 +57,6 @@ class SecurityBundle extends Bundle
$extension->addUserProviderFactory(new InMemoryFactory());
$extension->addUserProviderFactory(new LdapFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}

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

@ -18,7 +18,7 @@
"require": {
"php": ">=5.5.9",
"ext-xml": "*",
"symfony/security": "~3.2",
"symfony/security": "~3.2.14",
"symfony/http-kernel": "~3.2",
"symfony/polyfill-php70": "~1.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;
/**
@ -31,9 +32,6 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
*/
private $serverParams;
/**
* {@inheritdoc}
*/
public function __construct(ServerParams $serverParams = null)
{
$this->serverParams = $serverParams ?: new ServerParams();
@ -112,4 +110,9 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$form->submit($data, 'PATCH' !== $method);
}
public function isFileUpload($data)
{
return $data instanceof File;
}
}

View File

@ -26,14 +26,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.
*
@ -47,6 +39,11 @@ class NativeRequestHandler implements RequestHandlerInterface
'type',
);
public function __construct(ServerParams $params = null)
{
$this->serverParams = $params ?: new ServerParams();
}
/**
* {@inheritdoc}
*/
@ -126,6 +123,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/%s.json" 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,6 +22,7 @@ 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
{
@ -32,18 +35,45 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
* @var TokenStorageInterface
*/
private $storage;
private $namespace;
/**
* Creates a new CSRF provider using PHP's native session storage.
*
* @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)
*
* @param TokenGeneratorInterface|null $generator The token generator
* @param TokenStorageInterface|null $storage The storage for storing
* generated CSRF tokens
*/
public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null)
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)));
}
}
/**
@ -51,12 +81,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);
@ -67,9 +98,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);
}
@ -79,7 +111,7 @@ class CsrfTokenManager implements CsrfTokenManagerInterface
*/
public function removeToken($tokenId)
{
return $this->storage->removeToken($tokenId);
return $this->storage->removeToken($this->getNamespace().$tokenId);
}
/**
@ -87,10 +119,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

@ -22,7 +22,10 @@
"symfony/security-core": "~2.8|~3.0"
},
"require-dev": {
"symfony/http-foundation": "~2.8|~3.0"
"symfony/http-foundation": "~2.8.31|~3.2.14"
},
"conflict": {
"symfony/http-foundation": "<2.8.31|~3.2,<3.2.14"
},
"suggest": {
"symfony/http-foundation": "For using the class SessionTokenStorage."

View File

@ -29,22 +29,25 @@ class HttpUtils
{
private $urlGenerator;
private $urlMatcher;
private $domainRegexp;
/**
* Constructor.
*
* @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 ($urlMatcher !== null && !$urlMatcher instanceof UrlMatcherInterface && !$urlMatcher instanceof RequestMatcherInterface) {
throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.');
}
$this->urlMatcher = $urlMatcher;
$this->domainRegexp = $domainRegexp;
}
/**
@ -58,6 +61,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());

View File

@ -18,7 +18,7 @@
"require": {
"php": ">=5.5.9",
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/http-foundation": "~2.8|~3.0",
"symfony/http-foundation": "~2.8.31|~3.2.14",
"symfony/http-kernel": "~2.8|~3.0",
"symfony/polyfill-php56": "~1.0",
"symfony/polyfill-php70": "~1.0",