8000 [Form] Refactored CSRF implementation to be reusable and to work corr… · alexfilatov/symfony@7848a7c · GitHub
[go: up one dir, main page]

Skip to content

Commit 7848a7c

Browse files
Bernhard Schussekfabpot
Bernhard Schussek
authored andcommitted
[Form] Refactored CSRF implementation to be reusable and to work correctly with the session service
1 parent d341e8b commit 7848a7c

File tree

12 files changed

+384
-136
lines changed

12 files changed

+384
-136
lines changed

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,6 @@ public function boot()
4848
if ($this->container->hasParameter('document_root')) {
4949
File::setDocumentRoot($this->container->getParameter('document_root'));
5050
}
51-
52-
// the session ID should always be included in the CSRF token, even
53-
// if default CSRF protection is not enabled
54-
if ($container->has('form.default_context') && $container->has('session')) {
55-
$addSessionId = function () use ($container) {
56-
// automatically starts the session when the CSRF token is
57-
// generated
58-
$container->get('session')->start();
59-
60-
return $container->get('session')->getId();
61-
};
62-
63-
// $container->getDefinition('form.default_context')
64-
// ->addMethodCall('addCsrfSecret', array($addSessionId));
65-
//
66-
// var_dump($container->getDefinition('form.default_context'));
67-
}
6851
}
6952

7053
public function registerExtensions(ContainerBuilder $container)

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<parameter key="form.factory.class">Symfony\Component\Form\FormFactory</parameter>
99
<parameter key="form.field_factory.class">Symfony\Component\Form\FieldFactory\FieldFactory</parameter>
1010
<parameter key="form.field_factory.validator_guesser.class">Symfony\Component\Form\FieldFactory\ValidatorFieldFactoryGuesser</parameter>
11+
<parameter key="form.csrf_provider.class">Symfony\Component\Form\CsrfProvider\SessionCsrfProvider</parameter>
1112
<parameter key="form.default_context.class">Symfony\Component\Form\FormContext</parameter>
1213
<parameter key="form.csrf_protection.enabled">true</parameter>
1314
<parameter key="form.csrf_protection.field_name">_token</parameter>
@@ -32,6 +33,12 @@
3233
<tag name="form.field_factory.guesser" />
3334
<argument type="service" id="validator.mapping.class_metadata_factory" />
3435
</service>
36+
37+
<!-- CsrfProvider -->
38+
<service id="form.csrf_provider" class="%form.csrf_provider.class%" public="false">
39+
<argument type="service" id="session" />
40+
<argument>%form.csrf_protection.secret%</argument>
41+
</service>
3542

3643
<!-- FormContext -->
3744
<service id="form.default_context" class="%form.default_context.class%">
@@ -51,10 +58,8 @@
5158
<call method="csrfFieldName">
5259
<argument>%form.csrf_protection.field_name%</argument>
5360
</call>
54-
<call method="csrfSecrets">
55-
<argument type="collection">
56-
<argument>%form.csrf_protection.secret%</argument>
57-
</argument>
61+
<call method="csrfProvider">
62+
<argument type="service" id="form.csrf_provider" />
5863
</call>
5964
</service>
6065

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\CsrfProvider;
13+
14+
/**
15+
* Marks classes able to provide CSRF protection
16+
*
17+
* You can generate a CSRF token by using the method generateCsrfToken(). To
18+
* this method you should pass a value that is unique to the page that should
19+
* be secured against CSRF attacks. This value doesn't necessarily have to be
20+
* secret. Implementations of this interface are responsible for adding more
21+
* secret information.
22+
*
23+
* If you want to secure a form submission against CSRF attacks, you could
24+
* use the class name of the form as page ID. This way you make sure that the
25+
* form can only be submitted to pages that are designed to handle the form,
26+
* that is, that use the same class name to validate the CSRF token with
27+
* isCsrfTokenValid().
28+
*
29+
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
30+
*/
31+
interface CsrfProviderInterface
32+
{
33+
/**
34+
* Generates a CSRF token for a page of your application
35+
*
36+
* @param string $pageId Some value that identifies the page (for example,
37+
* the class name of the form). Doesn't have to be
38+
* a secret value.
39+
*/
40+
public function generateCsrfToken($pageId);
41+
42+
/**
43+
* Validates a CSRF token
44+
*
45+
* @param string $pageId The page ID used when generating the CSRF token
46+
* @param string $token The token supplied by the browser
47+
* @return boolean Whether the token supplied by the browser is
48+
* correct
49+
*/
50+
public function isCsrfTokenValid($pageId, $token);
51+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\CsrfProvider;
13+
14+
/**
15+
* Default implementation of CsrfProviderInterface
16+
*
17+
* This provider uses the session ID returned by session_id() as well as a
18+
* user-defined secret value to secure the CSRF token.
19+
*
20+
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
21+
*/
22+
class DefaultCsrfProvider implements CsrfProviderInterface
23+
{
24+
/**
25+
* A secret value used for generating the CSRF token
26+
* @var string
27+
*/
28+
protected $secret;
29+
30+
/**
31+
* Initializes the provider with a secret value
32+
*
33+
* @param string $secret A secret value included in the CSRF token
34+
*/
35+
public function __construct($secret)
36+
{
37+
$this->secret = $secret;
38+
}
39+
40+
/**
41+
* Returns the ID of the user session
42+
*
43+
* Automatically starts the session if necessary.
44+
*
45+
* @return string The session ID
46+
*/
47+
protected function getSessionId()
48+
{
49+
if (!session_id()) {
50+
session_start();
51+
}
52+
53+
return session_id();
54+
}
55+
56+
/**
57+
* {@inheritDoc}
58+
*/
59+
public function generateCsrfToken($pageId)
60+
{
61+
return sha1($this->secret.$pageId.$this->getSessionId());
62+
}
63+
64+
/**
65+
* {@inheritDoc}
66+
*/
67+
public function isCsrfTokenValid($pageId, $token)
68+
{
69+
return $token === $this->generateCsrfToken($pageId);
70+
}
71+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\CsrfProvider;
13+
14+
use Symfony\Component\HttpFoundation\Session;
15+
16+
/**
17+
* This provider uses a Symfony2 Session object to retrieve the user's
18+
* session ID
19+
*
20+
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
21+
* @see DefaultCsrfProvider
22+
*/
23+
class SessionCsrfProvider extends DefaultCsrfProvider
24+
{
25+
/**
26+
* The user session from which the session ID is returned
27+
* @var Session
28+
*/
29+
protected $session;
30+
31+
/**
32+
* Initializes the provider with a Session object and a secret value
33+
*
34+
* @param Session $session The user session
35+
* @param string $secret A secret value included in the CSRF token
36+
*/
37+
public function __construct(Session $session, $secret)
38+
{
39+
parent::__construct($secret);
40+
41+
$this->session = $session;
42+
}
43+
44+
/**
45+
* Returns the ID of the user session
46+
*
47+
* Automatically starts the session if necessary.
48+
*
49+
* @return string The session ID
50+
*/
51+
protected function getSessionId()
52+
{
53+
$this->session->start();
54+
55+
return $this->session->getId();
56+
}
57+
}

src/Symfony/Component/Form/Form.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Validator\ValidatorInterface;
1515
use Symfony\Component\Form\Exception\FormException;
16+
use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
1617

1718
/**
1819
* Form represents a form.
@@ -31,6 +32,10 @@
3132
*/
3233
class Form extends FieldGroup
3334
{
35+
/**
36+
* The validator to validate form values
37+
* @var ValidatorInterface
38+
*/
3439
protected $validator = null;
3540

3641
/**
@@ -50,9 +55,8 @@ public function __construct($name, $data = null, ValidatorInterface $validator =
5055
$this->setData($data);
5156
}
5257

53-
$this->addOption('csrf_protection');
5458
$this->addOption('csrf_field_name', '_token');
55-
$this->addOption('csrf_secrets', array(__FILE__.php_uname()));
59+
$this->addOption('csrf_provider');
5660
$this->addOption('field_factory');
5761
$this->addOption('validation_groups');
5862

@@ -69,11 +73,17 @@ public function __construct($name, $data = null, ValidatorInterface $validator =
6973
}
7074

7175
// Enable CSRF protection, if necessary
72-
if ($this->getOption('csrf_protection')) {
76+
if ($this->getOption('csrf_provider')) {
77+
if (!$this->getOption('csrf_provider') instanceof CsrfProviderInterface) {
78+
throw new FormException('The object passed to the "csrf_provider" option must implement CsrfProviderInterface');
79+
}
80+
81+
$token = $this->getOption('csrf_provider')->generateCsrfToken(get_class($this));
82+
7383
$field = new HiddenField($this->getOption('csrf_field_name'), array(
7484
'property_path' => null,
7585
));
76-
$field->setData($this->generateCsrfToken($this->getOption('csrf_secrets')));
86+
$field->setData($token);
7787

7888
$this->add($field);
7989
}
@@ -121,13 +131,13 @@ public function getCsrfFieldName()
121131
}
122132

123133
/**
124-
* Returns the secret values used for the CSRF protection
134+
* Returns the provider used for generating and validating CSRF tokens
125135
*
126-
* @return array A list of string valuesf
136+
* @return CsrfProviderInterface The provider instance
127137
*/
128-
public function getCsrfSecrets()
138+
public function getCsrfProvider()
129139
{
130-
return $this->getOption('csrf_secrets');
140+
return $this->getOption('csrf_provider');
131141
}
132142

133143
/**
@@ -236,10 +246,9 @@ public function isCsrfTokenValid()
236246
if (!$this->isCsrfProtected()) {
237247
return true;
238248
} else {
239-
$actual = $this->get($this->getOption('csrf_field_name'))->getDisplayedData();
240-
$expected = $this->generateCsrfToken($this->getOption('csrf_secrets'));
249+
$token = $this->get($this->getOption('csrf_field_name'))->getDisplayedData();
241250

242-
return $actual === $expected;
251+
return $this->getOption('csrf_provider')->isCsrfTokenValid(get_class($this), $token);
243252
}
244253
}
245254

0 commit comments

Comments
 (0)
0