Skip to content

Commit 8358cbf

Browse files
committed
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 :+1: lots of use cases for something like this @opensky
2 parents 61ab4dc + 753c067 commit 8358cbf

File tree

8 files changed

+76
-9
lines changed

8 files changed

+76
-9
lines changed

src/Symfony/Bridge/Twig/Extension/FormExtension.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
1515
use Symfony\Component\Form\FormView;
1616
use Symfony\Component\Form\Exception\FormException;
17+
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
1718
use Symfony\Component\Form\Util\FormUtil;
1819

1920
/**
@@ -24,15 +25,17 @@
2425
*/
2526
class FormExtension extends \Twig_Extension
2627
{
28+
protected $csrfProvider;
2729
protected $resources;
2830
protected $blocks;
2931
protected $environment;
3032
protected $themes;
3133
protected $varStack;
3234
protected $template;
3335

34-
public function __construct(array $resources = array())
36+
public function __construct(CsrfProviderInterface $csrfProvider, array $resources = array())
3537
{
38+
$this->csrfProvider = $csrfProvider;
3639
$this->themes = new \SplObjectStorage();
3740
$this->varStack = array();
3841
$this->blocks = new \SplObjectStorage();
@@ -81,6 +84,7 @@ public function getFunctions()
8184
'form_label' => new \Twig_Function_Method($this, 'renderLabel', array('is_safe' => array('html'))),
8285
'form_row' => new \Twig_Function_Method($this, 'renderRow', array('is_safe' => array('html'))),
8386
'form_rest' => new \Twig_Function_Method($this, 'renderRest', array('is_safe' => array('html'))),
87+
'csrf_token' => new \Twig_Function_Method($this, 'getCsrfToken'),
8488
'_form_is_choice_group' => new \Twig_Function_Method($this, 'isChoiceGroup', array('is_safe' => array('html'))),
8589
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'isChoiceSelected', array('is_safe' => array('html'))),
8690
);
@@ -269,6 +273,34 @@ protected function render(FormView $view, $section, array $variables = array())
269273
));
270274
}
271275

276+
/**
277+
* Returns a CSRF token.
278+
*
279+
* Use this helper for CSRF protection without the overhead of creating a
280+
* form.
281+
*
282+
* <code>
283+
* <input type="hidden" name="token" value="{{ csrf_token('rm_user_' ~ user.id) }}">
284+
* </code>
285+
*
286+
* Check the token in your action using the same intention.
287+
*
288+
* <code>
289+
* $csrfProvider = $this->get('form.csrf_provider');
290+
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
291+
* throw new \RuntimeException('CSRF attack detected.');
292+
* }
293+
* </code>
294+
*
295+
* @param string $intention The intention of the protected action
296+
*
297+
* @return string A CSRF token
298+
*/
299+
public function getCsrfToken($intention)
300+
{
301+
return $this->csrfProvider->generateCsrfToken($intention);
302+
}
303+
272304
/**
273305
* Returns the name of the extension.
274306
*

src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
<service id="templating.helper.form" class="%templating.helper.form.class%">
9898
<tag name="templating.helper" alias="form" />
9999
<argument type="service" id="templating.engine.php" />
100+
<argument type="service" id="form.csrf_provider" />
100101
<argument>%templating.helper.form.resources%</argument>
101102
</service>
102103

src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Templating\EngineInterface;
1616
use Symfony\Component\Form\FormView;
1717
use Symfony\Component\Form\Exception\FormException;
18+
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
1819
use Symfony\Component\Form\Util\FormUtil;
1920

2021
/**
@@ -27,6 +28,8 @@ class FormHelper extends Helper
2728
{
2829
protected $engine;
2930

31+
protected $csrfProvider;
32+
3033
protected $varStack;
3134

3235
protected $context;
@@ -38,14 +41,16 @@ class FormHelper extends Helper
3841
protected $templates;
3942

4043
/**
41-
* Constructor;
44+
* Constructor.
4245
*
43-
* @param EngineInterface $engine The templating engine
44-
* @param array $resources An array of theme name
46+
* @param EngineInterface $engine The templating engine
47+
* @param CsrfProviderInterface $csrfProvider The CSRF provider
48+
* @param array $resources An array of theme names
4549
*/
46-
public function __construct(EngineInterface $engine, array $resources)
50+
public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider, array $resources)
4751
{
4852
$this->engine = $engine;
53+
$this->csrfProvider = $csrfProvider;
4954
$this->resources = $resources;
5055
$this->varStack = array();
5156
$this->context = array();
@@ -172,6 +177,34 @@ public function rest(FormView $view, array $variables = array())
172177
return $this->renderSection($view, 'rest', $variables);
173178
}
174179

180+
/**
181+
* Returns a CSRF token.
182+
*
183+
* Use this helper for CSRF protection without the overhead of creating a
184+
* form.
185+
*
186+
* <code>
187+
* echo $view['form']->csrfToken('rm_user_'.$user->getId());
188+
* </code>
189+
*
190+
* Check the token in your action using the same intention.
191+
*
192+
* <code>
193+
* $csrfProvider = $this->get('form.csrf_provider');
194+
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
195+
* throw new \RuntimeException('CSRF attack detected.');
196+
* }
197+
* </code>
198+
*
199+
* @param string $intention The intention of the protected action
200+
*
201+
* @return string A CSRF token
202+
*/
203+
public function csrfToken($intention)
204+
{
205+
return $this->csrfProvider->generateCsrfToken($intention);
206+
}
207+
175208
/**
176209
* Renders a template.
177210
*

src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected function setUp()
3737
$loader = new FilesystemLoader(array());
3838
$engine = new PhpEngine($templateNameParser, $loader);
3939

40-
$this->helper = new FormHelper($engine, array('FrameworkBundle:Form'));
40+
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array('FrameworkBundle:Form'));
4141

4242
$engine->setHelpers(array(
4343
$this->helper,

src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected function setUp()
3737
$loader = new FilesystemLoader(array());
3838
$engine = new PhpEngine($templateNameParser, $loader);
3939

40-
$this->helper = new FormHelper($engine, array(
40+
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
4141
'FrameworkBundle:Form',
4242
'FrameworkBundle:FormTable'
4343
));

src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575

7676
<service id="twig.extension.form" class="%twig.extension.form.class%" public="false">
7777
<tag name="twig.extension" />
78+
<argument type="service" id="form.csrf_provider" />
7879
<argument>%twig.form.resources%</argument>
7980
</service>
8081

tests/Symfony/Tests/Bridge/Twig/Extension/FormExtensionDivLayoutTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected function setUp()
3838
__DIR__,
3939
));
4040

41-
$this->extension = new FormExtension(array(
41+
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
4242
'form_div_layout.html.twig',
4343
'custom_widgets.html.twig',
4444
));

tests/Symfony/Tests/Bridge/Twig/Extension/FormExtensionTableLayoutTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected function setUp()
3838
__DIR__,
3939
));
4040

41-
$this->extension = new FormExtension(array(
41+
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
4242
'form_table_layout.html.twig',
4343
'custom_widgets.html.twig',
4444
));

0 commit comments

Comments
 (0)