Secure actions on your website with Symfony2 and CSRF

How to make use POST method with CSRF token for simple actions like deleting a comment, or a simple-click action.

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:

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:

// 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 }