Secure actions on your website with Symfony2 and CSRF

Sometimes, it may be hard to secure some actions on website. Here are some few examples where creating a form is clearly overkill. Those simple actions should be secured.

  • disable, validate, delete a comment
  • enable an account
  • logout
  • send e-mail

symfony 1.4 legacy

In symfony 1.4, it was possible to secure a link with a POST method, using helper link_to. This helper was used like this:

<?php echo link_to('@route', array('sf_method' => 'POST'), 'your text);

Easy, isn't it? This helper created a Javascript function to post form:

<a href="/your-route" onclick="var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'post'; f.action = this.href">your text</a>

See code on github for more details : https://github.com/symfony/symfony1/blob/1.4/lib/helper/UrlHelper.php#L610

POST a link with jQuery

Add this snippet to your javascript library. Make it included on page rendering:

/**
 * Sample usage:
 *
 * <a href="/blog/48/delete" data-method="POST">delete this post</a>
 */
$(document).ready(function () {

    // Every link with an attribute data-method
    $("a[data-method]").click(function (event) {
        event.preventDefault();

        var target = $(event.currentTarget);
        var method = target.attr('data-method');
        var action = target.attr('href');

        // Create a form on click
        var form = $('<form/>', {
            style:  "display:none;",
            method: method,
            action: action,
        });

        form.appendTo(target);

        // Submit the form
        form.submit();
    });
});

And in your template:

<a data-method="POST" href="{{ path('message_delete', {id: message.id, token: token}) }}">delete</a>

Simple CSRF in Symfony2

Finally, we need to pass a token value to the template and check it:

<?php
// src/Acme/DemoBundle/Controller/MessageController.php

public function listAction()
{
    $token = $this->get('form.csrf_provider')->generateCsrfToken('message_list');

    return $this->render('AcmeDemoBundle:Message:list.html.twig', array(
        'token'    => $token,
        'messages' => array() // put your business here
    ))
}

public function deleteAction()
{
    if (!$this->get('form.csrf_provider')->isCsrfTokenValid($intention, $token)) {
        $this->get('session')->setFlash('notice', 'Woops! Token invalid!');

        return $this->redirect('message_list');
    }

    return $this->render('AcmeDemoBundle:Message:list.html.twig', array(
        'token'    => $token,
        'messages' => array() // put your business here
    ))
}

Constraint routing

If you don't change your routing, this deleteAction method will allow POST and GET requests. You need to change your routing and add a requirement on method:

# routing.yml
message_delete:
    pattern: /message/{id}/delete
    requirements: { _method: POST }

Damien Alexandre November 1, 2012

Nice tips,
but the Javascript could be improved by not using "return false;" : http://fuelyourcoding.com/jquery-events-stop-misusing-return-false/

And the $(document).find() can be reduced to $("a[data-method]").

My 2cts :-)

Romain Pouclet November 1, 2012

Nice !
Not sure I would have used a flash message, I don't like the idea of sending a "200 - OK" when the token is invalid.

Christophe Coevoet November 1, 2012

The javascript could be improved by using the event delegation instead of binding a listener on each link. And this would even work if you add some links on the page later:

$(document).on('click', 'a[data-method]', function(event){})

Btw, note that Symfony 2.1 provides a Twig function csrf_token() to generate a token directly in the template.