Skip to content

Commit d9a9310

Browse files
committed
Completely re-reading the security book
- more tutorial-styled - tried to move things into other entries - tried to keep as many anchor references as possible
1 parent 153565e commit d9a9310

20 files changed

+1554
-1706
lines changed

Diff for: book/security.rst

+565-1,703
Large diffs are not rendered by default.

Diff for: components/map.rst.inc

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
* :doc:`/components/security/firewall`
115115
* :doc:`/components/security/authentication`
116116
* :doc:`/components/security/authorization`
117+
* :doc:`/components/security/secure-tools`
117118

118119
* **Serializer**
119120

Diff for: components/security/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Security
88
firewall
99
authentication
1010
authorization
11+
secure-tools

Diff for: components/security/secure-tools.rst

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Securely Comparing Strings and Generating Random Numbers
2+
========================================================
3+
4+
.. versionadded:: 2.2
5+
The ``StringUtils`` and ``SecureRandom`` classes were introduced in Symfony
6+
2.2
7+
8+
The Symfony Security component comes with a collection of nice utilities related
9+
to security. These utilities are used by Symfony, but you should also use
10+
them if you want to solve the problem they address.
11+
12+
Comparing Strings
13+
~~~~~~~~~~~~~~~~~
14+
15+
The time it takes to compare two strings depends on their differences. This
16+
can be used by an attacker when the two strings represent a password for
17+
instance; it is known as a `Timing attack`_.
18+
19+
Internally, when comparing two passwords, Symfony uses a constant-time
20+
algorithm; you can use the same strategy in your own code thanks to the
21+
:class:`Symfony\\Component\\Security\\Core\\Util\\StringUtils` class::
22+
23+
use Symfony\Component\Security\Core\Util\StringUtils;
24+
25+
// is password1 equals to password2?
26+
$bool = StringUtils::equals($password1, $password2);
27+
28+
Generating a secure random Number
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
31+
Whenever you need to generate a secure random number, you are highly
32+
encouraged to use the Symfony
33+
:class:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom` class::
34+
35+
use Symfony\Component\Security\Core\Util\SecureRandom;
36+
37+
$generator = new SecureRandom();
38+
$random = $generator->nextBytes(10);
39+
40+
The
41+
:method:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom::nextBytes`
42+
methods returns a random string composed of the number of characters passed as
43+
an argument (10 in the above example).
44+
45+
The SecureRandom class works better when OpenSSL is installed but when it's
46+
not available, it falls back to an internal algorithm, which needs a seed file
47+
to work correctly. Just pass a file name to enable it::
48+
49+
$generator = new SecureRandom('/some/path/to/store/the/seed.txt');
50+
$random = $generator->nextBytes(10);
51+
52+
.. note::
53+
54+
If you're using the Symfony Framework, you can access a secure random
55+
instance directly from the container: its name is ``security.secure_random``.
56+
57+
.. _`Timing attack`: http://en.wikipedia.org/wiki/Timing_attack

Diff for: cookbook/map.rst.inc

+3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138

139139
* :doc:`/cookbook/security/index`
140140

141+
* :doc:`/cookbook/security/form_login_setup`
141142
* :doc:`/cookbook/security/entity_provider`
142143
* :doc:`/cookbook/security/remember_me`
143144
* :doc:`/cookbook/security/impersonating_user`
@@ -153,6 +154,8 @@
153154
* :doc:`/cookbook/security/pre_authenticated`
154155
* :doc:`/cookbook/security/target_path`
155156
* :doc:`/cookbook/security/csrf_in_login_form`
157+
* :doc:`/cookbook/security/access_control`
158+
* :doc:`/cookbook/security/multiple_user_providers`
156159

157160
* **Serializer**
158161

Diff for: cookbook/security/access_control.rst

+299
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
How does the Security access_control Work?
2+
==========================================
3+
4+
For each incoming request, Symfony checks each ``access_control`` entry
5+
to find *one* that matches the current request. As soon as it finds a matching
6+
``access_control`` entry, it stops - only the **first** matching ``access_control``
7+
is used to enforce access.
8+
9+
Each ``access_control`` has several options that configure two different
10+
things:
11+
12+
#. :ref:`should the incoming request match this access control entry <security-book-access-control-matching-options>`
13+
#. :ref:`once it matches, should some sort of access restriction be enforced <security-book-access-control-enforcement-options>`:
14+
15+
.. _security-book-access-control-matching-options:
16+
17+
1. Matching Options
18+
-------------------
19+
20+
Symfony creates an instance of :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher`
21+
for each ``access_control`` entry, which determines whether or not a given
22+
access control should be used on this request. The following ``access_control``
23+
options are used for matching:
24+
25+
* ``path``
26+
* ``ip`` or ``ips``
27+
* ``host``
28+
* ``methods``
29+
30+
Take the following ``access_control`` entries as an example:
31+
32+
.. configuration-block::
33+
34+
.. code-block:: yaml
35+
36+
# app/config/security.yml
37+
security:
38+
# ...
39+
access_control:
40+
- { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
41+
- { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ }
42+
- { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
43+
- { path: ^/admin, roles: ROLE_USER }
44+
45+
.. code-block:: xml
46+
47+
<!-- app/config/security.xml -->
48+
<?xml version="1.0" encoding="UTF-8"?>
49+
<srv:container xmlns="http://symfony.com/schema/dic/security"
50+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
51+
xmlns:srv="http://symfony.com/schema/dic/services"
52+
xsi:schemaLocation="http://symfony.com/schema/dic/services
53+
http://symfony.com/schema/dic/services/services-1.0.xsd">
54+
55+
<config>
56+
<!-- ... -->
57+
<access-control>
58+
<rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
59+
<rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" />
60+
<rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
61+
<rule path="^/admin" role="ROLE_USER" />
62+
</access-control>
63+
</config>
64+
</srv:container>
65+
66+
.. code-block:: php
67+
68+
// app/config/security.php
69+
$container->loadFromExtension('security', array(
70+
// ...
71+
'access_control' => array(
72+
array(
73+
'path' => '^/admin',
74+
'role' => 'ROLE_USER_IP',
75+
'ip' => '127.0.0.1',
76+
),
77+
array(
78+
'path' => '^/admin',
79+
'role' => 'ROLE_USER_HOST',
80+
'host' => 'symfony\.com$',
81+
),
82+
array(
83+
'path' => '^/admin',
84+
'role' => 'ROLE_USER_METHOD',
85+
'method' => 'POST, PUT',
86+
),
87+
array(
88+
'path' => '^/admin',
89+
'role' => 'ROLE_USER',
90+
),
91+
),
92+
));
93+
94+
For each incoming request, Symfony will decide which ``access_control``
95+
to use based on the URI, the client's IP address, the incoming host name,
96+
and the request method. Remember, the first rule that matches is used, and
97+
if ``ip``, ``host`` or ``method`` are not specified for an entry, that ``access_control``
98+
will match any ``ip``, ``host`` or ``method``:
99+
100+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
101+
| URI | IP | HOST | METHOD | ``access_control`` | Why? |
102+
+=================+=============+=============+============+================================+=============================================================+
103+
| ``/admin/user`` | 127.0.0.1 | example.com | GET | rule #1 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. |
104+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
105+
| ``/admin/user`` | 127.0.0.1 | symfony.com | GET | rule #1 (``ROLE_USER_IP``) | The ``path`` and ``ip`` still match. This would also match |
106+
| | | | | | the ``ROLE_USER_HOST`` entry, but *only* the **first** |
107+
| | | | | | ``access_control`` match is used. |
108+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
109+
| ``/admin/user`` | 168.0.0.1 | symfony.com | GET | rule #2 (``ROLE_USER_HOST``) | The ``ip`` doesn't match the first rule, so the second |
110+
| | | | | | rule (which matches) is used. |
111+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
112+
| ``/admin/user`` | 168.0.0.1 | symfony.com | POST | rule #2 (``ROLE_USER_HOST``) | The second rule still matches. This would also match the |
113+
| | | | | | third rule (``ROLE_USER_METHOD``), but only the **first** |
114+
| | | | | | matched ``access_control`` is used. |
115+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
116+
| ``/admin/user`` | 168.0.0.1 | example.com | POST | rule #3 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first two entries, |
117+
| | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. |
118+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
119+
| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_USER``) | The ``ip``, ``host`` and ``method`` prevent the first |
120+
| | | | | | three entries from matching. But since the URI matches the |
121+
| | | | | | ``path`` pattern of the ``ROLE_USER`` entry, it is used. |
122+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
123+
| ``/foo`` | 127.0.0.1 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its |
124+
| | | | | | URI doesn't match any of the ``path`` values. |
125+
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
126+
127+
.. _security-book-access-control-enforcement-options:
128+
129+
2. Access Enforcement
130+
---------------------
131+
132+
Once Symfony has decided which ``access_control`` entry matches (if any),
133+
it then *enforces* access restrictions based on the ``roles`` and ``requires_channel``
134+
options:
135+
136+
* ``role`` If the user does not have the given role(s), then access is denied
137+
(internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
138+
is thrown);
139+
140+
* ``requires_channel`` If the incoming request's channel (e.g. ``http``)
141+
does not match this value (e.g. ``https``), the user will be redirected
142+
(e.g. redirected from ``http`` to ``https``, or vice versa).
143+
144+
.. tip::
145+
146+
If access is denied, the system will try to authenticate the user if not
147+
already (e.g. redirect the user to the login page). If the user is already
148+
logged in, the 403 "access denied" error page will be shown. See
149+
:doc:`/cookbook/controller/error_pages` for more information.
150+
151+
.. _book-security-securing-ip:
152+
153+
Securing by IP
154+
--------------
155+
156+
Certain situations may arise when you may need to restrict access to a given
157+
path based on IP. This is particularly relevant in the case of
158+
:ref:`Edge Side Includes <edge-side-includes>` (ESI), for example. When ESI is
159+
enabled, it's recommended to secure access to ESI URLs. Indeed, some ESI may
160+
contain some private content like the current logged in user's information. To
161+
prevent any direct access to these resources from a web browser (by guessing the
162+
ESI URL pattern), the ESI route **must** be secured to be only visible from
163+
the trusted reverse proxy cache.
164+
165+
.. versionadded:: 2.3
166+
Version 2.3 allows multiple IP addresses in a single rule with the ``ips: [a, b]``
167+
construct. Prior to 2.3, users should create one rule per IP address to match and
168+
use the ``ip`` key instead of ``ips``.
169+
170+
.. caution::
171+
172+
As you'll read in the explanation below the example, the ``ip`` option
173+
does not restrict to a specific IP address. Instead, using the ``ip``
174+
key means that the ``access_control`` entry will only match this IP address,
175+
and users accessing it from a different IP address will continue down
176+
the ``access_control`` list.
177+
178+
Here is an example of how you might secure all ESI routes that start with a
179+
given prefix, ``/esi``, from outside access:
180+
181+
.. configuration-block::
182+
183+
.. code-block:: yaml
184+
185+
# app/config/security.yml
186+
security:
187+
# ...
188+
access_control:
189+
- { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
190+
- { path: ^/esi, roles: ROLE_NO_ACCESS }
191+
192+
.. code-block:: xml
193+
194+
<!-- app/config/security.xml -->
195+
<?xml version="1.0" encoding="UTF-8"?>
196+
<srv:container xmlns="http://symfony.com/schema/dic/security"
197+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
198+
xmlns:srv="http://symfony.com/schema/dic/services"
199+
xsi:schemaLocation="http://symfony.com/schema/dic/services
200+
http://symfony.com/schema/dic/services/services-1.0.xsd">
201+
202+
<config>
203+
<!-- ... -->
204+
<access-control>
205+
<rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY"
206+
ips="127.0.0.1, ::1" />
207+
<rule path="^/esi" role="ROLE_NO_ACCESS" />
208+
</access-control>
209+
</config>
210+
</srv:container>
211+
212+
.. code-block:: php
213+
214+
// app/config/security.php
215+
$container->loadFromExtension('security', array(
216+
// ...
217+
'access_control' => array(
218+
array(
219+
'path' => '^/esi',
220+
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
221+
'ips' => '127.0.0.1, ::1'
222+
),
223+
array(
224+
'path' => '^/esi',
225+
'role' => 'ROLE_NO_ACCESS'
226+
),
227+
),
228+
));
229+
230+
Here is how it works when the path is ``/esi/something`` coming from the
231+
``10.0.0.1`` IP:
232+
233+
* The first access control rule is ignored as the ``path`` matches but the
234+
``ip`` does not match either of the IPs listed;
235+
236+
* The second access control rule is enabled (the only restriction being the
237+
``path`` and it matches): as the user cannot have the ``ROLE_NO_ACCESS``
238+
role as it's not defined, access is denied (the ``ROLE_NO_ACCESS`` role can
239+
be anything that does not match an existing role, it just serves as a trick
240+
to always deny access).
241+
242+
Now, if the same request comes from ``127.0.0.1`` or ``::1`` (the IPv6 loopback
243+
address):
244+
245+
* Now, the first access control rule is enabled as both the ``path`` and the
246+
``ip`` match: access is allowed as the user always has the
247+
``IS_AUTHENTICATED_ANONYMOUSLY`` role.
248+
249+
* The second access rule is not examined as the first rule matched.
250+
251+
.. _book-security-securing-channel:
252+
253+
Forcing a Channel (http, https)
254+
-------------------------------
255+
256+
You can also require a user to access a URL via SSL; just use the
257+
``requires_channel`` argument in any ``access_control`` entries. If this
258+
``access_control`` is matched and the request is using the ``http`` channel,
259+
the user will be redirected to ``https``:
260+
261+
.. configuration-block::
262+
263+
.. code-block:: yaml
264+
265+
# app/config/security.yml
266+
security:
267+
# ...
268+
access_control:
269+
- { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
270+
271+
.. code-block:: xml
272+
273+
<!-- app/config/security.xml -->
274+
<?xml version="1.0" encoding="UTF-8"?>
275+
<srv:container xmlns="http://symfony.com/schema/dic/security"
276+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
277+
xmlns:srv="http://symfony.com/schema/dic/services"
278+
xsi:schemaLocation="http://symfony.com/schema/dic/services
279+
http://symfony.com/schema/dic/services/services-1.0.xsd">
280+
281+
<access-control>
282+
<rule path="^/cart/checkout"
283+
role="IS_AUTHENTICATED_ANONYMOUSLY"
284+
requires-channel="https" />
285+
</access-control>
286+
</srv:container>
287+
288+
.. code-block:: php
289+
290+
// app/config/security.php
291+
$container->loadFromExtension('security', array(
292+
'access_control' => array(
293+
array(
294+
'path' => '^/cart/checkout',
295+
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
296+
'requires_channel' => 'https',
297+
),
298+
),
299+
));

0 commit comments

Comments
 (0)