Skip to content

Symfony Cheat Sheet

Introduction

This cheat sheet aims to provide developers with security tips when building applications using the Symfony framework. It covers common vulnerabilities and best practices to ensure that your Symfony applications are secure.

While Symfony comes with built-in security mechanisms, developers must be aware of potential vulnerabilities and best practices to ensure the applications they build are secure. This guide aims to cover common security issues, emphasizing the importance of understanding Symfony's security features and how to utilize them effectively. Whether you're a newcomer to Symfony or an experienced developer looking to reinforce your security practices, this document serves as a valuable resource. By following the guidelines outlined here, you can strengthen the security of your Symfony applications and create a safer digital environment for users and data.

Main Sections

Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS) is a type of attack where malicious JavaScript code is injected into a displayed variable. For example, if the value of the variable name is <script>alert('hello')</script>, and we display it in HTML like this: Hello {{name}}, the injected script will be executed when the HTML is rendered.

Symfony comes by default with twig templates that automatically protect applications from XSS attacks by using output escaping to transform variables containing special characters by wrapping variable with {{ }} statement.

<p>Hello {{name}}</p>
{# if 'name' is '<script>alert('hello!')</script>', Twig will output this:
'<p>Hello &lt;script&gt;alert(&#39;hello!&#39;)&lt;/script&gt;</p>' #}

If you are rendering a variable that is trusted and contains HTML contents, you can use Twig raw filter to disable output escaping.

<p>{{ product.title|raw }}</p>
{# if 'product.title' is 'Lorem <strong>Ipsum</strong>', Twig will output
exactly that instead of 'Lorem &lt;strong&gt;Ipsum&lt;/strong&gt;' #}

Explore the Twig output escaping documentation to gain insights into disabling output escaping for a specific block or an entire template.

For other information on XSS prevention that is not specific to Symfony, you may refer the Cross Site Scripting Prevention Cheatsheet.

Cross-Site Request Forgery (CSRF)

Symfony Form component automatically includes CSRF tokens in the forms, providing built-in protection against CSRF attacks. Symfony validates these tokens automatically, eliminating the need for manual intervention to safeguard your application.

By default the CSRF token is added as a hidden field called _token, but this can be customized with other settings on a form-by-form basis:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostForm extends AbstractType
{

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            // ... 
            'csrf_protection' => true,  // enable/disable csrf protection for this form
            'csrf_field_name' => '_csrf_token',
            'csrf_token_id'   => 'post_item', // change arbitrary string used to generate
        ]);
    }

}

If you don't use Symfony Forms you can generate and validate CSRF tokens by yourself. To do this you have to install symfony/security-csrf component.

composer install symfony/security-csrf

Enable/disable the CSRF protection in config/packages/framework.yaml file:

framework:
    csrf_protection: ~

Next consider this HTML Twig template when CSRF token is generated by csrf_token() Twig function

<form action="{{ url('delete_post', { id: post.id }) }}" method="post">
    <input type="hidden" name="token" value="{{ csrf_token('delete-post') }}">
    <button type="submit">Delete post</button>
</form>

Then you can get value of CSRF token in controller using isCsrfTokenValid() function:

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController
{

    #[Route('/posts/{id}', methods: ['DELETE'], name: 'delete_post')]
    public function delete(Post $post, Request $request): Response 
    { 
        $token = $request->request->get('token')
        if($this->isCsrfTokenValid($token)) {
            // ...
        }

        // ...
    }
}

You can find more information about CSRF not related to Symfony in Cross-Site Request Forgery (CSRF) Cheat Sheet.

SQL Injection

SQL Injection is a type of security vulnerability that occurs when an attacker is able to manipulate a SQL query in a way that it can execute arbitrary SQL code. This can allow attackers to view, modify, or delete data in the database, potentially leading to unauthorized access or data loss.

Symfony, particularly when used with Doctrine ORM (Object-Relational Mapping), provides protection against SQL injection through prepared statements parameters. Thanks to this it is harder to mistakenly write unprotected queries, however it is still possible. The following example shows insecure DQL usage:

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ExampleController extends AbstractController {

    public function getPost(Request $request, EntityManagerInterface $em): Response
    {
        $id = $request->query->get('id');

        $dql = "SELECT p FROM App\Entity\Post p WHERE p.id = " . $id . ";";
        $query = $em->createQuery($dql);
        $post = $query->getSingleResult();

        // ...
    }
}

Examples bellow shows the correct ways that provides protection against SQL Injection:

  • Using entity repository built-in method
$id = $request->query->get('id');
$post = $em->getRepository(Post::class)->findOneBy(['id' => $id]);
  • Using Doctrine DQL Language
$query = $em->createQuery("SELECT p FROM App\Entity\Post p WHERE p.id = :id");
$query->setParameter('id', $id);
$post = $query->getSingleResult();
  • Using DBAL Query Builder
$qb = $em->createQueryBuilder();
$post = $qb->select('p')
            ->from('posts','p')
            ->where('id = :id')
            ->setParameter('id', $id)
            ->getQuery()
            ->getSingleResult();

For more information about Doctrine you can refer to their documentation. You may also refer the SQL Injection Prevention Cheatsheet for more information that is not specific to neither Symfony nor Doctrine.

Command Injection

Command Injection occurs when malicious code is injected into an application system and executed. For more information refer to Command Injection Defense Cheat Sheet.

Consider the following example, where file is removed using the exec() function without any input escaping:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;

#[AsController]
class ExampleController 
{

    #[Route('/remove_file', methods: ['POST'])]
    public function removeFile(Request $request): Response
    {
        $filename =  $request->request->get('filename');
        exec(sprintf('rm %s', $filename));

        // ...
    }
}

In the above code, there is no any validation of user's input. Imagine what could happen if user provides a malicious value like test.txt && rm -rf . . To mitigate this risk, it is advisable to use native PHP functions like in this case unlink() or Symfony Filesystem Component remove() method instead of exec().

For specific PHP filesystem functions relevant to your case, you can refer to the PHP documentation or Symfony Filesystem Component documentation.

Open Redirection

Open Redirection is a security flaw that occurs when a web application redirects users to a URL specified in an invalidated parameter. Attackers exploit this vulnerability to redirect users to malicious sites.

In the provided PHP code snippet:

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController 
{

    #[Route('/dynamic_redirect', methods: ['GET'])]
    public function dynamicRedirect(#[MapQueryParameter] string $url): Response 
    {
        return $this->redirect($url);
    }
}

The controller function redirects users based on the url query parameter without proper validation. Attackers can craft malicious URLs, leading unsuspecting users to malicious sites. To prevent open redirection, always validate and sanitize user input before redirection, and avoid using untrusted input directly in redirect functions.

File Upload Vulnerabilities

File upload vulnerabilities are security issues that arise when an application does not properly validate and handle file uploads. It's important to ensure that file uploads are handled securely to prevent various types of attacks. Here are some general guidelines to help mitigate this issue in Symfony:

Validate file type and size

Always validate the file type on the server side to ensure that only allowed file types are accepted. Also consider limiting the size of uploaded files to prevent denial-of-service attacks and to ensure that your server has enough resources to handle the uploads.

Example with PHP Attributes:

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints\File;

class UploadDto
{
    public function __construct(
        #[File(
            maxSize: '1024k',
            mimeTypes: [
                'application/pdf',
                'application/x-pdf',
            ],
        )]
        public readonly UploadedFile $file,
    ){}
}

Example with Symfony Form:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;

class FileForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'constraints' => [
                    new File([
                        'maxSize' => '1024k', 
                        'mimeTypes' => [
                            'application/pdf',
                            'application/x-pdf',
                        ],
                    ]),
                ],
            ]);
    }
}

Use unique filenames

Ensure that each uploaded file has a unique name to prevent overwriting existing files. You can use a combination of a unique identifier and the original filename to generate a unique name.

Store uploaded files securely

Store uploaded files outside the public directory to prevent direct access. If you use public directory to store them, configure your web server to deny access to the upload directory.

Refer the File Upload Cheatsheet to learn more.

Directory Traversal

A directory or path traversal attack aims to access files and directories that are stored on server by manipulating input data that reference files with “../” dot-dot-slash sequences and its variations or by using absolute file paths. For more details refer to OWASP Path Traversal.

You can protect your application before directory traversal attack by validating whether the absolute path of requested file location is correct or strip out directory information from filename input.

  • Check if path exists using PHP realpath function and that it leads to the storage directory
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController 
{

    #[Route('/download', methods: ['GET'])]
    public function download(#[MapQueryParameter] string $filename): Response 
    {
        $storagePath = $this->getParameter('kernel.project_dir') . '/storage';
        $filePath = $storagePath . '/' . $filename;

        $realBase = realpath($storagePath);
        $realPath = realpath($filePath);

        if ($realPath === false || !str_starts_with($realPath, $realBase))
        {
            //Directory Traversal!
        }

        // ...

    }
}
  • Strip out directory information with PHP basename function
// ...

$storagePath = $this->getParameter('kernel.project_dir') . '/storage';
$filePath = $storagePath . '/' . basename($filename);

// ...

Dependencies vulnerabilities

Dependency vulnerabilities can expose your application to various risks, making it crucial to adopt best practices. Keep all Symfony components and third-party libraries up-to-date.

Composer, the dependency manager for PHP makes it easy to update PHP packages:

composer update

When using multiple dependencies, some of them may contain security vulnerabilities. To address this concern, Symfony comes with Symfony Security Checker. This tool specifically examines the composer.lock file in your project to identify any known security vulnerabilities within the dependencies that have been installed and address any potential security issues in your Symfony project.

To use Security Checker run following command using Symfony CLI:

symfony check:security

You should also consider similar tools:

Cross Origin Resource Sharing

CORS is a security feature implemented in web browsers to control how web applications in one domain can request and interact with resources hosted on another domains.

In Symfony you can manage CORS policies using nemilo/cors-bundle. This bundle lets you control CORS rules precisely without changing your server settings.

To install it with composer, run:

composer require nelmio/cors-bundle

For Symfony Flex users, the installation generates a basic configuration file in the config/packages directory automatically. Take a look at the example configuration for routes starting with /API prefix.

# config/packages/nelimo_cors.yaml
nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['*']
        allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
        allow_headers: ['*']
        expose_headers: ['Link']
        max_age: 3600
    paths:
        '^/api': ~  # ~ means that configurations for this path is inherited from defaults

It's advisable to enhance the security of your Symfony application by adding to your responses essential security headers as:

  • Strict-Transport-Security
  • X-Frame-Options
  • X-Content-Type-Options
  • Content-Security-Policy
  • X-Permitted-Cross-Domain-Policies
  • Referrer-Policy
  • Clear-Site-Data
  • Cross-Origin-Embedder-Policy
  • Cross-Origin-Opener-Policy
  • Cross-Origin-Resource-Policy
  • Cache-Control

To find more details about individual header refer to OWASP secure headers project.

In Symfony you can add those headers either manually or automatically by listening the ResponseEvent to your to every response or configuring web servers like Nginx or Apache.

use Symfony\Component\HttpFoundation\Request;

$response = new Response();
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');

Session & Cookies Management

By default sessions are securely configured and enabled. However, they can be controlled manually in config/packages/framework.yaml under the framework.session key. Make sure to set the following in your session configuration to make your application more aware.

Ensure cookie_secure is not explicitly set to false(it is set to true by default). Setting http only to true means that cookie won't be accessible by JavaScript.

cookie_httponly: true

Make sure to set a short session TTL duration. According to OWASP's recommendations, aim for a session TTL of 2-5 minutes for high-value applications and 15-30 minutes for lower-risk applications.

cookie_lifetime: 5

It is recommended to set cookie_samesite to either lax or strict to prevent cookies being send from cross-origin requests. lax allows the cookie to be sent along with "safe" top-level navigations and same-site requests. With strict it would not be possible to send any cookie when the HTTP request is not from the same domain.

cookie_samesite: lax|strict

Setting cookie_secure to auto assures us that cookies are only sent over secure connections, meaning true for HTTPS and false for HTTP protocol.

cookie_secure: auto

OWASP provides more general information about sessions in Session Management Cheat Sheet. You may also refer the Cookie Security Guide.


In Symfony, sessions are managed by the framework itself and rely on Symfony's session handling mechanisms rather than PHP's default session handling via the session.auto_start = 1 directive in the php.ini file. The session.auto_start = 1 directive in PHP is used to automatically start a session on each request, bypassing explicit calls to session_start(). However, when using Symfony for session management, it's recommended to disable session.auto_start to prevent conflicts and unexpected behavior.

Authentication

Symfony Security provides a robust authentication system that includes providers, firewalls, and access controls to ensure a secure and controlled access environment. Authentication settings can be configured in config/packages/security.yaml.

  • Providers

    Symfony authentication relies on providers to fetch user information from various storages such as databases, LDAP, or custom sources. Providers get users based on the defined propety and load the corresponding user object.

    In below example Entity User Provider is presented which uses Doctrine to fetch user by unique identifier.

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    
  • Firewalls

    Symfony use firewalls to define security configurations for different parts of an application. Each firewall define a specific set of rules and actions for incoming requests. They protect different sections of the application by specifying which routes or URLs are secured, the authentication mechanisms to use, and how to handle unauthorized access. A firewall can be associated with specific patterns, request methods, access controls, and authentication providers.

    firewalls:
        dev: # disable security on routes used in development env
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin: # handle authentication in /admin pattern routes
            lazy: true
            provider: app_user_provider
            pattern: ^/admin
            custom_authenticator: App\Security\AdminAuthenticator
            logout:
                path: app_logout
                target: app_login
        main: # main firewall that include all remaining routes
            lazy: true
            provider: app_user_provider
    
  • Access Control

    Access control determines which users can access specific parts of an application. These rules consist of path patterns and required roles or permissions. Access control rules are configured under access_control key.

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN } # only user with ROLE_ADMIN role is allowed
        - { path: ^/login, roles: PUBLIC_ACCESS } # everyone can access this route
    

Error Handling Disclosure

Symfony has a robust error handling system. By default, Symfony applications are configured to display detailed error messages only in the development environment for security reasons. In the production environment, a generic error page is shown. Symfony's error handling system also allows to customize error pages based on different HTTP status codes, providing a seamless and branded user experience. Additionally, Symfony logs detailed error information, aiding developers in identifying and resolving issues efficiently.

For more information about error handling unrelated to Symfony refer to Error Handling Cheat Sheet.

Sensitive data

In Symfony the best way for storing configurations like API keys, etc., is through the use of environment variable, which are dependent on the application's location. To ensure the security of sensitive values, Symfony provides a secrets management system in which values are additionally encoded using cryptographic keys and stored as secrets.

Consider an example where an API_KEY is stored as secret:

To generate a pair of cryptographic keys you can run the following command. The private key file is highly sensitive and it shouldn't be committed in repository.

bin/console secrets:generate-keys

This command will generate a file for the API_KEY secret in config/secrets/env(dev|prod|etc.)

bin/console secret:set API_KEY

You can access secret values in your code in the same manner as environment variables. It's very important to note that if there are environment variables and secrets with identical names, the values from environment variables will always will override secrets.

For more details refer to Symfony Secrets Documentation.

Summary

  • Make sure your app is not in debug mode while in production. To turn off debug mode, set your APP_ENV environment variable to prod:

    APP_ENV=prod
    
  • Make sure your PHP configuration is secure. You may refer the PHP Configuration Cheat Sheet for more information on secure PHP configuration settings.

  • Ensure that the SSL certificate is properly configured in your web server and configure it to enforce HTTPS by redirecting HTTP traffic to HTTPS.

  • Implement security headers to enhance the security posture of your application.

  • Ensure that file and directory permissions are set correctly to minimize security risks.

  • Implement regular backups of your production database and critical files. Have a recovery plan in place to quickly restore your application in case of any issue.

  • Use security checkers to scan your dependencies to identify known vulnerabilities.

  • Consider setting up monitoring tools and error reporting mechanisms to quickly identify and address issues in your production environment. Explore tools like Blackfire.io.

References