简体   繁体   中英

Laravel 5 CSRF global token hidden field for all forms in a page

I recently migrated to Laravel 5, and now CSRF check is on every post submission. I thought about removing it but I want to follow the best practices, so I'll keep it that way.

On the other hand, I'm problems submitting ajax requests.. my page has multiple forms and some submissions are not even from forms, just plain ajax calls. My idea is to have one single hidden "token" input on the page and attach it to every submission. Are there any drawbacks on having that universal single token input?

Also, how can I output the token? Would it be ok to just create a hidden input on the page footer?

I don't see any drawbacks. You can easily create a global token field in your layout file:

<input type="hidden" name="_token" id="csrf-token" value="{{ Session::token() }}" />

Or if you use the form builder:

{!! Form::token() !!}

In jQuery you could use something like this to attach the token to every request.

There is a helper to add the form token inside forms. You can just use

{!! csrf_field() !!}

inside the forms. It will add the hidden input and the token.

You can use something like this at the bottom of the page:

$('form').append('{{csrf_field()}}');

This will append a hidden input to all your forms :

<input type="hidden" name="_token" value="yIcHUzipr2Y2McGE3EUk5JwLOPjxrC3yEBetRtlV">

And for all your AJAX requests:

$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        //////////// Only for your domain
        if (settings.url.indexOf(document.domain) >= 0) {
            xhr.setRequestHeader("X-CSRF-Token", "{{csrf_token()}}");
        }
    }
});

Here are some excerpts of how I got my CSRF working for all the different scenarios in my jQuery Mobile application that I recently upgraded to use Laravel 5:

I added an encrypted csrf token in a variable that will be passed to my views in my main base controller: app\\Http\\Controllers\\MyController.php

$this->data['encrypted_csrf_token'] = Crypt::encrypt(csrf_token());

Then, I added the meta tag in my main view header: resources\\views\\partials\\htmlHeader.blade.php

<meta name="_token" content="{!! $encrypted_csrf_token !!}"/>

Then, I also added this jquery snippet as suggested in some forums:

    $(function () {
            $.ajaxSetup({
                    headers: {
                            'X-XSRF-TOKEN': $('meta[name="_token"]').attr('content')
                    }
            });
    });

But, the key (for my setup at least) was the addition of the check for the XSRF-TOKEN cookie in my custom VerifyCsrfToken middleware: app\\Http\\Middleware\\VerifyCsrfToken.php:

    /**
     * Determine if the session and input CSRF tokens match.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function tokensMatch($request)
    {       
            $token = $request->session()->token();

            $header = $request->header('X-XSRF-TOKEN');

            $cookie = $request->cookie('XSRF-TOKEN');

            return StringUtils::equals($token, $request->input('_token')) ||
                   ($header && StringUtils::equals($token, $this->encrypter->decrypt($header))) ||
                    ($cookie && StringUtils::equals($token, $cookie));
    }

Before I added that, just about all of my AJAX POSTs (including form submissions and lazyloading listviews) were failing due to a TokenMismatchException .

EDIT: On second thought, I'm not sure how much sense it makes to compare the session token with the one set in the cookie (which would have come from the session token in the first place right?). That may have just been bypassing the security of it all.

I think my main issue was with the jquery snippet above which was supposed to be adding the X-XSRF-TOKEN header to every ajax request. That wasn't working for me in my in jQuery Mobile app (specifically, in my lazyloader plugin ) until I added some options for the plugin itself. I added a new default selector csrf (which would be meta[name="_token"] in this case) and a new default setting csrfHeaderKey (which would be X-XSRF-TOKEN in this case). Basically, during initialization of the plugin, a new _headers property is initialized with the CSRF token if one is locatable by the csrf selector (default or user-defined). Then, in the 3 different places where an ajax POST can be fired off (when resetting session variables or when lazyloading a listview) the headers option of $.ajax is set with whatever is in _headers .

Anyway, since the X-XSRF-TOKEN received on the server-side comes from the encrypted meta _token, I think the CSRF protection is now working as it should.

My app\\Http\\Middleware\\VerifyCsrfToken.php now looks like this (which is essentially back to the default implementation provided by Laravel 5 - LOL):

    /**
     * Determine if the session and input CSRF tokens match.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function tokensMatch($request)
    {
            $token = $request->session()->token();

            $_token = $request->input('_token');

            $header = $request->header('X-XSRF-TOKEN');

            return StringUtils::equals($token, $_token) ||
                   ($header && StringUtils::equals($token, $this->encrypter->decrypt($header)));
    }

I think you can do something like this (not tested will update if I get a chance)

$(document).on('submit', 'form', function(e)
      $(this).append('<input name="_token" value="{{{ Session::token() }}}">);
});

you actually might want to store token in a variable that you reupdate as it expires.

The benefit of appending it on submit is if you append elements via ajax I think it'll still work without having to add anything else.

EDIT: Here's a great article on using Rails UJS with Laravel (which includes this auto CRSF token functionality): https://medium.com/@barryvdh/unobtrusive-javascript-with-jquery-ujs-and-laravel-e05f444d3439

You need to pass along the header X-XSRF-TOKEN which contains an encrypted version of the csrf-token .

There are two ways which this can be done that I am aware of. You can encrypt the token and pass it along to the view:

$xsrfToken = app('Illuminate\Encryption\Encrypter')->encrypt(csrf_token());

return view('some.ajax.form.view')->with('xsrf_token', $xsrfToken);

Or you can grab the token from cookies using JavaScript (Angular makes this easy). In vanilla JS you might do something like this:

function getCookie(name) {
    var pattern = RegExp(name + "=.[^;]*")
    matched = document.cookie.match(pattern)
    if (matched) {
        var cookie = matched[0].split('=')
        return decodeURIComponent(cookie[1])
    }
    return false
}

In jQuery you might then do something like this for the ajax request:

$.ajax({
    // your request
    //
    beforeSend: function(request) {
        return request.setRequestHeader('X-XSRF-TOKEN', getCookie('XSRF-TOKEN'));
    }
});

All the answer do not cover JS files. What if we are not using blade and we want to use JS files. blade syntax will not work there.

so here is the code which will work everywhere for forms and for ajax.

var csrf = document.querySelector('meta[name="csrf-token"]').content;
var csrf_field = '<input type="hidden" name="_token" value=“'+csrf+'”>';

$('form').append(csrf_field);

$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (settings.url.indexOf(document.domain) >= 0) {
            xhr.setRequestHeader("X-CSRF-Token", csrf);
        }
    }
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM