merged branch kriswallsmith/csrf-token-helper (PR #3080)

Commits
-------

753c067 [FrameworkBundle] added $view['form']->csrfToken() helper
e1aced8 [Twig] added {{ csrf_token() }} helper

Discussion
----------

[Twig] [FrameworkBundle] added CSRF token helper

I've added a templating helper and Twig function for generating a CSRF token without the overhead of creating a form.

```html+jinja
<form action="{{ path('user_delete', { 'id': user.id }) }}" method="post">
    <input type="hidden" name="_method" value="delete">
    <input type="hidden" name="_token" value="{{ csrf_token('delete_user_' ~ user.id) }}">
    <button type="submit">delete</button>
</form>
```

```php
<?php

class UserController extends Controller
{
    public function delete(User $user, Request $request)
    {
        $csrfProvider = $this->get('form.csrf_provider');
        if (!$csrfProvider->isCsrfTokenValid('delete_user_'.$user->getId(), $request->request->get('_token')) {
            throw new RuntimeException('CSRF attack detected.');
        }

        // etc...
    }
}
```

The test that is failing on Travis appears to be unrelated, but I may be wrong?

```
1) Symfony\Bundle\SecurityBundle\Tests\Functional\LocalizedRoutesAsPathTest::testLoginLogoutProcedure with data set #1 ('de')
RuntimeException: OUTPUT:
Catchable fatal error: Argument 3 passed to Symfony\Bundle\FrameworkBundle\Controller\TraceableControllerResolver::__construct() must be an instance of Symfony\Component\HttpKernel\Debug\Stopwatch, instance of Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser given, called in /tmp/2.1.0-DEV/StandardFormLogin/cache/securitybundletest/appSecuritybundletestDebugProjectContainer.php on line 94 and defined in /home/vagrant/builds/kriswallsmith/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/TraceableControllerResolver.php on line 37
```

---------------------------------------------------------------------------

by pablodip at 2012-01-10T14:18:45Z

As you don't need forms to use the csrf provider, how about putting its service without the form prefix? It could even make sense to put the CsrfProvider as a component since you can use it standalone and in more cases than only forms. It would be a small component though.

---------------------------------------------------------------------------

by Tobion at 2012-01-10T17:54:14Z

I think it would be more clear to generate the token in the controller. Doing so in the template will spread the CSRF intention across template and controller. So I don't think this extension is necessary.

---------------------------------------------------------------------------

by kriswallsmith at 2012-01-10T17:58:14Z

@pablodip I'm open to the idea of a Csrf component. This would be a good place for some nonce classes as well.

@Tobion I disagree. One use case is for a list of users, each with a delete form. Iterating over the users in the controller and generating a token for each, just to iterate over them again in the view is a waste and adds complexity.

---------------------------------------------------------------------------

by Tobion at 2012-01-10T18:05:14Z

I see. But I don't understand why the intention needs to be different for each user to delete. Usually the intention is the same for each form type. I thought this is enough.

---------------------------------------------------------------------------

by kriswallsmith at 2012-01-10T18:06:13Z

Yes, a static intention would suffice.

---------------------------------------------------------------------------

by Tobion at 2012-01-10T18:07:08Z

Then your use case is not valid anymore.

---------------------------------------------------------------------------

by Tobion at 2012-01-10T18:12:25Z

I would suggest to make a cookbook article out of it about how to create a simple form without the form component.
And include such things as validating the result using the validator component and checking the CSRF.

---------------------------------------------------------------------------

by kriswallsmith at 2012-01-10T21:32:50Z

This helper makes it easier to use CSRF protection without a form and we should make it as easy as possible. Spreading the intention across controller and template is not concerning to me. Either way, a cookbook entry is a great idea.

---------------------------------------------------------------------------

by Tobion at 2012-01-10T21:47:12Z

Well, it's just one line more without this helper. So I disagree it makes it really easier when you know how to use the CsrfProvider which is a pre-condition anyway since you must still validate its correctness by hand.

---------------------------------------------------------------------------

by kriswallsmith at 2012-01-13T13:24:15Z

Another use case is when rendering a page with a bunch of simple buttons with different intentions: delete user, delete comment, follow, unfollow... Creating all of these in the controller just leads to spaghetti.

---------------------------------------------------------------------------

by jwage at 2012-01-17T21:55:53Z

👍 lots of use cases for something like this @OpenSky
This commit is contained in:
Fabien Potencier 2012-01-22 10:31:29 +01:00
commit 8358cbf7a6
8 changed files with 76 additions and 9 deletions

View File

@ -14,6 +14,7 @@ namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Util\FormUtil;
/**
@ -24,6 +25,7 @@ use Symfony\Component\Form\Util\FormUtil;
*/
class FormExtension extends \Twig_Extension
{
protected $csrfProvider;
protected $resources;
protected $blocks;
protected $environment;
@ -31,8 +33,9 @@ class FormExtension extends \Twig_Extension
protected $varStack;
protected $template;
public function __construct(array $resources = array())
public function __construct(CsrfProviderInterface $csrfProvider, array $resources = array())
{
$this->csrfProvider = $csrfProvider;
$this->themes = new \SplObjectStorage();
$this->varStack = array();
$this->blocks = new \SplObjectStorage();
@ -81,6 +84,7 @@ class FormExtension extends \Twig_Extension
'form_label' => new \Twig_Function_Method($this, 'renderLabel', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Method($this, 'renderRow', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Method($this, 'renderRest', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'getCsrfToken'),
'_form_is_choice_group' => new \Twig_Function_Method($this, 'isChoiceGroup', array('is_safe' => array('html'))),
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'isChoiceSelected', array('is_safe' => array('html'))),
);
@ -269,6 +273,34 @@ class FormExtension extends \Twig_Extension
));
}
/**
* Returns a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* <input type="hidden" name="token" value="{{ csrf_token('rm_user_' ~ user.id) }}">
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function getCsrfToken($intention)
{
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Returns the name of the extension.
*

View File

@ -97,6 +97,7 @@
<service id="templating.helper.form" class="%templating.helper.form.class%">
<tag name="templating.helper" alias="form" />
<argument type="service" id="templating.engine.php" />
<argument type="service" id="form.csrf_provider" />
<argument>%templating.helper.form.resources%</argument>
</service>

View File

@ -15,6 +15,7 @@ use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Util\FormUtil;
/**
@ -27,6 +28,8 @@ class FormHelper extends Helper
{
protected $engine;
protected $csrfProvider;
protected $varStack;
protected $context;
@ -38,14 +41,16 @@ class FormHelper extends Helper
protected $templates;
/**
* Constructor;
* Constructor.
*
* @param EngineInterface $engine The templating engine
* @param array $resources An array of theme name
* @param EngineInterface $engine The templating engine
* @param CsrfProviderInterface $csrfProvider The CSRF provider
* @param array $resources An array of theme names
*/
public function __construct(EngineInterface $engine, array $resources)
public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider, array $resources)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
$this->resources = $resources;
$this->varStack = array();
$this->context = array();
@ -172,6 +177,34 @@ class FormHelper extends Helper
return $this->renderSection($view, 'rest', $variables);
}
/**
* Returns a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* echo $view['form']->csrfToken('rm_user_'.$user->getId());
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function csrfToken($intention)
{
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Renders a template.
*

View File

@ -37,7 +37,7 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$this->helper = new FormHelper($engine, array('FrameworkBundle:Form'));
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array('FrameworkBundle:Form'));
$engine->setHelpers(array(
$this->helper,

View File

@ -37,7 +37,7 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$this->helper = new FormHelper($engine, array(
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
'FrameworkBundle:Form',
'FrameworkBundle:FormTable'
));

View File

@ -75,6 +75,7 @@
<service id="twig.extension.form" class="%twig.extension.form.class%" public="false">
<tag name="twig.extension" />
<argument type="service" id="form.csrf_provider" />
<argument>%twig.form.resources%</argument>
</service>

View File

@ -38,7 +38,7 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
__DIR__,
));
$this->extension = new FormExtension(array(
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
'form_div_layout.html.twig',
'custom_widgets.html.twig',
));

View File

@ -38,7 +38,7 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
__DIR__,
));
$this->extension = new FormExtension(array(
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
'form_table_layout.html.twig',
'custom_widgets.html.twig',
));