feature#6554 [Security] Added Security\Csrf sub-component with better token generation (bschussek)

This PR was merged into the master branch.

Discussion
----------

[Security] Added Security\Csrf sub-component with better token generation

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | TODO

**Update September 27, 2013**

This PR simplifies the CSRF mechanism to generate completely random tokens. A random token is generated once per ~~intention~~ token ID and then stored in the session. Tokens are valid until the session expires.

Since the CSRF token generator depends on `StringUtils` and `SecureRandom` from Security\Core, and since Security\Http currently depends on the Form component for token generation, I decided to add a new Security\Csrf sub-component that contains the improved CSRF token generator. Consequences:

* Security\Http now depends on Security\Csrf instead of Form
* Form now optionally depends on Security\Csrf
* The configuration for the "security.secure_random" service and the "security.csrf.*" services was moved to FrameworkBundle to guarantee BC

In the new Security\Csrf sub-component, I tried to improve the naming where I could do so without breaking BC:

* CSRF "providers" are now called "token generators"
* CSRF "intentions" are now called "token IDs", because that's really what they are

##### TODO

- [ ] The documentation needs to be checked for references to the configuration of the application secret. Remarks that the secret is used for CSRF protection need to be removed.
- [ ] Add aliases "csrf_token_generator" and "csrf_token_id" for "csrf_provider" and "intention" in the SecurityBundle configuration
- [x] Make sure `SecureRandom` never blocks for `CsrfTokenGenerator`

Commits
-------

7f02304 [Security] Added missing PHPDoc tag
2e04e32 Updated Composer dependencies to require the Security\Csrf component where necessary
bf85e83 [FrameworkBundle][SecurityBundle] Added service configuration for the new Security CSRF sub-component
2048cf6 [Form] Deprecated the CSRF implementation and added an optional dependency to the Security CSRF sub-component instead
85d4959 [Security] Changed Security HTTP sub-component to depend on CSRF sub-component instead of Form
1bf1640 [Security] Added CSRF sub-component
This commit is contained in:
Fabien Potencier 2013-09-30 17:35:08 +02:00
commit 0f80916313
55 changed files with 1158 additions and 178 deletions

View File

@ -168,6 +168,13 @@ UPGRADE FROM 2.x to 3.0
`ChoiceListInterface::getChoicesForValues()` and
`ChoiceListInterface::getValuesForChoices()` should be sufficient.
* The interface `Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface`
and all of its implementations were removed. Use the new interface
`Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface` instead.
* The options "csrf_provider" and "intention" were renamed to "csrf_token_generator"
and "csrf_token_id".
### FrameworkBundle

View File

@ -52,6 +52,7 @@
"symfony/security": "self.version",
"symfony/security-acl": "self.version",
"symfony/security-core": "self.version",
"symfony/security-csrf": "self.version",
"symfony/security-http": "self.version",
"symfony/security-bundle": "self.version",
"symfony/serializer": "self.version",

View File

@ -12,7 +12,7 @@
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
@ -24,9 +24,9 @@ class TwigRenderer extends FormRenderer implements TwigRendererInterface
*/
private $engine;
public function __construct(TwigRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
public function __construct(TwigRendererEngineInterface $engine, CsrfTokenGeneratorInterface $csrfTokenGenerator = null)
{
parent::__construct($engine, $csrfProvider);
parent::__construct($engine, $csrfTokenGenerator);
$this->engine = $engine;
}

View File

@ -17,6 +17,7 @@
],
"require": {
"php": ">=5.3.3",
"symfony/security-csrf": "~2.4",
"twig/twig": "~1.11"
},
"require-dev": {
@ -26,7 +27,7 @@
"symfony/templating": "~2.1",
"symfony/translation": "~2.2",
"symfony/yaml": "~2.0",
"symfony/security": "~2.0",
"symfony/security": "~2.4",
"symfony/stopwatch": "~2.2"
},
"suggest": {

View File

@ -6,6 +6,8 @@ CHANGELOG
* allowed multiple IP addresses in profiler matcher settings
* added stopwatch helper to time templates with the WebProfilerBundle
* added service definition for "security.secure_random" service
* added service definitions for the new Security CSRF sub-component
2.3.0
-----

View File

@ -56,6 +56,10 @@ class FrameworkExtension extends Extension
$loader->load('debug_prod.xml');
// Enable services for CSRF protection (even without forms)
$loader->load('security.xml');
$loader->load('security_csrf.xml');
if ($container->getParameter('kernel.debug')) {
$loader->load('debug.xml');
@ -158,9 +162,7 @@ class FrameworkExtension extends Extension
if (!isset($config['session'])) {
throw new \LogicException('CSRF protection needs that sessions are enabled.');
}
if (!isset($config['secret'])) {
throw new \LogicException('CSRF protection needs a secret to be set.');
}
$loader->load('form_csrf.xml');
$container->setParameter('form.type_extension.csrf.enabled', true);

View File

@ -4,15 +4,8 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="form.csrf_provider.class">Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider</parameter>
</parameters>
<services>
<service id="form.csrf_provider" class="%form.csrf_provider.class%">
<argument type="service" id="session" />
<argument>%kernel.secret%</argument>
</service>
<service id="form.csrf_provider" class="Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfTokenGeneratorAdapter" parent="security.csrf.token_generator" />
<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension">
<tag name="form.type_extension" alias="form" />

View File

@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="security.secure_random.class">Symfony\Component\Security\Core\Util\SecureRandom</parameter>
</parameters>
<services>
<!-- Pseudo-Random Number Generator -->
<service id="security.secure_random" class="%security.secure_random.class%">
<tag name="monolog.logger" channel="security" />
<argument>%kernel.cache_dir%/secure_random.seed</argument>
<argument type="service" id="logger" on-invalid="ignore" />
</service>
</services>
</container>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="security.csrf.token_generator.class">Symfony\Component\Security\Csrf\CsrfTokenGenerator</parameter>
<parameter key="security.csrf.token_storage.class">Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage</parameter>
</parameters>
<services>
<service id="security.csrf.token_storage" class="%security.csrf.token_storage.class%" public="false">
<argument type="service" id="session" />
</service>
<service id="security.csrf.token_generator" class="%security.csrf.token_generator.class%">
<argument type="service" id="security.csrf.token_storage" />
<argument type="service" id="security.secure_random" />
</service>
</services>
</container>

View File

@ -247,7 +247,7 @@ class FormHelper extends Helper
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* $csrfProvider = $this->get('security.csrf.token_generator');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }

View File

@ -30,7 +30,6 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertEquals('%form.type_extension.csrf.enabled%', $def->getArgument(1));
$this->assertEquals('_csrf', $container->getParameter('form.type_extension.csrf.field_name'));
$this->assertEquals('%form.type_extension.csrf.field_name%', $def->getArgument(2));
$this->assertEquals('s3cr3t', $container->getParameterBag()->resolveValue($container->findDefinition('form.csrf_provider')->getArgument(1)));
}
public function testProxies()

View File

@ -46,7 +46,7 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
));
return array_merge(parent::getExtensions(), array(
new TemplatingExtension($this->engine, $this->csrfProvider, array(
new TemplatingExtension($this->engine, $this->csrfTokenGenerator, array(
'FrameworkBundle:Form',
)),
));

View File

@ -46,7 +46,7 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
));
return array_merge(parent::getExtensions(), array(
new TemplatingExtension($this->engine, $this->csrfProvider, array(
new TemplatingExtension($this->engine, $this->csrfTokenGenerator, array(
'FrameworkBundle:Form',
'FrameworkBundle:FormTable',
)),

View File

@ -23,6 +23,8 @@
"symfony/http-kernel": "~2.3",
"symfony/filesystem": "~2.3",
"symfony/routing": "~2.2",
"symfony/security-core": "~2.4",
"symfony/security-csrf": "~2.4",
"symfony/stopwatch": "~2.3",
"symfony/templating": "~2.1",
"symfony/translation": "~2.3",
@ -30,7 +32,7 @@
},
"require-dev": {
"symfony/finder": "~2.0",
"symfony/security": "~2.3",
"symfony/security": "~2.4",
"symfony/form": "~2.3",
"symfony/class-loader": "~2.1",
"symfony/validator": "~2.1"

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* Added 'host' option to firewall configuration
* Moved 'security.secure_random' service configuration to FrameworkBundle
2.3.0
-----
@ -79,9 +80,9 @@ CHANGELOG
logout:
path: /logout_path
target: /
csrf_parameter: _csrf_token # Optional (defaults to "_csrf_token")
csrf_provider: form.csrf_provider # Required to enable protection
intention: logout # Optional (defaults to "logout")
csrf_parameter: _csrf_token # Optional (defaults to "_csrf_token")
csrf_provider: security.csrf.token_generator # Required to enable protection
intention: logout # Optional (defaults to "logout")
```
If the LogoutListener has CSRF protection enabled but cannot validate a token,

View File

@ -151,12 +151,5 @@
<argument type="service" id="security.context" />
<argument type="service" id="security.encoder_factory" />
</service>
<!-- Pseudorandom Number Generator -->
<service id="security.secure_random" class="Symfony\Component\Security\Core\Util\SecureRandom">
<tag name="monolog.logger" channel="security" />
<argument>%kernel.cache_dir%/secure_random.seed</argument>
<argument type="service" id="logger" on-invalid="ignore" />
</service>
</services>
</container>

View File

@ -12,8 +12,8 @@
namespace Symfony\Bundle\SecurityBundle\Templating\Helper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Templating\Helper\Helper;
/**
@ -43,15 +43,15 @@ class LogoutUrlHelper extends Helper
/**
* Registers a firewall's LogoutListener, allowing its URL to be generated.
*
* @param string $key The firewall key
* @param string $logoutPath The path that starts the logout process
* @param string $intention The intention for CSRF token generation
* @param string $csrfParameter The CSRF token parameter name
* @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance
* @param string $key The firewall key
* @param string $logoutPath The path that starts the logout process
* @param string $csrfTokenId The ID of the CSRF token
* @param string $csrfParameter The CSRF token parameter name
* @param CsrfTokenGeneratorInterface $csrfTokenGenerator A CsrfTokenGeneratorInterface instance
*/
public function registerListener($key, $logoutPath, $intention, $csrfParameter, CsrfProviderInterface $csrfProvider = null)
public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, CsrfTokenGeneratorInterface $csrfTokenGenerator = null)
{
$this->listeners[$key] = array($logoutPath, $intention, $csrfParameter, $csrfProvider);
$this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenGenerator);
}
/**
@ -94,9 +94,9 @@ class LogoutUrlHelper extends Helper
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key));
}
list($logoutPath, $intention, $csrfParameter, $csrfProvider) = $this->listeners[$key];
list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenGenerator) = $this->listeners[$key];
$parameters = null !== $csrfProvider ? array($csrfParameter => $csrfProvider->generateCsrfToken($intention)) : array();
$parameters = null !== $csrfTokenGenerator ? array($csrfParameter => $csrfTokenGenerator->generateCsrfToken($csrfTokenId)) : array();
if ('/' === $logoutPath[0]) {
$request = $this->container->get('request');

View File

@ -37,12 +37,12 @@ security:
username_parameter: "user_login[username]"
password_parameter: "user_login[password]"
csrf_parameter: "user_login[_token]"
csrf_provider: form.csrf_provider
csrf_provider: security.csrf.token_generator
anonymous: ~
logout:
path: /logout_path
target: /
csrf_provider: form.csrf_provider
csrf_provider: security.csrf.token_generator
access_control:
- { path: .*, roles: IS_AUTHENTICATED_FULLY }

View File

@ -17,13 +17,12 @@
],
"require": {
"php": ">=5.3.3",
"symfony/security": "~2.2",
"symfony/security": "~2.4",
"symfony/http-kernel": "~2.2"
},
"require-dev": {
"symfony/framework-bundle": "~2.2",
"symfony/twig-bundle": "~2.2",
"symfony/form": "~2.1",
"symfony/validator": "~2.2",
"symfony/yaml": "~2.0",
"symfony/expression-language": "~2.4"

View File

@ -1,9 +1,15 @@
CHANGELOG
=========
2.4.0
-----
* moved CSRF implementation to the new Security CSRF sub-component
* deprecated CsrfProviderInterface and its implementations
* deprecated options "csrf_provider" and "intention" in favor of the new options "csrf_token_generator" and "csrf_token_id"
2.3.0
------
-----
* deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace
* deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace

View File

@ -12,8 +12,8 @@
namespace Symfony\Component\Form\Extension\Csrf;
use Symfony\Component\Form\Extension\Csrf\Type;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
@ -24,9 +24,9 @@ use Symfony\Component\Translation\TranslatorInterface;
class CsrfExtension extends AbstractExtension
{
/**
* @var CsrfProviderInterface
* @var CsrfTokenGeneratorInterface
*/
private $csrfProvider;
private $tokenGenerator;
/**
* @var TranslatorInterface
@ -41,13 +41,13 @@ class CsrfExtension extends AbstractExtension
/**
* Constructor.
*
* @param CsrfProviderInterface $csrfProvider The CSRF provider
* @param TranslatorInterface $translator The translator for translating error messages.
* @param null|string $translationDomain The translation domain for translating.
* @param CsrfTokenGeneratorInterface $tokenGenerator The CSRF token generator
* @param TranslatorInterface $translator The translator for translating error messages
* @param null|string $translationDomain The translation domain for translating
*/
public function __construct(CsrfProviderInterface $csrfProvider, TranslatorInterface $translator = null, $translationDomain = null)
public function __construct(CsrfTokenGeneratorInterface $tokenGenerator, TranslatorInterface $translator = null, $translationDomain = null)
{
$this->csrfProvider = $csrfProvider;
$this->tokenGenerator = $tokenGenerator;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
@ -58,7 +58,7 @@ class CsrfExtension extends AbstractExtension
protected function loadTypeExtensions()
{
return array(
new Type\FormTypeCsrfExtension($this->csrfProvider, true, '_token', $this->translator, $this->translationDomain),
new Type\FormTypeCsrfExtension($this->tokenGenerator, true, '_token', $this->translator, $this->translationDomain),
);
}
}

View File

@ -11,39 +11,16 @@
namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
/**
* Marks classes able to provide CSRF protection
*
* You can generate a CSRF token by using the method generateCsrfToken(). To
* this method you should pass a value that is unique to the page that should
* be secured against CSRF attacks. This value doesn't necessarily have to be
* secret. Implementations of this interface are responsible for adding more
* secret information.
*
* If you want to secure a form submission against CSRF attacks, you could
* supply an "intention" string. This way you make sure that the form can only
* be submitted to pages that are designed to handle the form, that is, that use
* the same intention string to validate the CSRF token with isCsrfTokenValid().
* Alias interface of {@link CsrfTokenGeneratorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use
* {@link CsrfTokenGeneratorInterface} instead.
*/
interface CsrfProviderInterface
interface CsrfProviderInterface extends CsrfTokenGeneratorInterface
{
/**
* Generates a CSRF token for a page of your application.
*
* @param string $intention Some value that identifies the action intention
* (i.e. "authenticate"). Doesn't have to be a secret value.
*/
public function generateCsrfToken($intention);
/**
* Validates a CSRF token.
*
* @param string $intention The intention used when generating the CSRF token
* @param string $token The token supplied by the browser
*
* @return Boolean Whether the token supplied by the browser is correct
*/
public function isCsrfTokenValid($intention, $token);
}

View File

@ -0,0 +1,26 @@
<?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\Component\Form\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\CsrfTokenGenerator;
/**
* Adapter for using the new token generator with the old interface.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0.
*/
class CsrfTokenGeneratorAdapter extends CsrfTokenGenerator implements CsrfProviderInterface
{
}

View File

@ -18,6 +18,11 @@ namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider;
* user-defined secret value to secure the CSRF token.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use
* {@link \Symfony\Component\Security\Csrf\CsrfTokenGenerator} in
* combination with {@link \Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage}
* instead.
*/
class DefaultCsrfProvider implements CsrfProviderInterface
{

View File

@ -20,6 +20,11 @@ use Symfony\Component\HttpFoundation\Session\Session;
* @see DefaultCsrfProvider
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use
* {@link \Symfony\Component\Security\Csrf\CsrfTokenGenerator} in
* combination with {@link \Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage}
* instead.
*/
class SessionCsrfProvider extends DefaultCsrfProvider
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
@ -30,20 +30,20 @@ class CsrfValidationListener implements EventSubscriberInterface
private $fieldName;
/**
* The provider for generating and validating CSRF tokens
* @var CsrfProviderInterface
* The generator for CSRF tokens
* @var CsrfTokenGeneratorInterface
*/
private $csrfProvider;
private $tokenGenerator;
/**
* A text mentioning the intention of the CSRF token
* A text mentioning the tokenId of the CSRF token
*
* Validation of the token will only succeed if it was generated in the
* same session and with the same intention.
* same session and with the same tokenId.
*
* @var string
*/
private $intention;
private $tokenId;
/**
* The message displayed in case of an error.
@ -68,11 +68,11 @@ class CsrfValidationListener implements EventSubscriberInterface
);
}
public function __construct($fieldName, CsrfProviderInterface $csrfProvider, $intention, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null)
public function __construct($fieldName, CsrfTokenGeneratorInterface $tokenGenerator, $tokenId, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null)
{
$this->fieldName = $fieldName;
$this->csrfProvider = $csrfProvider;
$this->intention = $intention;
$this->tokenGenerator = $tokenGenerator;
$this->tokenId = $tokenId;
$this->errorMessage = $errorMessage;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
@ -84,7 +84,7 @@ class CsrfValidationListener implements EventSubscriberInterface
$data = $event->getData();
if ($form->isRoot() && $form->getConfig()->getOption('compound')) {
if (!isset($data[$this->fieldName]) || !$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) {
if (!isset($data[$this->fieldName]) || !$this->tokenGenerator->isCsrfTokenValid($this->tokenId, $data[$this->fieldName])) {
$errorMessage = $this->errorMessage;
if (null !== $this->translator) {

View File

@ -12,12 +12,13 @@
namespace Symfony\Component\Form\Extension\Csrf\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
@ -26,9 +27,9 @@ use Symfony\Component\Translation\TranslatorInterface;
class FormTypeCsrfExtension extends AbstractTypeExtension
{
/**
* @var CsrfProviderInterface
* @var CsrfTokenGeneratorInterface
*/
private $defaultCsrfProvider;
private $defaultTokenGenerator;
/**
* @var Boolean
@ -50,9 +51,9 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
*/
private $translationDomain;
public function __construct(CsrfProviderInterface $defaultCsrfProvider, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null)
public function __construct(CsrfTokenGeneratorInterface $defaultTokenGenerator, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null)
{
$this->defaultCsrfProvider = $defaultCsrfProvider;
$this->defaultTokenGenerator = $defaultTokenGenerator;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;
$this->translator = $translator;
@ -74,8 +75,8 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
$builder
->addEventSubscriber(new CsrfValidationListener(
$options['csrf_field_name'],
$options['csrf_provider'],
$options['intention'],
$options['csrf_token_generator'],
$options['csrf_token_id'],
$options['csrf_message'],
$this->translator,
$this->translationDomain
@ -94,7 +95,7 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
{
if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
$factory = $form->getConfig()->getFormFactory();
$data = $options['csrf_provider']->generateCsrfToken($options['intention']);
$data = $options['csrf_token_generator']->generateCsrfToken($options['csrf_token_id']);
$csrfForm = $factory->createNamed($options['csrf_field_name'], 'hidden', $data, array(
'mapped' => false,
@ -109,12 +110,24 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// BC clause for the "intention" option
$csrfTokenId = function (Options $options) {
return $options['intention'];
};
// BC clause for the "csrf_provider" option
$csrfTokenGenerator = function (Options $options) {
return $options['csrf_provider'];
};
$resolver->setDefaults(array(
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_provider' => $this->defaultCsrfProvider,
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
'intention' => 'unknown',
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
'csrf_token_generator' => $csrfTokenGenerator,
'csrf_token_id' => $csrfTokenId,
'csrf_provider' => $this->defaultTokenGenerator,
'intention' => 'unknown',
));
}

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Templating;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
@ -24,10 +24,10 @@ use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
*/
class TemplatingExtension extends AbstractExtension
{
public function __construct(PhpEngine $engine, CsrfProviderInterface $csrfProvider = null, array $defaultThemes = array())
public function __construct(PhpEngine $engine, CsrfTokenGeneratorInterface $csrfTokenGenerator = null, array $defaultThemes = array())
{
$engine->addHelpers(array(
new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfProvider))
new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfTokenGenerator))
));
}
}

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
/**
* Renders a form into HTML using a rendering engine.
@ -30,9 +30,9 @@ class FormRenderer implements FormRendererInterface
private $engine;
/**
* @var CsrfProviderInterface
* @var CsrfTokenGeneratorInterface
*/
private $csrfProvider;
private $csrfTokenGenerator;
/**
* @var array
@ -49,10 +49,10 @@ class FormRenderer implements FormRendererInterface
*/
private $variableStack = array();
public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
public function __construct(FormRendererEngineInterface $engine, CsrfTokenGeneratorInterface $csrfTokenGenerator = null)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
$this->csrfTokenGenerator = $csrfTokenGenerator;
}
/**
@ -74,13 +74,13 @@ class FormRenderer implements FormRendererInterface
/**
* {@inheritdoc}
*/
public function renderCsrfToken($intention)
public function renderCsrfToken($tokenId)
{
if (null === $this->csrfProvider) {
throw new BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
if (null === $this->csrfTokenGenerator) {
throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenGeneratorInterface is injected in FormRenderer::__construct().');
}
return $this->csrfProvider->generateCsrfToken($intention);
return $this->csrfTokenGenerator->generateCsrfToken($tokenId);
}
/**

View File

@ -73,20 +73,20 @@ interface FormRendererInterface
* <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
* </code>
*
* Check the token in your action using the same intention.
* Check the token in your action using the same token ID.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* $csrfProvider = $this->get('security.csrf.token_generator');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
* @param string $tokenId The ID of the CSRF token
*
* @return string A CSRF token
*/
public function renderCsrfToken($intention);
public function renderCsrfToken($tokenId);
/**
* Makes a technical name human readable.

View File

@ -471,7 +471,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
public function testCsrf()
{
$this->csrfProvider->expects($this->any())
$this->csrfTokenGenerator->expects($this->any())
->method('generateCsrfToken')
->will($this->returnValue('foo&bar'));

View File

@ -17,7 +17,7 @@ use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormIntegrationTestCase
{
protected $csrfProvider;
protected $csrfTokenGenerator;
protected function setUp()
{
@ -27,7 +27,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
\Locale::setDefault('en');
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
$this->csrfTokenGenerator = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface');
parent::setUp();
}
@ -35,13 +35,13 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
protected function getExtensions()
{
return array(
new CsrfExtension($this->csrfProvider),
new CsrfExtension($this->csrfTokenGenerator),
);
}
protected function tearDown()
{
$this->csrfProvider = null;
$this->csrfTokenGenerator = null;
parent::tearDown();
}

View File

@ -336,7 +336,7 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
public function testCsrf()
{
$this->csrfProvider->expects($this->any())
$this->csrfTokenGenerator->expects($this->any())
->method('generateCsrfToken')
->will($this->returnValue('foo&bar'));

View File

@ -19,13 +19,14 @@ class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase
{
protected $dispatcher;
protected $factory;
protected $csrfProvider;
protected $tokenGenerator;
protected $form;
protected function setUp()
{
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
$this->tokenGenerator = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface');
$this->form = $this->getBuilder('post')
->setDataMapper($this->getDataMapper())
->getForm();
@ -35,7 +36,7 @@ class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase
{
$this->dispatcher = null;
$this->factory = null;
$this->csrfProvider = null;
$this->tokenGenerator = null;
$this->form = null;
}
@ -65,7 +66,7 @@ class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase
$data = "XP4HUzmHPi";
$event = new FormEvent($this->form, $data);
$validation = new CsrfValidationListener('csrf', $this->csrfProvider, 'unknown', 'Invalid.');
$validation = new CsrfValidationListener('csrf', $this->tokenGenerator, 'unknown', 'Invalid.');
$validation->preSubmit($event);
// Validate accordingly

View File

@ -37,7 +37,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $csrfProvider;
protected $tokenGenerator;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
@ -46,7 +46,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
protected function setUp()
{
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
$this->tokenGenerator = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface');
$this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface');
parent::setUp();
@ -54,7 +54,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
protected function tearDown()
{
$this->csrfProvider = null;
$this->tokenGenerator = null;
$this->translator = null;
parent::tearDown();
@ -63,7 +63,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new CsrfExtension($this->csrfProvider, $this->translator),
new CsrfExtension($this->tokenGenerator, $this->translator),
));
}
@ -123,7 +123,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
public function testGenerateCsrfToken()
{
$this->csrfProvider->expects($this->once())
$this->tokenGenerator->expects($this->once())
->method('generateCsrfToken')
->with('%INTENTION%')
->will($this->returnValue('token'));
@ -131,7 +131,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
$view = $this->factory
->create('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'intention' => '%INTENTION%',
'compound' => true,
))
@ -153,7 +153,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
*/
public function testValidateTokenOnSubmitIfRootAndCompound($valid)
{
$this->csrfProvider->expects($this->once())
$this->tokenGenerator->expects($this->once())
->method('isCsrfTokenValid')
->with('%INTENTION%', 'token')
->will($this->returnValue($valid));
@ -161,7 +161,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
$form = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'intention' => '%INTENTION%',
'compound' => true,
))
@ -182,13 +182,13 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
public function testFailIfRootAndCompoundAndTokenMissing()
{
$this->csrfProvider->expects($this->never())
$this->tokenGenerator->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'intention' => '%INTENTION%',
'compound' => true,
))
@ -209,7 +209,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
public function testDontValidateTokenIfCompoundButNoRoot()
{
$this->csrfProvider->expects($this->never())
$this->tokenGenerator->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory
@ -217,7 +217,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
->add($this->factory
->createNamedBuilder('form', 'form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'intention' => '%INTENTION%',
'compound' => true,
))
@ -233,13 +233,13 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
public function testDontValidateTokenIfRootButNotCompound()
{
$this->csrfProvider->expects($this->never())
$this->tokenGenerator->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory
->create('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'intention' => '%INTENTION%',
'compound' => false,
));
@ -269,7 +269,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
public function testsTranslateCustomErrorMessage()
{
$this->csrfProvider->expects($this->once())
$this->tokenGenerator->expects($this->once())
->method('isCsrfTokenValid')
->with('%INTENTION%', 'token')
->will($this->returnValue(false));
@ -282,7 +282,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
$form = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'csrf_provider' => $this->tokenGenerator,
'csrf_message' => 'Foobar',
'intention' => '%INTENTION%',
'compound' => true,

View File

@ -24,11 +24,14 @@
},
"require-dev": {
"symfony/validator": "~2.2",
"symfony/http-foundation": "~2.2"
"symfony/http-foundation": "~2.2",
"symfony/security-csrf": "~2.4"
},
"suggest": {
"symfony/validator": "",
"symfony/http-foundation": ""
"symfony/validator": "For form validation.",
"symfony/security-csrf": "For protecting forms against CSRF attacks.",
"symfony/twig-bridge": "For templating with Twig.",
"symfony/framework-bundle": "For templating with PHP."
},
"autoload": {
"psr-0": { "Symfony\\Component\\Form\\": "" }

View File

@ -7,6 +7,9 @@ CHANGELOG
* The switch user listener now preserves the query string when switching a user
* The remember-me cookie hashes now use HMAC, which means that current cookies will be invalidated
* added simpler customization options
* structured component into three sub-components Acl, Core and Http
* added Csrf sub-component
* changed Http sub-component to depend on Csrf sub-component instead of the Form component
2.3.0
-----

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,105 @@
<?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\Component\Security\Csrf;
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
use Symfony\Component\Security\Core\Util\SecureRandom;
use Symfony\Component\Security\Core\Util\StringUtils;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
/**
* Generates and validates CSRF tokens.
*
* @since 2.4
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class CsrfTokenGenerator implements CsrfTokenGeneratorInterface
{
/**
* The entropy of the token in bits.
* @var integer
*/
const TOKEN_ENTROPY = 256;
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* The generator for random values.
* @var SecureRandomInterface
*/
private $random;
/**
* Creates a new CSRF provider using PHP's native session storage.
*
* @param TokenStorageInterface $storage The storage for storing generated
* CSRF tokens
* @param SecureRandomInterface $random The used random value generator
* @param integer $entropy The amount of entropy collected for
* newly generated tokens (in bits)
*
*/
public function __construct(TokenStorageInterface $storage = null, SecureRandomInterface $random = null, $entropy = self::TOKEN_ENTROPY)
{
if (null === $storage) {
$storage = new NativeSessionTokenStorage();
}
if (null === $random) {
$random = new SecureRandom();
}
$this->storage = $storage;
$this->random = $random;
$this->entropy = $entropy;
}
/**
* {@inheritDoc}
*/
public function generateCsrfToken($tokenId)
{
$currentToken = $this->storage->getToken($tokenId, false);
// Token exists and is still valid
if (false !== $currentToken) {
return $currentToken;
}
// Token needs to be (re)generated
// Generate an URI safe base64 encoded string that does not contain "+",
// "/" or "=" which need to be URL encoded and make URLs unnecessarily
// longer.
$bytes = $this->random->nextBytes($this->entropy / 8);
$token = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
$this->storage->setToken($tokenId, $token);
return $token;
}
/**
* {@inheritDoc}
*/
public function isCsrfTokenValid($tokenId, $token)
{
if (!$this->storage->hasToken($tokenId)) {
return false;
}
return StringUtils::equals((string) $this->storage->getToken($tokenId), $token);
}
}

View File

@ -0,0 +1,52 @@
<?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\Component\Security\Csrf;
/**
* Generates and validates CSRF tokens.
*
* You can generate a CSRF token by using the method {@link generateCsrfToken()}.
* This method expects a unique token ID as argument. The token ID can later be
* used to validate a token provided by the user.
*
* Token IDs do not necessarily have to be secret, but they should NEVER be
* created from data provided by the client. A good practice is to hard-code the
* token IDs for the various CSRF tokens used by your application.
*
* You should use the method {@link isCsrfTokenValid()} to check a CSRF token
* submitted by the client. This method will return true if the CSRF token is
* valid.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface CsrfTokenGeneratorInterface
{
/**
* Generates a CSRF token with the given token ID.
*
* @param string $tokenId An ID that identifies the token
*
* @return string The generated CSRF token
*/
public function generateCsrfToken($tokenId);
/**
* Validates a CSRF token.
*
* @param string $tokenId The token ID used when generating the token
* @param string $token The token supplied by the client
*
* @return Boolean Whether the token supplied by the client is correct
*/
public function isCsrfTokenValid($tokenId, $token);
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2013 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,21 @@
Security Component - CSRF
=========================
The Security CSRF (cross-site request forgery) component provides a class
`CsrfTokenGenerator` for generating and validating CSRF tokens.
Resources
---------
Documentation:
http://symfony.com/doc/2.4/book/security.html
Tests
-----
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/Security/Csrf/
$ composer.phar install --dev
$ phpunit

View File

@ -0,0 +1,148 @@
<?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\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\CsrfTokenGenerator;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfTokenGeneratorTest extends \PHPUnit_Framework_TestCase
{
/**
* A non alpha-numeric byte string
* @var string
*/
private static $bytes;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $random;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $storage;
/**
* @var CsrfTokenGenerator
*/
private $generator;
public static function setUpBeforeClass()
{
self::$bytes = base64_decode('aMf+Tct/RLn2WQ==');
}
protected function setUp()
{
$this->random = $this->getMock('Symfony\Component\Security\Core\Util\SecureRandomInterface');
$this->storage = $this->getMock('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface');
$this->generator = new CsrfTokenGenerator($this->storage, $this->random);
}
protected function tearDown()
{
$this->random = null;
$this->storage = null;
$this->generator = null;
}
public function testGenerateNewToken()
{
$this->storage->expects($this->once())
->method('getToken')
->with('token_id', false)
->will($this->returnValue(false));
$this->storage->expects($this->once())
->method('setToken')
->with('token_id', $this->anything())
->will($this->returnCallback(function ($tokenId, $token) use (&$storedToken) {
$storedToken = $token;
}));
$this->random->expects($this->once())
->method('nextBytes')
->will($this->returnValue(self::$bytes));
$token = $this->generator->generateCsrfToken('token_id');
$this->assertSame($token, $storedToken);
$this->assertTrue(ctype_print($token), 'is printable');
$this->assertStringNotMatchesFormat('%S+%S', $token, 'is URI safe');
$this->assertStringNotMatchesFormat('%S/%S', $token, 'is URI safe');
$this->assertStringNotMatchesFormat('%S=%S', $token, 'is URI safe');
}
public function testUseExistingTokenIfAvailable()
{
$this->storage->expects($this->once())
->method('getToken')
->with('token_id', false)
->will($this->returnValue('TOKEN'));
$this->storage->expects($this->never())
->method('setToken');
$this->random->expects($this->never())
->method('nextBytes');
$token = $this->generator->generateCsrfToken('token_id');
$this->assertEquals('TOKEN', $token);
}
public function testMatchingTokenIsValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
->method('getToken')
->with('token_id')
->will($this->returnValue('TOKEN'));
$this->assertTrue($this->generator->isCsrfTokenValid('token_id', 'TOKEN'));
}
public function testNonMatchingTokenIsNotValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
->method('getToken')
->with('token_id')
->will($this->returnValue('TOKEN'));
$this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR'));
}
public function testNonExistingTokenIsNotValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(false));
$this->storage->expects($this->never())
->method('getToken');
$this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR'));
}
}

View File

@ -0,0 +1,99 @@
<?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\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @runTestsInSeparateProcesses
*/
class NativeSessionTokenStorageTest extends \PHPUnit_Framework_TestCase
{
const SESSION_NAMESPACE = 'foobar';
/**
* @var NativeSessionTokenStorage
*/
private $storage;
public static function setUpBeforeClass()
{
ini_set('session.save_handler', 'files');
ini_set('session.save_path', sys_get_temp_dir());
parent::setUpBeforeClass();
}
protected function setUp()
{
$_SESSION = array();
$this->storage = new NativeSessionTokenStorage(self::SESSION_NAMESPACE);
}
public function testStoreTokenInClosedSession()
{
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
public function testStoreTokenInClosedSessionWithExistingSessionId()
{
session_id('foobar');
$this->assertSame(PHP_SESSION_NONE, session_status());
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(PHP_SESSION_ACTIVE, session_status());
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
public function testStoreTokenInActiveSession()
{
session_start();
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
/**
* @depends testStoreTokenInClosedSession
*/
public function testCheckToken()
{
$this->assertFalse($this->storage->hasToken('token_id'));
$this->storage->setToken('token_id', 'TOKEN');
$this->assertTrue($this->storage->hasToken('token_id'));
}
/**
* @depends testStoreTokenInClosedSession
*/
public function testGetExistingToken()
{
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame('TOKEN', $this->storage->getToken('token_id'));
}
public function testGetNonExistingToken()
{
$this->assertSame('DEFAULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
}

View File

@ -0,0 +1,144 @@
<?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\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase
{
const SESSION_NAMESPACE = 'foobar';
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $session;
/**
* @var SessionTokenStorage
*/
private $storage;
protected function setUp()
{
if (!class_exists('Symfony\Component\HttpFoundation\Session\SessionInterface')) {
$this->markTestSkipped('The "HttpFoundation" component is not available');
}
$this->session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')
->disableOriginalConstructor()
->getMock();
$this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE);
}
public function testStoreTokenInClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('set')
->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN');
$this->storage->setToken('token_id', 'TOKEN');
}
public function testStoreTokenInActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('set')
->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN');
$this->storage->setToken('token_id', 'TOKEN');
}
public function testCheckTokenInClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('has')
->with(self::SESSION_NAMESPACE.'/token_id')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->hasToken('token_id'));
}
public function testCheckTokenInActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('has')
->with(self::SESSION_NAMESPACE.'/token_id')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->hasToken('token_id'));
}
public function testGetTokenFromClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('get')
->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
public function testGetTokenFromActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('get')
->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
}

View File

@ -0,0 +1,101 @@
<?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\Component\Security\Csrf\TokenStorage;
/**
* Token storage that uses PHP's native session handling.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeSessionTokenStorage implements TokenStorageInterface
{
/**
* The namespace used to store values in the session.
* @var string
*/
const SESSION_NAMESPACE = '_csrf';
/**
* @var Boolean
*/
private $sessionStarted = false;
/**
* @var string
*/
private $namespace;
/**
* Initializes the storage with a session namespace.
*
* @param string $namespace The namespace under which the token is stored
* in the session
*/
public function __construct($namespace = self::SESSION_NAMESPACE)
{
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
public function getToken($tokenId, $default = null)
{
if (!$this->sessionStarted) {
$this->startSession();
}
if (isset($_SESSION[$this->namespace][$tokenId])) {
return $_SESSION[$this->namespace][$tokenId];
}
return $default;
}
/**
* {@inheritdoc}
*/
public function setToken($tokenId, $token)
{
if (!$this->sessionStarted) {
$this->startSession();
}
$_SESSION[$this->namespace][$tokenId] = $token;
}
/**
* {@inheritdoc}
*/
public function hasToken($tokenId)
{
if (!$this->sessionStarted) {
$this->startSession();
}
return isset($_SESSION[$this->namespace][$tokenId]);
}
private function startSession()
{
if (version_compare(PHP_VERSION, '5.4', '>=')) {
if (PHP_SESSION_NONE === session_status()) {
session_start();
}
} elseif (!session_id()) {
session_start();
}
$this->sessionStarted = true;
}
}

View File

@ -0,0 +1,89 @@
<?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\Component\Security\Csrf\TokenStorage;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Token storage that uses a Symfony2 Session object.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorage implements TokenStorageInterface
{
/**
* The namespace used to store values in the session.
* @var string
*/
const SESSION_NAMESPACE = '_csrf';
/**
* The user session from which the session ID is returned
* @var SessionInterface
*/
private $session;
/**
* @var string
*/
private $namespace;
/**
* Initializes the storage with a Session object and a session namespace.
*
* @param SessionInterface $session The user session
* @param string $namespace The namespace under which the token
* is stored in the session
*/
public function __construct(SessionInterface $session, $namespace = self::SESSION_NAMESPACE)
{
$this->session = $session;
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
public function getToken($tokenId, $default = null)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
return $this->session->get($this->namespace . '/' . $tokenId, $default);
}
/**
* {@inheritdoc}
*/
public function setToken($tokenId, $token)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
$this->session->set($this->namespace . '/' . $tokenId, $token);
}
/**
* {@inheritdoc}
*/
public function hasToken($tokenId)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
return $this->session->has($this->namespace . '/' . $tokenId);
}
}

View File

@ -0,0 +1,49 @@
<?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\Component\Security\Csrf\TokenStorage;
/**
* Stores CSRF tokens.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TokenStorageInterface
{
/**
* Reads a stored CSRF token.
*
* @param string $tokenId The token ID
* @param mixed $default The value to be returned if no token is set
*
* @return mixed The stored token or the default value, if no token is set
*/
public function getToken($tokenId, $default = null);
/**
* Stores a CSRF token.
*
* @param string $tokenId The token ID
* @param mixed $token The CSRF token
*/
public function setToken($tokenId, $token);
/**
* Checks whether a token with the given token ID exists.
*
* @param string $tokenId The token ID
*
* @return Boolean Returns true if a token is stored for the given token ID,
* false otherwise.
*/
public function hasToken($tokenId);
}

View File

@ -0,0 +1,38 @@
{
"name": "symfony/security-csrf",
"type": "library",
"description": "Symfony Security Component - CSRF Library",
"keywords": [],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3",
"symfony/security-core": "~2.4"
},
"require-dev": {
"symfony/http-foundation": "~2.1"
},
"suggest": {
"symfony/http-foundation": "For using the class SessionTokenStorage."
},
"autoload": {
"psr-0": { "Symfony\\Component\\Security\\Csrf\\": "" }
},
"target-dir": "Symfony/Component/Security/Csrf",
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Symfony Security Component CSRF Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./vendor</directory>
<directory>./Tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -11,12 +11,12 @@
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
@ -34,18 +34,18 @@ class LogoutListener implements ListenerInterface
private $handlers;
private $successHandler;
private $httpUtils;
private $csrfProvider;
private $csrfTokenGenerator;
/**
* Constructor.
*
* @param SecurityContextInterface $securityContext
* @param HttpUtils $httpUtils An HttpUtilsInterface instance
* @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance
* @param array $options An array of options to process a logout attempt
* @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance
* @param HttpUtils $httpUtils An HttpUtilsInterface instance
* @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance
* @param array $options An array of options to process a logout attempt
* @param CsrfTokenGeneratorInterface $csrfTokenGenerator A CsrfTokenGeneratorInterface instance
*/
public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), CsrfProviderInterface $csrfProvider = null)
public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), CsrfTokenGeneratorInterface $csrfTokenGenerator = null)
{
$this->securityContext = $securityContext;
$this->httpUtils = $httpUtils;
@ -55,7 +55,7 @@ class LogoutListener implements ListenerInterface
'logout_path' => '/logout',
), $options);
$this->successHandler = $successHandler;
$this->csrfProvider = $csrfProvider;
$this->csrfTokenGenerator = $csrfTokenGenerator;
$this->handlers = array();
}
@ -72,7 +72,7 @@ class LogoutListener implements ListenerInterface
/**
* Performs the logout if requested
*
* If a CsrfProviderInterface instance is available, it will be used to
* If a CsrfTokenGeneratorInterface instance is available, it will be used to
* validate the request.
*
* @param GetResponseEvent $event A GetResponseEvent instance
@ -88,10 +88,10 @@ class LogoutListener implements ListenerInterface
return;
}
if (null !== $this->csrfProvider) {
if (null !== $this->csrfTokenGenerator) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new LogoutException('Invalid CSRF token.');
}
}

View File

@ -13,10 +13,11 @@ namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\HttpUtils;
@ -29,7 +30,7 @@ use Psr\Log\LoggerInterface;
class SimpleFormAuthenticationListener extends AbstractAuthenticationListener
{
private $simpleAuthenticator;
private $csrfProvider;
private $csrfTokenGenerator;
/**
* Constructor.
@ -46,16 +47,16 @@ class SimpleFormAuthenticationListener extends AbstractAuthenticationListener
* @param LoggerInterface $logger A LoggerInterface instance
* @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
* @param SimpleFormAuthenticatorInterface $simpleAuthenticator A SimpleFormAuthenticatorInterface instance
* @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance
* @param CsrfTokenGeneratorInterface $csrfTokenGenerator A CsrfTokenGeneratorInterface instance
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null, SimpleFormAuthenticatorInterface $simpleAuthenticator = null)
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenGeneratorInterface $csrfTokenGenerator = null, SimpleFormAuthenticatorInterface $simpleAuthenticator = null)
{
if (!$simpleAuthenticator) {
throw new \InvalidArgumentException('Missing simple authenticator');
}
$this->simpleAuthenticator = $simpleAuthenticator;
$this->csrfProvider = $csrfProvider;
$this->csrfTokenGenerator = $csrfTokenGenerator;
$options = array_merge(array(
'username_parameter' => '_username',
@ -84,10 +85,10 @@ class SimpleFormAuthenticationListener extends AbstractAuthenticationListener
*/
protected function attemptAuthentication(Request $request)
{
if (null !== $this->csrfProvider) {
if (null !== $this->csrfTokenGenerator) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}

View File

@ -11,9 +11,9 @@
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
@ -32,12 +32,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationListener
{
private $csrfProvider;
private $csrfTokenGenerator;
/**
* {@inheritdoc}
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenGeneratorInterface $csrfTokenGenerator = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
@ -47,7 +47,7 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfProvider = $csrfProvider;
$this->csrfTokenGenerator = $csrfTokenGenerator;
}
/**
@ -67,10 +67,10 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL
*/
protected function attemptAuthentication(Request $request)
{
if (null !== $this->csrfProvider) {
if (null !== $this->csrfTokenGenerator) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}

View File

@ -25,10 +25,11 @@
"require-dev": {
"symfony/form": "~2.0",
"symfony/routing": "~2.2",
"symfony/security-csrf": "~2.4",
"psr/log": "~1.0"
},
"suggest": {
"symfony/form": "",
"symfony/security-csrf": "",
"symfony/routing": ""
},
"autoload": {

View File

@ -24,6 +24,7 @@
"replace": {
"symfony/security-acl": "self.version",
"symfony/security-core": "self.version",
"symfony/security-csrf": "self.version",
"symfony/security-http": "self.version"
},
"require-dev": {