27

I am trying to implement some ajax functionality in my Symfony 2 project. Using jquery's $.post I want to send some data back to my controller. However, when I just POST the data no CSRF protection is in place, as symfony's csrf protection only seems to apply to forms.

What would be a pretty straightforward way to implement this?

When using forms I can just do $form->isValid() to find out whether or not the CSRF token passes. I am currently placing everything I want to POST in a form and then posting that. Which basically means I am only using that form to implement CSRF protection, which seems hacky.

Elnur Abdurrakhimov
  • 44,533
  • 10
  • 148
  • 133
peterrus
  • 651
  • 2
  • 6
  • 18
  • Aren't all of the other pages just accessible pages from the user any way? Meaning that the usual security.yml settings would stop people who should be see this seeing it. – qooplmao Mar 31 '13 at 03:59

4 Answers4

26

In Symfony2 CSRF token is based on session by default. If you want to generate it, you just have to get this service and call generation method:

//Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider by default
$csrf = $this->get('form.csrf_provider');
//Intention should be empty string, if you did not define it in parameters
$token = $csrf->generateCsrfToken($intention); 

return new Response($token);

This question might be useful for you

Community
  • 1
  • 1
Vitalii Zurian
  • 17,858
  • 4
  • 64
  • 81
  • 2
    Alright, that solves half of the problem. Now I would need to verify if the POST'ed token, I would do that with this: http://api.symfony.com/2.0/Symfony/Component/Form/Extension/Csrf/CsrfProvider/SessionCsrfProvider.html#method_isCsrfTokenValid – peterrus Aug 21 '12 at 12:40
  • Now I would need to return the token into the template. As the post is made from a full-fledged HTML page, I could put it in a div with a CSS hidden property, and let every ajax-enabled script on that page use it. But as the token is only valid once, I would have to let every ajax POST return a new token and set that back into the hidden div. Now if a request is made, and the response times out, serverside a new token would've been generated but it wouldn't be set. Small chance this happens though. I am going to accept this as an answer, although It still feels really hacky. – peterrus Aug 21 '12 at 12:47
  • @peterrus Well... If you are not happy with this native behavior - you can write your own csrf_provider and implement behavior that you need :) – Vitalii Zurian Aug 21 '12 at 12:49
  • Well the problem is not in the csrf provider afaik. In fact there isn't really a problem now, but I would feel more certain about it if this workflow was described in the official documentation ;) – peterrus Aug 21 '12 at 12:58
  • 6
    For embedding the token: Setting the X-CSRF-Token header seems like a neat solution: http://erlend.oftedal.no/blog/?blogid=118 – peterrus Aug 21 '12 at 15:18
  • @peterrus Although this is an older question/answer I'm wondering if you have a working example since I have the same issue where I have many "formless" inputs that are sending data to my server with no CSRF protection at the moment. Thanks – John the Ripper Jul 07 '16 at 12:28
  • Sorry John, I haven't been using symfony for a few years now :( – peterrus Jul 07 '16 at 17:45
  • No problem @peterrus. Thanks for the reply either way as I've figured this one out :) – John the Ripper Jul 12 '16 at 17:18
  • 2
    The name of the service is changed to `security.csrf.token_manager` in Symfony 3. The interface is changed, too. You need to call `getToken()` instead of `generateCsrfToken()`. – fracz Nov 05 '16 at 18:01
2

I had this problem, intermittently. Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.

/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
    $storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
    return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});
Josh Ribakoff
  • 2,948
  • 3
  • 27
  • 26
1

You should try this snippet. Symfony form should generate special _csrf_token that should be send with post request. Without this value security alert will be raised.

Of course #targetForm should be replaced by form id and /endpoint by target ajax url

$('#targetForm').bind('submit', function(e) {

    e.preventDefault();
    var data = $(this).serialize();

    $.post('/endpoint', data, function(data) {
        // some logic here
    });

});
Luke Adamczewski
  • 395
  • 3
  • 14
  • 1
    Yes, But I am trying not to use a form for every bit of ajax that I want to use. Still good option for sending forms over ajax! – peterrus Aug 21 '12 at 12:34
0

In Symfony 4+ you can use dependency injection right into your controller or action or wherever, for example, if you are submitting a form and wish to refresh the token of the same form, the $tokenId is the FQDN of the form type class:

namespace App\Controller;

use App\Form\MyFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class MyController extends AbstractController
{
    public function submit(CsrfTokenManagerInterface $tokenManager): JsonResponse
    {
        // ...
        $token = $tokenManager->refreshToken(MyFormType::class);
        return new JsonResponse(['token' => $token->getValue()]);
    }
}

And in your JavaScript you can update the existing token <input>.

const token = document.getElementById('_token');
fetch(url, opts)
    .then(resp => resp.json())
    .then(response => {
        if (response.token) {
            token.value = response.token;
        }
    });
Yes Barry
  • 9,514
  • 5
  • 50
  • 69