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:
- should the incoming request match this access control entry
- once it matches, should some sort of access restriction be enforced:
1. Matching Options
Symfony uses ChainRequestMatcher for
each access_control
entry, which determines which implementation of
RequestMatcherInterface should be used
on this request. The following access_control
options are used for matching:
path
: a regular expression (without delimiters)ip
orips
: netmasks are also supported (can be a comma-separated string)port
: an integerhost
: a regular expressionmethods
: one or many HTTP methodsrequest_matcher
: a service implementingRequestMatcherInterface
attributes
: an array, which can be used to specify one or more request attributes that must match exactlyroute
: a route name
Take the following access_control
entries as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# config/packages/security.yaml
parameters:
env(TRUSTED_IPS): '10.0.0.1, 10.0.0.2'
security:
# ...
access_control:
- { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 }
- { 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] }
# ips can be comma-separated, which is especially useful when using env variables
- { path: '^/admin', roles: ROLE_USER_IP, ips: '%env(TRUSTED_IPS)%' }
- { path: '^/admin', roles: ROLE_USER_IP, ips: [127.0.0.1, ::1, '%env(TRUSTED_IPS)%'] }
# for custom matching needs, use a request matcher service
- { roles: ROLE_USER, request_matcher: App\Security\RequestMatcher\MyRequestMatcher }
# require ROLE_ADMIN for 'admin' route. You can use the shortcut "route: "xxx", instead of "attributes": ["_route": "xxx"]
- { attributes: {'_route': 'admin'}, roles: ROLE_ADMIN }
- { route: 'admin', roles: ROLE_ADMIN }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
<!-- config/packages/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
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<srv:parameters>
<srv:parameter key="env(TRUSTED_IPS)">10.0.0.1, 10.0.0.2</srv:parameter>
</srv:parameters>
<config>
<!-- ... -->
<rule path="^/admin" role="ROLE_USER_PORT" ip="127.0.0.1" port="8080"/>
<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" methods="POST, PUT"/>
<!-- ips can be comma-separated, which is especially useful when using env variables -->
<rule path="^/admin" role="ROLE_USER_IP" ip="%env(TRUSTED_IPS)%"/>
<rule path="^/admin" role="ROLE_USER_IP">
<ip>127.0.0.1</ip>
<ip>::1</ip>
<ip>%env(TRUSTED_IPS)%</ip>
</rule>
<!-- for custom matching needs, use a request matcher service -->
<rule role="ROLE_USER" request-matcher="App\Security\RequestMatcher\MyRequestMatcher"/>
<!-- require ROLE_ADMIN for 'admin' route. You can use the shortcut route="xxx" -->
<rule role="ROLE_ADMIN">
<attribute key="_route">admin</attribute>
</rule>
<rule route="admin" role="ROLE_ADMIN"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
// config/packages/security.php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Config\SecurityConfig;
return static function (ContainerBuilder $container, SecurityConfig $security): void {
$container->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2');
// ...
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_PORT'])
->ips(['127.0.0.1'])
->port(8080)
;
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_IP'])
->ips(['127.0.0.1'])
;
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_HOST'])
->host('symfony\.com$')
;
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_METHOD'])
->methods(['POST', 'PUT'])
;
// ips can be comma-separated, which is especially useful when using env variables
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_IP'])
->ips([env('TRUSTED_IPS')])
;
$security->accessControl()
->path('^/admin')
->roles(['ROLE_USER_IP'])
->ips(['127.0.0.1', '::1', env('TRUSTED_IPS')])
;
// for custom matching needs, use a request matcher service
$security->accessControl()
->roles(['ROLE_USER'])
->requestMatcher('App\Security\RequestMatcher\MyRequestMatcher')
;
// require ROLE_ADMIN for 'admin' route. You can use the shortcut route('xxx') method,
// instead of attributes(['_route' => 'xxx']) method
$security->accessControl()
->roles(['ROLE_ADMIN'])
->attributes(['_route' => 'admin'])
;
$security->accessControl()
->roles(['ROLE_ADMIN'])
->route('admin')
;
};
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
, port
, host
or method
are not specified for an entry, that
access_control
will match any ip
, port
, host
or method
.
See the following examples:
- Example #1:
-
- URI
/admin/user
- IP:
127.0.0.1
, Port:80
, Host:example.com
, Method:GET
- Rule applied: rule #2 (
ROLE_USER_IP
) - Why? The URI matches
path
and the IP matchesip
.
- URI
- Example #2:
-
- URI
/admin/user
- IP:
127.0.0.1
, Port:80
, Host:symfony.com
, Method:GET
- Rule applied: rule #2 (
ROLE_USER_IP
) - Why? The
path
andip
still match. This would also match theROLE_USER_HOST
entry, but only the firstaccess_control
match is used.
- URI
- Example #3:
-
- URI
/admin/user
- IP:
127.0.0.1
, Port:8080
, Host:symfony.com
, Method:GET
- Rule applied: rule #1 (
ROLE_USER_PORT
) - Why? The
path
,ip
andport
match.
- URI
- Example #4:
-
- URI
/admin/user
- IP:
168.0.0.1
, Port:80
, Host:symfony.com
, Method:GET
- Rule applied: rule #3 (
ROLE_USER_HOST
) - Why? The
ip
doesn't match neither the first rule nor the second rule. - So the third rule (which matches) is used.
- URI
- Example #5:
-
- URI
/admin/user
- IP:
168.0.0.1
, Port:80
, Host:symfony.com
, Method:POST
- Rule applied: rule #3 (
ROLE_USER_HOST
) - Why? The third rule still matches. This would also match the fourth rule
- (
ROLE_USER_METHOD
), but only the first matchedaccess_control
is used.
- URI
- Example #6:
-
- URI
/admin/user
- IP:
168.0.0.1
, Port:80
, Host:example.com
, Method:POST
- Rule applied: rule #4 (
ROLE_USER_METHOD
) - Why? The
ip
andhost
don't match the first three entries, but - the fourth -
ROLE_USER_METHOD
- matches and is used.
- URI
- Example #7:
-
- URI
/foo
- IP:
127.0.0.1
, Port:80
, Host:symfony.com
, Method:POST
- Rule applied: matches no entries
- Why? This doesn't match any
access_control
rules, since its URI - doesn't match any of the
path
values.
- URI
Caution
Matching the URI is done without $_GET
parameters.
Deny access in PHP code if you want
to disallow access based on $_GET
parameter values.
2. Access Enforcement
Once Symfony has decided which access_control
entry matches (if any),
it then enforces access restrictions based on the roles
, allow_if
and requires_channel
options:
roles
If the user does not have the given role, then access is denied (internally, an AccessDeniedException is thrown).allow_if
If the expression returns false, then access is denied;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 fromhttp
tohttps
, or vice versa).
Tip
Behind the scenes, the array value of roles
is passed as the
$attributes
argument to each voter in the application with the
Request as $subject
. You
can learn how to use your custom attributes by reading
How to Use Voters to Check User Permissions.
Caution
If you define both roles
and allow_if
, and your Access Decision
Strategy is the default one (affirmative
), then the user will be granted
access if there's at least one valid condition. If this behavior doesn't fit
your needs, change the Access Decision Strategy.
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 How to Customize Error Pages for more information.
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:
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
# ...
access_control:
#
# the 'ips' option supports IP addresses and subnet masks
- { path: '^/internal', roles: PUBLIC_ACCESS, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
- { path: '^/internal', roles: ROLE_NO_ACCESS }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<!-- config/packages/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
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<!-- the 'ips' option supports IP addresses and subnet masks -->
<rule path="^/internal" role="PUBLIC_ACCESS">
<ip>127.0.0.1</ip>
<ip>::1</ip>
</rule>
<rule path="^/internal" role="ROLE_NO_ACCESS"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->accessControl()
->path('^/internal')
->roles(['PUBLIC_ACCESS'])
// the 'ips' option supports IP addresses and subnet masks
->ips(['127.0.0.1', '::1'])
;
$security->accessControl()
->path('^/internal')
->roles(['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 haveROLE_NO_ACCESS
, then access is denied (ROLE_NO_ACCESS
can be anything that does not match an existing role, it only 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 theip
match: access is allowed as the user always has thePUBLIC_ACCESS
role. - The second access rule is not examined as the first rule matched.
Securing by an Expression
Once an access_control
entry is matched, you can deny access via the
roles
key or use more complex logic with an expression in the allow_if
key:
1 2 3 4 5 6 7 8 9 10
# config/packages/security.yaml
security:
# ...
access_control:
-
path: ^/_internal/secure
# the 'roles' and 'allow_if' options work like an OR expression, so
# access is granted if the expression is TRUE or the user has ROLE_ADMIN
roles: 'ROLE_ADMIN'
allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<!-- config/packages/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
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<!-- the 'role' and 'allow-if' options work like an OR expression, so
access is granted if the expression is TRUE or the user has ROLE_ADMIN -->
<rule path="^/_internal/secure"
role="ROLE_ADMIN"
allow-if="'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->accessControl()
->path('^/_internal/secure')
// the 'role' and 'allow-if' options work like an OR expression, so
// access is granted if the expression is TRUE or the user has ROLE_ADMIN
->roles(['ROLE_ADMIN'])
->allowIf('"127.0.0.1" == request.getClientIp() or request.headers.has("X-Secure-Access")')
;
};
In this case, when the user tries to access any URL starting with
/_internal/secure
, they will only be granted access if the IP address is
127.0.0.1
or a secure header, or if the user has the ROLE_ADMIN
role.
Note
Internally allow_if
triggers the built-in
ExpressionVoter
as like it was part of the attributes defined in the roles
option.
Inside the expression, you have access to a number of different variables
and functions including request
, which is the Symfony
Request object (see
The HttpFoundation Component).
For a list of the other functions and variables, see functions and variables.
Tip
The allow_if
expressions can also contain custom functions registered
with expression providers.
Restrict to a port
Add the port
option to any access_control
entries to require users to
access those URLs via a specific port. This could be useful for example for
localhost:8080
.
1 2 3 4 5
# config/packages/security.yaml
security:
# ...
access_control:
- { path: ^/cart/checkout, roles: PUBLIC_ACCESS, port: 8080 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!-- config/packages/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
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<rule path="^/cart/checkout"
role="PUBLIC_ACCESS"
port="8080"
/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->accessControl()
->path('^/cart/checkout')
->roles(['PUBLIC_ACCESS'])
->port(8080)
;
};
Forcing a Channel (http, https)
You can also require a user to access a URL via SSL; 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
:
1 2 3 4 5
# config/packages/security.yaml
security:
# ...
access_control:
- { path: ^/cart/checkout, roles: PUBLIC_ACCESS, requires_channel: https }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!-- config/packages/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
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<rule path="^/cart/checkout"
role="PUBLIC_ACCESS"
requires-channel="https"
/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->accessControl()
->path('^/cart/checkout')
->roles(['PUBLIC_ACCESS'])
->requiresChannel('https')
;
};