Rails CSRF protection with Single Page Application (react, angular, ember)
Asked Answered
M

3

8

Ok. I officially lost my mind with this problem.

Let's take a default Rails application (5, but I tried also with a 4 default app).

I'm trying to use a simple javascript code to send an ajax POST request to one controller action.

In my ApplicationController I have this code:

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  protected

    def set_csrf_cookie
      cookies["X-CSRF-Token"] = form_authenticity_token
    end

end

which sets a cookie "X-CSRF-Token" with the value of form_authenticity_token.

After that I can read this cookie in my SPA (Single Page Application) using this code:

<script>
function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(";");
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i];
      while (c.charAt(0) === " ") c = c.substring(1, c.length);
      if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
  }

// var token = document.getElementsByName('csrf-token')[0].content; // this works!
const token = readCookie("X-CSRF-Token"); // this doesn't work!

fetch('/api/v1', {
  method: 'POST',
  body: {""},
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': token
  },
  credentials: 'include'
}).then(function(response) {
  return response.json();
});
</script>

When I use this line:

var token = document.getElementsByName('csrf-token')[0].content;

it works because it reads what Rails insert in html page with:

<%= csrf_meta_tags %>

<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="VXaKlO+/Gr/8pGhr5y0bThQ5L/0IDiznMR/9SpaoI6vOoF9KtmB5/9ka+Hz+zjyssNRi/Em/Ye27C+E5pl3odg==">

So the content of "csrf-token" works and my Rails application can validate CSRF.

This is the code from Rails source: https://github.com/rails/rails/blob/v5.2.0/actionpack/lib/action_controller/metal/request_forgery_protection.rb

When instead I use this line:

const token = readCookie("X-CSRF-Token");

it doesn't work and I get this error:

Started POST "/api/v1" for 172.18.0.1 at 2018-05-01 18:52:56 +0000
Processing by MyController#action as */*
  Parameters: {"body"=>{}}
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 2ms (ActiveRecord: 0.0ms)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

Also if I use another page with another server (npm http-server or Microsoft IIS or others) with the same script the problem is the same.

If I copy the content of "csrf-token" from Rails html page and use this line in my Javascript script:

const token = "VXaKlO+/Gr/8pGhr5y0bThQ5L/0IDiznMR/9SpaoI6vOoF9KtmB5/9ka+Hz+zjyssNRi/Em/Ye27C+E5pl3odg==";

it WORKS!

So my question is: WHY?


What I have read (nut nothing!):

Maximalist answered 1/5, 2018 at 19:2 Comment(1)
You could simplify your readCookie function with this: document.cookie.match("X-CSRF-Token=([^;]+)")[1]Underhill
A
5

The name of the header Rails expects is X_CSRF_TOKEN (note the underscores). I don't see a problem with the rest of the code you've shared - except maybe that the token from the cookie must be URI decoded (decodeURIComponent), so check this as well if you still get the warning.

Acroter answered 8/5, 2018 at 10:43 Comment(4)
Are you sure about underscores in X_CSRF_TOKEN?Maximalist
It shouldn't matter too much as Rails most probably treats them the same - it's how I use it though so just to be on the safe side. Posting only when I know certainly it works :)Acroter
I think it is here in code: github.com/rails/rails/blob/master/actionpack/lib/… and I see hyphens... right?Maximalist
There is a lot going on under the hood, check github.com/rails/rails/blob/… as well - the headers in the request environment are all underscore, however they have already been processed.. I think it's safe to assume that 1. Using the header with hyphens is the preferred way and 2. Rails/rack seems to treat hyphens and underscores equallyAcroter
B
6

Tnx, I got your code working. I only needed to add decodeURIComponent()

const token = decodeURIComponent(readCookie("X-CSRF-Token"));

I used this approach for a Progressive Web App with cached html. The default rails meta tag (<%= csrf_meta_tags %>) doesn't work with cached html.

This blog post also gives some other alternatives: https://www.fastly.com/blog/caching-uncacheable-csrf-security

Biped answered 9/11, 2018 at 13:59 Comment(1)
Could you give some more details about how you reproduced this problem with cached html? I'm having the same issue (I believe) with my SPA. I'm setting the X-CSRF-Token in a header, but have recently received a huge spike of InvalidAuthenticityToken exceptions, I believe because some browser is caching HTML when the browser closes (and my cookie expires).Revers
A
5

The name of the header Rails expects is X_CSRF_TOKEN (note the underscores). I don't see a problem with the rest of the code you've shared - except maybe that the token from the cookie must be URI decoded (decodeURIComponent), so check this as well if you still get the warning.

Acroter answered 8/5, 2018 at 10:43 Comment(4)
Are you sure about underscores in X_CSRF_TOKEN?Maximalist
It shouldn't matter too much as Rails most probably treats them the same - it's how I use it though so just to be on the safe side. Posting only when I know certainly it works :)Acroter
I think it is here in code: github.com/rails/rails/blob/master/actionpack/lib/… and I see hyphens... right?Maximalist
There is a lot going on under the hood, check github.com/rails/rails/blob/… as well - the headers in the request environment are all underscore, however they have already been processed.. I think it's safe to assume that 1. Using the header with hyphens is the preferred way and 2. Rails/rack seems to treat hyphens and underscores equallyAcroter
Z
1

are you checkedreadCookie("X-CSRF-Token") value? the value probably is different when storing to cookie, probably the value escaped

Zephyrus answered 2/7, 2018 at 6:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.