Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

CSRF Protection and Form Security

CSRF Protection and Form Security

~13 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

To wrap up block 3, let's understand a security feature we've already used UNKNOWINGLY on every {{ form(form) }} call: CSRF protection.

What is a CSRF attack?

Cross-Site Request Forgery: a MALICIOUS, foreign website tricks a logged-in user's browser into unknowingly sending a request to OUR application – e.g. a hidden form that automatically triggers POST /projects/1/delete on our task manager while the user visits a completely different page. Since the browser AUTOMATICALLY sends valid session cookies along, this request would look "legitimate" to our server.

How Symfony's CSRF protection works

Symfony Forms AUTOMATICALLY insert a hidden _token field with a session-bound, cryptographically random value – a foreign site CANNOT know EXACTLY this token, since it's accessible neither in the URL nor to JavaScript on another domain. If the token is missing or doesn't match, Symfony rejects the request BEFORE any further processing.

Best part: THIS HAPPENS AUTOMATICALLY. createForm() (chapter 15) inserts the token, handleRequest() checks it – we've had CSRF protection for EVERY one of our forms since chapter 15, without knowing it.

Making the token visible

Render a form and open the browser's source view – you'll find a field like:

<input type="hidden" id="project__token" name="project[_token]" value="a1b2c3...">

CSRF protection WITHOUT Symfony Forms

Some actions (e.g. a "delete" button that doesn't need a full form with text fields) often don't use Symfony Forms at all – but they still deserve CSRF protection. Manually, with the csrf_token() helper:

<form method="post" action="{{ path('project_delete', {id: project.id}) }}">
    <input type="hidden" name="_token" value="{{ csrf_token('delete-project-' ~ project.id) }}">
    <button type="submit">Delete</button>
</form>
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Csrf\CsrfToken;

#[Route('/projects/{id}/delete', name: 'project_delete', methods: ['POST'])]
public function delete(int $id, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
{
    $token = new CsrfToken('delete-project-' . $id, $request->request->get('_token'));

    if (!$csrfTokenManager->isTokenValid($token)) {
        throw $this->createAccessDeniedException('Invalid CSRF token.');
    }

    // Delete the project (block 4)
    return $this->redirectToRoute('project_index');
}

The string 'delete-project-' . $id is the "token ID" – deliberately made UNIQUE per project, so a token generated for project 1 can't be reused to delete project 2.

methods: ['GET'] is also a security principle

Achtung: Recall chapters 7/9: state-changing actions (create, update, delete) should NEVER be reachable via GET routes – GET requests are often PREFETCHED or repeated by browsers, crawlers, and proxies, which could lead to ACCIDENTAL deletions with a GET-based delete route, with no malicious intent at all. methods: ['POST'] for EVERY action with side effects isn't a style choice, it's a security principle.

With that, block 3 (Twig & forms) is complete! Our task manager now has real, safely validated, CSRF-protected forms – block 4 finally adds a REAL database, so the entered data persists permanently.