83

How can I retrieve the CSRF token to pass with a JSON request?

I know that for security reasons Rails is checking the CSRF token on all the request types (including JSON/XML).

I could put in my controller skip_before_filter :verify_authenticity_token, but I would lose the CRSF protection (not advisable :-) ).

This similar (still not accepted) answer suggests to

Retrieve the token with <%= form_authenticity_token %>

The question is how? Do I need to do a first call to any of my pages to retrieve the token and then do my real authentication with Devise? Or it is an information one-off that I can get from my server and then use consistently (until I manually change it on the server itself)?

Community
  • 1
  • 1

10 Answers10

131

EDIT:

In Rails 4 I now use what @genkilabs suggests in the comment below:

protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }

Which, instead of completely turning off the built in security, kills off any session that might exist when something hits the server without the CSRF token.


skip_before_filter :verify_authenticity_token, :if => Proc.new { |c| c.request.format == 'application/json' }

This would turn off the CSRF check for json posts/puts that have properly been marked as such.

For example, in iOS setting the following to your NSURLRequest where "parameters" are your parameters:


[request setHTTPMethod:@"POST"];

[request setValue:@"application/json" 
       forHTTPHeaderField:@"content-type"];

[request setValue:@"application/json" 
       forHTTPHeaderField:@"accept"];

[request setHTTPBody:[NSData dataWithBytes:[parameters UTF8String] 
                                            length:[parameters length]]];
Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
Ryan Crews
  • 3,015
  • 1
  • 32
  • 28
  • 3
    doing this you are exposed to an attack if the attacker is marking the format as 'json'. And that is why Rails by the definition is issuing a warning. I want to have my json to be able to pass properly a token, otherwise having the warning in the logs is just fine. –  Apr 06 '12 at 23:55
  • 22
    you are exposed to attacks, which is why they recommend that you have some other security in place when turning off the filter. Typically for API requests the requester would send an API key along with the post data, that you would then verify before executing the desired method. – Ryan Crews Apr 07 '12 at 00:19
  • that a good a idea thanks! I am going to accept your suggestion! ^^ –  Apr 07 '12 at 01:45
  • 20
    In rails 4 you can do something like: ```protect_from_forgery with: :null_session, :if => Proc.new { |c| c.request.format == 'application/json' }``` – genkilabs Aug 30 '13 at 21:24
  • 2
    @genkilabs I like that, added to answer for rails 4 users, thanks =] – Ryan Crews Feb 20 '14 at 01:03
  • 1
    Two things I did just now to make this work in my Rails & iOS app: 1) I didn't see any reason why we were mixing old- and new-style hash syntax in Ruby, so the code in my controller looks like: `protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }` 2) Unrelated to this question but related to the subject of POSTing JSON from iOS, since I'm using `AFNetworking` and Rails wasn't wrapping parameters automagically, I did the following: `manager.requestSerializer = [AFJSONRequestSerializer serializer];` – mharper May 01 '14 at 21:36
  • 1
    @mharper I have no idea why I mixed syntax, updated now, thanks =] – Ryan Crews May 01 '14 at 21:59
  • 1
    I am using rails 4.06. The request.format is returning a Mime::Type. So `c.request.format == 'application/json'` always returning false. The working version for me is `protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format.to_s == 'application/json' }` – Runbai Ma Aug 01 '14 at 19:41
  • I am a little confused by the solution. `protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }` Doesn't this mean the method is only called if the request format is JSON? That seems like the opposite of what we want. Or does it mean it always called but only with null season if the request format is JSON? – sabman Mar 30 '16 at 21:52
  • 1
    That statement says if a request doesn't get through the verify_authenticity_token method, then Rails will not fetch the session by cookie data but instead create a new session for the request. The request will be an instance of NullSessionHash, hence null_session. Also, use the following if you don't want exact matching. This will handle additional similar content types: `protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format =~ %r{application/json} }` – Jimmy Shaw Jul 20 '17 at 18:09
19

You can send the CSRF token, after a successful log-in, using a custom header.

E.g, put this in your sessions#create :

response.headers['X-CSRF-Token'] = form_authenticity_token

Sample log-in response header providing the CSRF token:

HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Connection: Keep-Alive
Content-Length: 35
Content-Type: application/json; charset=utf-8
Date: Mon, 22 Oct 2012 11:39:04 GMT
Etag: "9d719d3b9aabd413c3603e04e8a3933d"
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-10-12)
Set-Cookie: [cut for readability] 
X-Csrf-Token: PbtMPfrszxH6QfRcWJCCyRo7BlxJUPU7HqC2uz2tKGw=
X-Request-Id: 178746992d7aca928c876818fcdd4c96
X-Runtime: 0.169792
X-Ua-Compatible: IE=Edge

This Token is valid until you log-in again or (log-out if you support this through your API). Your client can extract and store the token from the log-in response headers. Then, each POST/PUT/DELETE request must set the X-CSRF-Token header with the value received at the log-in time.

Sample POST headers with the CSRF token:

POST /api/report HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate, compress
Content-Type: application/json; charset=utf-8
Cookie: [cut for readability]
Host: localhost:3000
User-Agent: HTTPie/0.3.0
X-CSRF-Token: PbtMPfrszxH6QfRcWJCCyRo7BlxJUPU7HqC2uz2tKGw=

Documentation: form_authenticity_token

tasos
  • 392
  • 3
  • 9
18

Indeed simplest way. Don't bother with changing the headers.

Make sure you have:

<%= csrf_meta_tag %>

in your layouts/application.html.erb

Just do a hidden input field like so:

<input name="authenticity_token" 
       type="hidden" 
       value="<%= form_authenticity_token %>"/>

Or if you want a jquery ajax post:

$.ajax({     
    type: 'POST',
    url: "<%= someregistration_path %>",
    data: { "firstname": "text_data_1", "last_name": "text_data2", "authenticity_token": "<%= form_authenticity_token %>" },                                                                                  
    error: function( xhr ){ 
      alert("ERROR ON SUBMIT");
    },
    success: function( data ){ 
      //data response can contain what we want here...
      console.log("SUCCESS, data="+data);
    }
});

Basically when you post your json data just add a valid authenticity_token field to the post data and the warning should go away...

Danila Ganchar
  • 10,266
  • 13
  • 49
  • 75
  • IMHO it should not only put a warning in the logger but completely discard posts without a valid csrf tag by default it just posts the warning and submits the data just fine... – Walter Schreppers May 06 '13 at 13:10
  • The shorter way to do the hidden authenticity token input field is `= token_tag(nil)` (nil is important) – Yo Ludke Nov 06 '15 at 13:39
  • In older versions of rails, the tag ends plural. <%= csrf_meta_tags %> – cyonder Oct 18 '17 at 20:50
4

I resolved that error this way:

class ApplicationController < ActionController::Base
  protect_from_forgery
  skip_before_action :verify_authenticity_token, if: :json_request?

  protected

  def json_request?
    request.format.json?
  end
end

Source: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html

Omar Ali
  • 8,467
  • 4
  • 33
  • 58
gusjabar
  • 49
  • 1
  • 1
  • I'm not sure you'd want to do this -- browsers can use JSON APIs. – Joe Van Dyk Jan 05 '16 at 04:15
  • Thank you! Needed a quick (and dirty) way to solve this problem and this might work well enough for what I'm trying to achieve. – NerdyTherapist Jun 29 '16 at 20:44
  • Linked doc (now updated for Rails 5) appears to be: `protect_from_forgery with: :exception, unless: -> { request.format.json? }` and it worked for me. Thanks. – akostadinov Aug 23 '16 at 20:34
  • 1
    This just disables the check, which avoids the problem. If and app is going to disable this then what's the point of having protection from forgery turned on? – jefflunt Nov 22 '16 at 21:14
3

What's worrying is that in Rails 3.2.3 we now get the CSRF warning in production.log but the post does not fail! I want it to fail as it protects me from attacks. And you can add the csrf token with jquery before filter btw:

http://jasoncodes.com/posts/rails-csrf-vulnerability

2

I have used the below. Using include? so if the content type is application/json;charset=utf-8 then it is still working.

protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format.include? 'application/json' }
DaCart
  • 71
  • 3
2

This answer is better.

You get to keep the CSRF-TOKEN validation with no extra effort (the token is appended) before any XMLHttpRequest send. No JQuery, no nothing just copy/paste and refresh.

Simply add this code.

(function() {
    var send = XMLHttpRequest.prototype.send,
        token = $('meta[name=csrf-token]').attr('content');
    XMLHttpRequest.prototype.send = function(data) {
        this.setRequestHeader('X-CSRF-Token', token);
        return send.apply(this, arguments);
    };
}());
Community
  • 1
  • 1
Emanuel
  • 610
  • 6
  • 15
1

I had the same issue with the following version of Rails:
gem 'rails', :git => 'git://github.com/rails/rails.git', :branch => '3-2-stable'

I updated to 3.2.2 and everything works fine for me now. :)
gem 'rails', '3.2.2'

Olivier Grimard
  • 2,783
  • 1
  • 16
  • 8
  • thanks for the suggestion. I tried it out, but I still have the same warning `WARNING: Can't verify CSRF token authenticity` –  Mar 16 '12 at 12:37
0

I ran into the same issue tonight. The reason that happens is because when you sign in the last csrf-token is no longer valid. What I did was: $("meta[name=csrf-token]").attr('content', '<%= form_authenticity_token %>'); in your app/views/devise/sessions/create.js.rb.

Now it does have a valid csrf-token :) I hope it helps

nbit001
  • 224
  • 2
  • 6
0

Also for development/test mode.

protect_from_forgery with: :exception unless %w(development test).include? Rails.env

This warning shows because you are using :null_session, in Rails 4.1 it works by default if no with: options specified.

protect_from_forgery
merqlove
  • 3,674
  • 1
  • 23
  • 22