-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Completely re-reading the security book #4606
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d9a9310
Completely re-reading the security book
weaverryan 95d6a7d
[#4606] Updating thanks to comments from everyone!
weaverryan 614da15
Changing to _ for consistency
weaverryan aedfcd2
[#4606] Tweaks thanks entirely to stof
weaverryan fe9fdac
[#4606] Getting my XML (and PHP) on in the new security chapter
weaverryan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ Security | |
firewall | ||
authentication | ||
authorization | ||
secure_tools |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
Securely Comparing Strings and Generating Random Numbers | ||
======================================================== | ||
|
||
The Symfony Security component comes with a collection of nice utilities | ||
related to security. These utilities are used by Symfony, but you should | ||
also use them if you want to solve the problem they address. | ||
|
||
Comparing Strings | ||
~~~~~~~~~~~~~~~~~ | ||
|
||
The time it takes to compare two strings depends on their differences. This | ||
can be used by an attacker when the two strings represent a password for | ||
instance; it is known as a `Timing attack`_. | ||
|
||
Internally, when comparing two passwords, Symfony uses a constant-time | ||
algorithm; you can use the same strategy in your own code thanks to the | ||
:class:`Symfony\\Component\\Security\\Core\\Util\\StringUtils` class:: | ||
|
||
use Symfony\Component\Security\Core\Util\StringUtils; | ||
|
||
// is some known string (e.g. password) equal to some user input? | ||
$bool = StringUtils::equals($knownString, $userInput); | ||
|
||
.. caution:: | ||
|
||
To avoid timing attacks, the known string must be the first argument | ||
and the user-entered string the second. | ||
|
||
Generating a Secure random Number | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Whenever you need to generate a secure random number, you are highly | ||
encouraged to use the Symfony | ||
:class:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom` class:: | ||
|
||
use Symfony\Component\Security\Core\Util\SecureRandom; | ||
|
||
$generator = new SecureRandom(); | ||
$random = $generator->nextBytes(10); | ||
|
||
The | ||
:method:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom::nextBytes` | ||
method returns a random string composed of the number of characters passed as | ||
an argument (10 in the above example). | ||
|
||
The SecureRandom class works better when OpenSSL is installed. But when it's | ||
not available, it falls back to an internal algorithm, which needs a seed file | ||
to work correctly. Just pass a file name to enable it:: | ||
|
||
use Symfony\Component\Security\Core\Util\SecureRandom; | ||
|
||
$generator = new SecureRandom('/some/path/to/store/the/seed.txt'); | ||
$random = $generator->nextBytes(10); | ||
|
||
.. note:: | ||
|
||
If you're using the Symfony Framework, you can access a secure random | ||
instance directly from the container: its name is ``security.secure_random``. | ||
|
||
.. _`Timing attack`: http://en.wikipedia.org/wiki/Timing_attack |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
How Does the Security access_control Work? | ||
========================================== | ||
|
||
For each incoming request, Symfony checks each ``access_control`` entry | ||
to find *one* that matches the current request. As soon as it finds a matching | ||
``access_control`` entry, it stops - only the **first** matching ``access_control`` | ||
is used to enforce access. | ||
|
||
Each ``access_control`` has several options that configure two different | ||
things: | ||
|
||
#. :ref:`should the incoming request match this access control entry <security-book-access-control-matching-options>` | ||
#. :ref:`once it matches, should some sort of access restriction be enforced <security-book-access-control-enforcement-options>`: | ||
|
||
.. _security-book-access-control-matching-options: | ||
|
||
1. Matching Options | ||
------------------- | ||
|
||
Symfony creates an instance of :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher` | ||
for each ``access_control`` entry, which determines whether or not a given | ||
access control should be used on this request. The following ``access_control`` | ||
options are used for matching: | ||
|
||
* ``path`` | ||
* ``ip`` or ``ips`` | ||
* ``host`` | ||
* ``methods`` | ||
|
||
Take the following ``access_control`` entries as an example: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/security.yml | ||
security: | ||
# ... | ||
access_control: | ||
- { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 } | ||
- { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ } | ||
- { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] } | ||
- { path: ^/admin, roles: ROLE_USER } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/config/security.xml --> | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<srv:container xmlns="http://symfony.com/schema/dic/security" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns:srv="http://symfony.com/schema/dic/services" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
|
||
<config> | ||
<!-- ... --> | ||
<access-control> | ||
<rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" /> | ||
<rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" /> | ||
<rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" /> | ||
<rule path="^/admin" role="ROLE_USER" /> | ||
</access-control> | ||
</config> | ||
</srv:container> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/security.php | ||
$container->loadFromExtension('security', array( | ||
// ... | ||
'access_control' => array( | ||
array( | ||
'path' => '^/admin', | ||
'role' => 'ROLE_USER_IP', | ||
'ip' => '127.0.0.1', | ||
), | ||
array( | ||
'path' => '^/admin', | ||
'role' => 'ROLE_USER_HOST', | ||
'host' => 'symfony\.com$', | ||
), | ||
array( | ||
'path' => '^/admin', | ||
'role' => 'ROLE_USER_METHOD', | ||
'method' => 'POST, PUT', | ||
), | ||
array( | ||
'path' => '^/admin', | ||
'role' => 'ROLE_USER', | ||
), | ||
), | ||
)); | ||
|
||
For each incoming request, Symfony will decide which ``access_control`` | ||
to use based on the URI, the client's IP address, the incoming host name, | ||
and the request method. Remember, the first rule that matches is used, and | ||
if ``ip``, ``host`` or ``method`` are not specified for an entry, that ``access_control`` | ||
will match any ``ip``, ``host`` or ``method``: | ||
|
||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| URI | IP | HOST | METHOD | ``access_control`` | Why? | | ||
+=================+=============+=============+============+================================+=============================================================+ | ||
| ``/admin/user`` | 127.0.0.1 | example.com | GET | rule #1 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/admin/user`` | 127.0.0.1 | symfony.com | GET | rule #1 (``ROLE_USER_IP``) | The ``path`` and ``ip`` still match. This would also match | | ||
| | | | | | the ``ROLE_USER_HOST`` entry, but *only* the **first** | | ||
| | | | | | ``access_control`` match is used. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/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 | | ||
| | | | | | rule (which matches) is used. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/admin/user`` | 168.0.0.1 | symfony.com | POST | rule #2 (``ROLE_USER_HOST``) | The second rule still matches. This would also match the | | ||
| | | | | | third rule (``ROLE_USER_METHOD``), but only the **first** | | ||
| | | | | | matched ``access_control`` is used. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/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, | | ||
| | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_USER``) | The ``ip``, ``host`` and ``method`` prevent the first | | ||
| | | | | | three entries from matching. But since the URI matches the | | ||
| | | | | | ``path`` pattern of the ``ROLE_USER`` entry, it is used. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
| ``/foo`` | 127.0.0.1 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its | | ||
| | | | | | URI doesn't match any of the ``path`` values. | | ||
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ||
|
||
.. _security-book-access-control-enforcement-options: | ||
|
||
2. Access Enforcement | ||
--------------------- | ||
|
||
Once Symfony has decided which ``access_control`` entry matches (if any), | ||
it then *enforces* access restrictions based on the ``roles`` and ``requires_channel`` | ||
options: | ||
|
||
* ``role`` If the user does not have the given role(s), then access is denied | ||
(internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` | ||
is thrown); | ||
|
||
* ``requires_channel`` If the incoming request's channel (e.g. ``http``) | ||
does not match this value (e.g. ``https``), the user will be redirected | ||
(e.g. redirected from ``http`` to ``https``, or vice versa). | ||
|
||
.. tip:: | ||
|
||
If access is denied, the system will try to authenticate the user if not | ||
already (e.g. redirect the user to the login page). If the user is already | ||
logged in, the 403 "access denied" error page will be shown. See | ||
:doc:`/cookbook/controller/error_pages` for more information. | ||
|
||
.. _book-security-securing-ip: | ||
|
||
Matching access_control By IP | ||
----------------------------- | ||
|
||
Certain situations may arise when you need to have an ``access_control`` | ||
entry that *only* matches requests coming from some IP address or range. | ||
For example, this *could* be used to deny access to a URL pattern to all | ||
requests *except* those from a trusted, internal server. | ||
|
||
.. caution:: | ||
|
||
As you'll read in the explanation below the example, the ``ips`` option | ||
does not restrict to a specific IP address. Instead, using the ``ips`` | ||
key means that the ``access_control`` entry will only match this IP address, | ||
and users accessing it from a different IP address will continue down | ||
the ``access_control`` list. | ||
|
||
Here is an example of how you configure some example ``/internal*`` URL | ||
pattern so that it is only accessible by requests from the local server itself: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/security.yml | ||
security: | ||
# ... | ||
access_control: | ||
# | ||
- { path: ^/internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } | ||
- { path: ^/internal, roles: ROLE_NO_ACCESS } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/config/security.xml --> | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<srv:container xmlns="http://symfony.com/schema/dic/security" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns:srv="http://symfony.com/schema/dic/services" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
|
||
<config> | ||
<!-- ... --> | ||
<access-control> | ||
<rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY" | ||
ips="127.0.0.1, ::1" /> | ||
<rule path="^/esi" role="ROLE_NO_ACCESS" /> | ||
</access-control> | ||
</config> | ||
</srv:container> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/security.php | ||
$container->loadFromExtension('security', array( | ||
// ... | ||
'access_control' => array( | ||
array( | ||
'path' => '^/esi', | ||
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', | ||
'ips' => '127.0.0.1, ::1' | ||
), | ||
array( | ||
'path' => '^/esi', | ||
'role' => 'ROLE_NO_ACCESS' | ||
), | ||
), | ||
)); | ||
|
||
Here is how it works when the path is ``/internal/something`` coming from | ||
the external IP address ``10.0.0.1``: | ||
|
||
* The first access control rule is ignored as the ``path`` matches but the | ||
IP address does not match either of the IPs listed; | ||
|
||
* The second access control rule is enabled (the only restriction being the | ||
``path``) and so it matches. If you make sure that no users ever have | ||
``ROLE_NO_ACCESS``, then access is denied (``ROLE_NO_ACCESS`` can be anything | ||
that does not match an existing role, it just serves as a trick to always | ||
deny access). | ||
|
||
But if the same request comes from ``127.0.0.1`` or ``::1`` (the IPv6 loopback | ||
address): | ||
|
||
* Now, the first access control rule is enabled as both the ``path`` and the | ||
``ip`` match: access is allowed as the user always has the | ||
``IS_AUTHENTICATED_ANONYMOUSLY`` role. | ||
|
||
* The second access rule is not examined as the first rule matched. | ||
|
||
.. _book-security-securing-channel: | ||
|
||
Forcing a Channel (http, https) | ||
------------------------------- | ||
|
||
You can also require a user to access a URL via SSL; just use the | ||
``requires_channel`` argument in any ``access_control`` entries. If this | ||
``access_control`` is matched and the request is using the ``http`` channel, | ||
the user will be redirected to ``https``: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/security.yml | ||
security: | ||
# ... | ||
access_control: | ||
- { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/config/security.xml --> | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<srv:container xmlns="http://symfony.com/schema/dic/security" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns:srv="http://symfony.com/schema/dic/services" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
|
||
<access-control> | ||
<rule path="^/cart/checkout" | ||
role="IS_AUTHENTICATED_ANONYMOUSLY" | ||
requires-channel="https" /> | ||
</access-control> | ||
</srv:container> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/security.php | ||
$container->loadFromExtension('security', array( | ||
'access_control' => array( | ||
array( | ||
'path' => '^/cart/checkout', | ||
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', | ||
'requires_channel' => 'https', | ||
), | ||
), | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about "HTTP" and "HTTPS" (all letters capitalised)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While maybe more correct, I think it maybe only takes away from understanding that we're talking about literally forcing the
https://
part in the URL.