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

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.

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.

Request history

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.

Password retrieval

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.

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.

Access to phpinfo
Phpinfo with secret disclosure

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!

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.

Access to a 404 request
Route lists

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

UserController source code

By abusing this behaviour, it is possible to recover the entire source code of the application.

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:

profiler 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:

Dev environment error

Conversely, in production it will take this form:

Production error

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:

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:

With the profiler activated
Without the profiler

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.

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:

Administrators only 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/ :

Security.yaml Configuration
Access is restricted

Using php attributes, it is possible to set the rules at controller/route level:

Adding the IsGranted attribute to the controller

Directly in the Controller code:

Using denyAccessUnlessGranted
A non-admin cannot access the page

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.

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:

notAdmin sees the invoice for the user admin

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.

Adding the IsGranted 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.

Voter code

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 returns true; if not, false.

With this vote in place, a user such as notadmin will no longer be able to access resources belonging to admin.

notAdmin can no longer access the resource

Conversely, the admin user can always access his own resources, which is the expected behaviour.

Admin can still see his invoice

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.

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.

Twig view displaying the text variable

When we access the page, we can see that the text is encoded correctly:

The text is encoded correctly

Body HTML code:

The chevrons are encoded correctly

Twig offers a tag for not encoding by default via the raw filter. This makes the following code vulnerable:

The following template is vulnerable to XSS

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.

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:

Only image src attributes are authorised

We will then have the following Twig file:

Using our sanitizer on Twig

This will return only the following html code:

The malicious code has been removed

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.

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:

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.

Homepage with 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.
Reading the /etc/passwd file
  • {%block U%}cat /etc/passwd{%endblock%}{%set x=block(_charset|first)|split(000)%}{{[x|first]|map(x|last)|join}} allows command execution.
Executing a command to read the /etc/passwd file

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.

Implementation of a Twig sandbox
Message.html.twig file

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:

Reset email with an illegitimate domain

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:

Malicious form

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:

The anti-CSRF token is no longer present on all forms

Or directly on a specific form:

The TaskType form is no longer protected against CSRF attacks

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.

My application is vulnerable to CVE-2024-50342

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