Introduction
A number of PHP frameworks have emerged over the last few years, most notably Symfony, launched in 2005. This framework is now in version 7 and has evolved considerably since its inception.
Symfony incorporates a number of security measures to protect applications on different fronts. Audits show that applications developed with Symfony generally have fewer vulnerabilities than those created using native PHP.
In this article, we will explore the security mechanisms offered by Symfony. We will also look at how a developer can deliberately or inadvertently bypass these protections, which can lead to vulnerabilities. Finally, we’ll discuss the potential vulnerabilities arising from misuse of certain components of the framework.
To do this, we will use Symfony 6.4, the latest LTS version at the time of writing.
Comprehensive Guide to Symfony Security Best Practices
- Exploiting the Symfony Profiler
- Securing Access Control on Symfony Applications
- Protecting Against Cross-Site Scripting (XSS) Vulnerabilities
- Countering SSTI (Server-Side Template Injection) Vulnerabilities
- Preventing Host Header Poisoning Attacks
- Implement Rate Limiting
- Countering CSRF Attacks
- Monitor Vulnerable Components in your Symfony Applications with Composer
- Use Doctrine to Prevent SQL Injections
- Conclusion
Exploiting the Symfony Profiler
Although rarely observed in production, it is unfortunately common to find Symfony development environments with the profiler activated during a reconnaissance phase.
This exposure represents a critical vulnerability. The profiler provides valuable information about the application, helping an attacker to better understand the code and architecture. In some cases, this can even lead to the compromise of the application server or database.
The Symfony profiler is a very useful development tool, but it should never be made publicly available. The official Symfony documentation clearly states this:
‘Never enable the profiler in production environments as it will lead to major security vulnerabilities in your project’.
This is because the profiler exposes the application to various exploits, some of which will be examined to illustrate the seriousness of this vulnerability. Note that the EOS (Enemies Of Symfony) tool can be used to automate these exploits.
Recovering passwords
When the profiler is accessible, one of the first risks is the possibility of recovering sensitive credentials.
The Symfony profiler can be used to display platform requests via a specific URL: https://yourdomain/_profiler/empty/search/results?limit=10
.
All that is then needed is to locate a connection request, identifiable by an HTTP 302 status and a URL of the /login
type.
This allows access to the data transmitted via the form, exposing sensitive information such as user IDs.
In this example, we see that the administrator uses the password ‘UnMotDePasseSuperCompliqué’.
If this password is reused in a production environment, an attacker could exploit this data to obtain administrator access, enabling immediate privilege escalation.
Retrieving secrets
With the profiler, you can retrieve the application’s secrets. For example, this can be done using the toolbar by accessing the ‘View phpinfo()’ option, or directly at the following URL: https://yourdomain/_profiler/phpinfo
.
By exploiting the phpinfo page, an attacker can access sensitive information, such as the Symfony secret. If this secret is compromised, it can manipulate critical security mechanisms, such as the generation of CSRF tokens or the validation of signatures, considerably increasing the risks for the application.
Another exploitation linked to the Symfony secret concerns fragments, if this feature is enabled. With this secret, an attacker can forge specific requests to manipulate fragments.
This can even lead to serious attacks, such as executing commands on the server.
What’s more, if your application’s secret is compromised, you must regenerate it immediately!
Accessing the Symfony application route list
By exploiting the profiler on a request returning a 404 error, an attacker can access all the routes of the Symfony application.
This information provides a detailed map of the application.
Reading files
By exploiting the URL /_profiler/open
with a valid file path and a specified start line, an attacker can access the contents of the project files. For example, the URL below can be used to read the source code of the UserController.
This flaw represents a critical threat, as it discloses sensitive information about the application’s business logic and architecture, facilitating more complex attacks.
https://yourdomain/_profiler/open?file=src/Controller/UserController.php&line=1
By abusing this behaviour, it is possible to recover the entire source code of the application.
How to secure Symfony development mode and profiler?
It’s easy enough to check whether an application is vulnerable. The presence of the Symfony toolbar may indicate that the platform is running in development mode. However, this indicator is not always reliable.
Deactivate the toolbar and the profiler
It is possible to deactivate the toolbar while keeping the profiler active, as shown in the following configuration:
Here, although the toolbar is not visible, the profiler remains accessible, which can leave the application vulnerable if no other security measures are implemented to limit access.
It is still possible to access the profiler directly via the following URL:
https:///_profiler/empty/search/results?limit=10
Dev mode also has fairly specific errors. For example, a 404 page will look like this:
Conversely, in production it will take this form:
Note: This is how it works by default, but it may change if the 404 page has been customised.
It is also possible to attempt to access the page directly: https://localhost:8000/_profiler/empty/search/results?limit=10
However, it is possible to modify the profiler URL via the config/routes/web_profiler.yaml file
:
The most reliable way of checking whether development mode is enabled is to analyse the .env
file or the APP_ENV
environment variable. This variable clearly indicates whether the application is configured in dev or prod mode.
Examine the app routes to check profiler access
It is also advisable to examine the application’s routes to check whether access to the profiler is authorised. The availability of the profiler in an environment where it should not be active increases the risks.
You can display the list of routes with the following command:
Note: It may be tempting to rely on a strategy of protection by obscurity, for example by using hard-to-guess URLs for the profiler and disabling the toolbar. However, this approach is strongly discouraged, as it relies on fragile security that can be circumvented.
If it is necessary to expose a development environment, it is advisable to restrict it to authorised IP addresses, using strict filtering rules to considerably reduce the risk of unauthorised access.
Securing Access Control on Symfony Applications
The absence or broken access control is one of the most common vulnerabilities in web application penetration testing.
Although this largely depends on the application’s business logic, Symfony offers tools for implementing these controls.
These features enable developers to precisely define and verify user roles, thereby reducing the risk of unauthorised access to sensitive resources.
Restrict access according to user roles
A common problem with access control is restricting access to certain functions or pages according to role. For example, a page reserved for administration should only be accessible to users with the administrator role.
A user such as [email protected] should not be able to access the following page:
Several methods can be used to protect this page.
One solution is to configure the config/packages/security.yaml
file. By defining the following configuration, you can ensure that only people with the admin role can access all pages starting with /admin/
:
Using php attributes, it is possible to set the rules at controller/route level:
Directly in the Controller code:
Symfony’s role management is both comprehensive and flexible. For example, it is possible to define a hierarchy of roles. A higher-level role, such as SUPER_ADMIN, can automatically inherit the permissions of a lower-level role, such as ADMIN.
This hierarchy can be easily configured via the configuration file, as shown in the example below:
Symfony also lets you check access rights, whether in services or directly in Twig templates.
To learn more about security management in Symfony, we recommend that you consult the official documentation.
Limit access to resources and prevent IDOR (Insecure Direct Object Reference) vulnerabilities
In the previous section, we mainly talked about page access controls. However, it is just as important to manage access rights to specific resources within these pages to prevent IDOR vulnerabilities.
For example, in an invoice management application, a user must be able to view his own invoices, but not those of others. So, even if a user has the right to access the /invoice/{id}
page, this access must be conditional on an additional check to ensure that the user is indeed the owner of the invoice linked to this ID.
Use symfony voters to manage access to resources
Symfony offers tools such as voters to manage this type of logic effectively.
Here, for example, notAdmin is able to access the admin’s invoice:
To reinforce rights control and ensure that a user only accesses the resources they are authorised to, it is possible to add a constraint directly to the route using an attribute.
In this example, the #[IsGranted]
attribute uses two arguments:
invoice-view
: represents the action or permission to be checked, in this case the permission to view an invoice.invoice
: designates the subject on which the verification is applied, in this case an instance of the Invoice entity.
A corresponding voter will then be defined in the src/Security/Voter/InvoiceVoter
file to manage this logic.
In this simplified example, the voter is designed solely to manage authorisation to view invoices. Although limited to this use, it provides a good illustration of how a Symfony voter works in general.
The voter is based on two main methods:
supports()
: This method checks whether the voter should intervene for a given request. Here, the voter only applies if the attribute is invoice-view and the resource concerned is an invoice.voteOnAttribute()
: This method contains the logic for determining whether the user is authorised to access the resource. For example, it checks whether the user is the owner of the invoice. If so, it returnstrue
; if not,false
.
With this vote in place, a user such as notadmin will no longer be able to access resources belonging to admin.
Conversely, the admin user can always access his own resources, which is the expected behaviour.
Note that you can build the base of a voter via the symfony/maker-bundle with the following command: php bin/console make:Voter
.
Analyse voter results using the profiler
It is also possible to see the results of voters on the _profiler:
The strategy used here is defined as affirmative. This means that a single voter must return a positive response (true) to grant access to the resource. This approach is ideal when several rules can be applied, but a single positive validation is enough to authorise access.
This is how Symfony works by default. However, you can modify this strategy and choose another, depending on your needs:
consensus
: Access is granted if there are more voters authorising it than refusing it.unanimous
: Access is granted only if no one votes against it.
It is also possible to implement a customised strategy to meet specific rights management needs.
With these bases, you can effectively secure access to your application.
Protecting Against Cross-Site Scripting (XSS) Vulnerabilities
XSS are one of the most common vulnerabilities observed during audits.
Use the Twig template engine to automatically encode data
One of the great advantages of Symfony is the use of the Twig template engine, which makes views secure by design.
By default, Twig automatically encodes variables inserted into templates, reducing the risk of XSS injections.
Note: In this section, the value of the text variable will be <img src=x onerror=alert(1) /
>.
For example, the following view is not vulnerable to XSS thanks to Twig’s automatic encoding management.
When we access the page, we can see that the text is encoded correctly:
Body HTML code:
Do not use the raw filter, which removes the default encoding
Twig offers a tag for not encoding by default via the raw filter. This makes the following code vulnerable:
We therefore strongly advise against using the raw filter in your applications, to prevent the risk of XSS injection. If it is necessary, it is crucial to ensure that the data is secure.
Countering XSS using rich text
One of the most common cases of XSS attacks involves the use of rich text.
When an application allows users to insert elements such as images, bold text or links, it may be tempted to use the |raw filter to display this data in the desired format.
However, this approach opens the door to attackers. A malicious user could inject JavaScript code into this enriched data, which would then be executed by the browser, compromising the security of the site.
If you use Twig to generate your views, we recommend that you use Symfony’s HTML Sanitizer.
For example, if you only want to allow the src attribute in tags, you can apply the following configuration:
We will then have the following Twig file:
This will return only the following html code:
It is crucial to note that the security of your application depends largely on the configuration of the sanitizer. We therefore recommend that you limit this configuration to elements and attributes that are strictly necessary for the application to function properly.
Note: The Symfony sanitizer is not exclusive to Symfony, so you can use it in other PHP projects.
Furthermore, if your application manages client-side rendering, you can also use DomPurify to reinforce security.
Countering SSTI (Server-Side Template Injection) Vulnerabilities
There are significant advantages to using Twig, including built-in protection against XSS attacks.
However, in some cases, Twig can be abused to allow users to create or modify dynamic templates. This practice can introduce Server-Side Template Injection (SSTI) vulnerabilities, enabling an attacker to inject and execute malicious code directly on the server.
Example of an SSTI exploitation
Take, for example, the case where the application allows a manager to define a personalised message on the main page.
Here is an example of a vulnerable Twig view:
This vulnerability manifests itself when a Twig rendering is performed from an arbitrary input provided by a user.
In our case, the manager submits a template via a form, which is then stored in the database. This message is then retrieved and Twig rendered on it.
This type of vulnerable code is frequently encountered when looking for examples of how to render a string from a string or a database.
The idea here is, for example, to allow the manager to define a message like: Hello {{ app.user.email }}
In this way, a user accessing the page will see a personalised welcome message.
A malicious user with access to this functionality can exploit it to execute arbitrary code on the server.
For example, by injecting specific instructions into the template, the attacker can hijack the operation of the application and compromise its security.
Here are some examples of messages that could be used to exploit this vulnerability:
{{‘/etc/passwd’|file_excerpt(1,30) }}
: Allows arbitrary files to be read, such as the/etc/passwd
file on a Unix server, potentially exposing sensitive information.
{%block U%}cat /etc/passwd{%endblock%}{%set x=block(_charset|first)|split(000)%}{{[x|first]|map(x|last)|join}}
allows command execution.
How to protect yourself?
Prohibit template customisation
The most radical solution, but also the most effective, is to prohibit a user from directly entering templates interpreted by Twig.
If this functionality is essential, it is advisable to set up a controlled replacement system. For example, you can use regular expressions (regex) to identify and replace certain contents with their values before rendering.
This approach limits the possibility of arbitrary code execution while retaining a certain amount of flexibility for the user.
Implement a sandbox with Twig
Another solution is to set up a sandbox with Twig. For example, we can modify the controller to apply a strict security policy.
Implementing a sandbox can be an interesting solution, as it limits the actions that can be executed in the template. However, misconfiguring the sandbox can leave room for bypasses that can be exploited by an attacker.
It is also crucial to control the methods and properties accessible via the sandbox. For example, if the application allows a user’s hash field to be read, this could enable an attacker to recover the hash of their password.
Particular attention must therefore be paid to the configuration of the sandbox and the authorised objects.
Use a logic-less template engine
The final solution is to use a logic-less template engine such as Mustache, which separates visual rendering from code interpretation as far as possible.
In Mustache templates, there are no explicit mechanisms for managing the flow of control, because all control is determined by the data transmitted. In addition, it is impossible to integrate business or application logic directly into the templates.
This minimalist design considerably limits the possibilities for exploitation, particularly the most critical attacks, such as Remote Code Execution (RCE). As a result, the overall risk of attack is significantly reduced.
Preventing Host Header Poisoning Attacks
Symfony applications are often vulnerable to Host Header Poisoning attacks.
This vulnerability can manifest itself in features such as password reset.
Let’s take the example of an application where the host is set to localhost. The application generates a password reset link based on the Host header.
An attacker could exploit this behaviour by sending a reset link to the administrator, redirecting them to a domain they control.
POST /reset-password HTTP/2
Host: evil-localhost:8000
Cookie: PHPSESSID=5setp64tm5gjqbc0l9pkmgcsn1; main_auth_profile_token=734949
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: https://localhost:8000/reset-password
Content-Type: application/x-www-form-urlencoded
Content-Length: 206
Te: trailers
reset_password_request_form%5Bemail%5D=admin%40local.dev&reset_password_request_form%5B_token%5D=677a3.RDMh-XGfZIRgCx2wovHAWGa3UDxpn7ZS5yTstAj-Rac.CgVSngfZDfQIYEvl-MiZMgL_KXAk7cIjvxe72V2WP_AOYXHIOe0PwhFKTA
The admin will receive the following link:
If the administrator clicks on the link, the attacker will be able to recover the reset token. With this token, the attacker will be able to request a password change on the legitimate application, which will lead to the theft of the administrator’s account.
To correct this attack at application level, the trusted_hosts field can be added to the framework.yaml
file.
Implement Rate Limiting
The lack of rate limiting, although often considered less critical, can nevertheless be exploited in many applications that do not limit the number of requests a user can make over a given period.
A typical exploitation consists of submitting a large number of username and password combinations on a login page in order to discover valid accesses.
Symfony offers a rate-limiter that lets you define and configure limits on the number of requests a user can send in a given period of time:
composer require symfony/rate-limiter
Next, simply modify the security.yaml
file to enable rate limiting. By setting login_throttling: null
, the default configuration will be activated. However, you can customise the settings further or even create your own rate limiter to suit your needs.
The default configuration authorises up to 5 attempts per minute per user from the same IP address, which limits the risk of bruteforce attacks targeted at a specific account. In addition, an IP address is limited to 25 attempts per minute, to deter brute force attacks on multiple accounts simultaneously.
If you wish to customise the settings, you can consult the documentation to explore the various options.
Countering CSRF Attacks
As a reminder, a CSRF (Cross-Site Request Forgery) attack consists of inciting an authenticated user to perform an action on an application without their knowledge.
Take, for example, a user creation form, which is only accessible to administrators. The attacker’s objective would be to direct the administrator to a site they control, where a malicious form would be designed to submit this form without the administrator’s knowledge. This would allow the attacker to exploit the administrator’s privileges to create a malicious user.
Here is an example of the form of a malicious form that the attacker could trick the administrator into executing:
Natively, Symfony is protected against this type of attack, as shown by the following response after submitting the form:
This is made possible by the fact that Symfony forms add a _token
parameter by default. This token acts as an effective protection against CSRF attacks by ensuring that only requests from the legitimate application are accepted.
It is possible to deactivate the token globally:
Or directly on a specific form:
Disabling the CSRF token is not good security practice. This mechanism is an essential barrier against CSRF attacks. If you are considering disabling it, it is essential to assess the implications carefully and ensure that adequate compensatory measures are put in place.
Monitor Vulnerable Components in your Symfony Applications with Composer
Symfony uses Composer to manage its packages, which simplifies installation and dependency management. However, vulnerabilities are regularly discovered in certain packages, which can compromise the security of your applications.
It is therefore crucial to put in place a policy of regular updates to guarantee the security of your dependencies. A major advantage of Composer is its built-in command, which can detect vulnerabilities in the packages used by your application.
Like Composer, npm offers a practical command for managing vulnerabilities in JavaScript dependencies. The npm audit
command allows you to list known vulnerabilities in your project, indicate their severity and suggest patches or actions to be taken to resolve them.
Use Doctrine to Prevent SQL Injections
Most Symfony projects use Doctrine to interact with the database. Doctrine prefers to use abstract and secure methods rather than writing SQL queries directly.
As a result, SQL injections are rare in projects using Doctrine, as developers generally pass parameters via dedicated methods, allowing Doctrine to automatically generate prepared statements.
Conclusion
In this article, we have covered the common vulnerabilities identified during a security audit. Symfony offers effective protection against many vulnerabilities by default. Where protection is not built-in, the framework provides tools for implementing it.
In terms of PHP security, we strongly recommend the use of proven frameworks such as Symfony. Using such frameworks significantly improves security compared with applications developed without a framework, thanks to the built-in mechanisms and best practices they impose.
Author: Thomas DELFINO – Pentester @Vaadata