Ebooks

Symfony CSRF tutorial

Symfony CSRF tutorial shows how to implement CSRF protection in Symfony applications.

Symfony

Symfony is a set of reusable PHP components and a PHP framework for web projects. Symfony was published as free software in 2005. The original author of Symfony is Fabien Potencier. Symfony was inspired by Djanog, Spring, and ROR frameworks.

CSRF

Cross-site request forgery (CSRF) is an attack in which a malicious users attempt to make legitimate users unknowingly submit data that they do not intend to submit. CSRF attacks specifically target state-changing requests, not theft of data. A successful CSRF attack can force the user to perform state changing requests like transferring funds or changing their profile details.

CSRF protection works by adding a hidden field to the form that contains a value (token) that only the application and the user know. This ensures that the user - not some other entity - is submitting the given data.

The symfony/security-csrf component provides CsrfTokenManager for generating and validating CSRF tokens. Forms created with the Symfony Form component include CSRF tokens by default and Symfony checks them automatically, so we do not have to do anything to be protected against CSRF attacks. The csrf_token() Twig function renders the CSRF token for a user.

Symfony CSRF protection example

In the following example, we manually create a form for which we implement the CSRF protection. In this application we define routes in the routes.yaml file.

$ composer create-project symfony/skeleton csrf-app
$ cd csrf-app

With composer, we create a new Symfony skeleton project and locate to the project directory.

$ composer require symfony/security-csrf

We install the security-csrf package.

$ composer require server --dev

We install the development web server.

config/routes.yaml
index:
    path: /
    controller: App\Controller\AppController::index

process-form:
    path: /process
    controller: App\Controller\AppController::processForm

We define two routes for the application. The index route shows the home page with the form. The process-form processes the submitted form and checks the CSRF token.

src/Controller/AppController.php
<?php

namespace App\Controller;

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

class AppController extends AbstractController
{
    public function index()
    {
        return $this->render('home/index.html.twig');
    }

    public function processForm(Request $request)
    {
        $token = $request->request->get("token");

        if (!$this->isCsrfTokenValid('myform', $token))
        {
            return new Response('Operation not allowed', Response::HTTP_BAD_REQUEST,
                ['content-type' => 'text/plain']);
        }

        $name = $request->request->get("name");
        $email = $request->request->get("email");

        $msg = "$name with $email saved";

        return new Response($msg, Response::HTTP_CREATED, ['content-type' => 'text/plain']);
    }
}

The AppController has two actions: index() and processForm().

public function index()
{
    return $this->render('home/index.html.twig');
}

The index() function renders the home page. The home page contains the HTML form.

$token = $request->request->get("token");

We retrieve the CSRF token with the get() method from the request.

if (!$this->isCsrfTokenValid('myform', $token))
{
    return new Response('Operation not allowed', Response::HTTP_BAD_REQUEST,
        ['content-type' => 'text/plain']);
}

We check the validity of the token with isCsrfTokenValid() method. If the token is not valid, we return a response with Response::HTTP_BAD_REQUEST code. The name of the token myform is specified in the HTML form in the template.

templates/home/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block body %}

    <section class="ui container">

        <form class="ui form" action="{{ path('process-form') }}" method="post">

            <input type="hidden" name="token" value="{{ csrf_token('myform') }}" />

            <div class="field">
                <label>Name:</label>
                <input name="name" type="text">
            </div>

            <div class="field">
                <label>Email</label>
                <input name="email" type="text">
            </div>

            <button class="ui button" type="submit">Send</button>

        </form>

    </section>

{% endblock %}

This is the Twig template for the home page with the form. The form is styled with the Semantic UI library.

<form class="ui form" action="{{ path('process-form') }}" method="post">

The form action points to the process-form path. The form's method is POST, which means that CSRF protection is necessary.

<input type="hidden" name="token" value="{{ csrf_token('myform') }}" />

We add the hidden input with the CSRF token. The token is generated with csrf_token().

templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>
        {% block title %}Welcome!
        {% endblock %}
    </title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
</head>

<body>
    {% block body %}{% endblock %}
</body>


<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js">
</html>

This is the base template file. It loads the Semantic UI library.

$ php bin/console server:run

We run the application.

$ curl -d "name=Peter&email=peter@example.com" -X POST http://localhost:8000/process
Operation not allowed

If we try to bypass the form and try to access the controller action with the curl tool, we get an error message.

In this tutorial we have implemented the CSRF protection in our Symfony application.

You might also be interested in the following related tutorials: Symfony validation tutorial, Symfony form tutorial, PHP tutorial, or list all Symfony tutorials.