How does Rails CSRF protection work? - ruby-on-rails

Rails raises an InvalidAuthenticityToken when the CSRF token doesn't match. But, from reading the source, I can't figure out how this actually happens. I start by acking the tree for that class:
$ ack --ignore-dir=test InvalidAuthenticityToken
actionpack/lib/action_controller/metal/request_forgery_protection.rb
4: class InvalidAuthenticityToken < ActionControllerError #:nodoc:
17: # which will check the token and raise an ActionController::InvalidAuthenticityToken
actionpack/lib/action_dispatch/middleware/show_exceptions.rb
22: 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
Only two hits, ignoring the comment. The first one is the class definition:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
The second one is translating the exception into an HTTP status code. CSRF protection gets enabled by calling protect_from_forgery in the controller, so let's look at that:
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
before_filter :verify_authenticity_token, options
end
It adds a filter:
def verify_authenticity_token
verified_request? || handle_unverified_request
end
Which calls this when verification fails:
def handle_unverified_request
reset_session
end
So how is InvalidAuthenticityToken actually raised?

The behavior was changed fairly recently but the documentation has yet to be updated. The new approach being used is to presume the session has been hijacked, and therefore to clear the session. Assuming your session contains the all-important authentication information for this request (like the fact you're logged in as alice) and your controller assures the user is authenticated for this action, your request will be redirected to a login page (or however you choose to handle a non logged-in user). However, for requests which are not authenticated, like a sign-up form, the request would go through using an empty session.
It seems this commit also goes on to close a CSRF vulnerability, but I didn't read into the details of that.
To obtain the old behavior, you would simply define this method:
def handle_unverified_request
raise(ActionController::InvalidAuthenticityToken)
end
You can read more about CSRF and other Rails security issues at the Ruby on Rails Security Guide.

verify_authenticity_token used to be defined as
verified_request? || raise(ActionController::InvalidAuthenticityToken)
but as you noted, it now calls handle_unverified_request, which in turn calls reset_session
I don't think Rails actually throws that exception anymore.
http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails states
After applying this patch failed CSRF
requests will no longer generate HTTP
500 errors, instead the session will
be reset. Users can override this
behaviour by overriding
handle_unverified_request in their own
controllers.
https://github.com/rails/rails/commit/66ce3843d32e9f2ac3b1da20067af53019bbb034

Related

Rails: Can't verify CSRF token authenticity when making a POST request

I want to make POST request to my local dev, like this:
HTTParty.post('http://localhost:3000/fetch_heroku',
:body => {:type => 'product'},)
However, from the server console it reports
Started POST "/fetch_heroku" for 127.0.0.1 at 2016-02-03 23:33:39 +0800
ActiveRecord::SchemaMigration Load (0.0ms) SELECT "schema_migrations".* FROM "schema_migrations"
Processing by AdminController#fetch_heroku as */*
Parameters: {"type"=>"product"}
Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1ms
Here is my controller and routes setup, it's quite simple.
def fetch_heroku
if params[:type] == 'product'
flash[:alert] = 'Fetch Product From Heroku'
Heroku.get_product
end
end
post 'fetch_heroku' => 'admin#fetch_heroku'
I'm not sure what I need to do? To turn off the CSRF would certainly work, but I think it should be my mistake when creating such an API.
Is there any other setup I need to do?
Cross site request forgery (CSRF/XSRF) is when a malicious web page tricks users into performing a request that is not intended for example by using bookmarklets, iframes or just by creating a page which is visually similar enough to fool users.
The Rails CSRF protection is made for "classical" web apps - it simply gives a degree of assurance that the request originated from your own web app. A CSRF token works like a secret that only your server knows - Rails generates a random token and stores it in the session. Your forms send the token via a hidden input and Rails verifies that any non GET request includes a token that matches what is stored in the session.
However in an API thats intended to be used cross site and even serve non-browser clients its not very useful due to the problems with cross-domain cookies and providing CSRF tokens.
In that case you should use a token based strategy of authenticating API requests with an API key and secret since you are verifying that the request comes from an approved API client - not from your own app.
You can deactivate CSRF as pointed out by #dcestari:
class ApiController < ActionController::Base
protect_from_forgery with: :null_session
end
Updated. In Rails 5 you can generate API only applications by using the --api option:
rails new appname --api
They do not include the CSRF middleware and many other components that are superflouus.
http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
https://labs.kollegorna.se/blog/2015/04/build-an-api-now/
WARNING: Can't verify CSRF token authenticity rails
Another way to turn off CSRF that won't render a null session is to add:
skip_before_action :verify_authenticity_token
in your Rails Controller. This will ensure you still have access to session info.
Again, make sure you only do this in API controllers or in other places where CSRF protection doesn't quite apply.
There is relevant info on a configuration of CSRF with respect to API controllers on api.rubyonrails.org:
⋮
It's important to remember that XML or JSON requests are also affected and if you're building an API you should change forgery protection method in ApplicationController (by default: :exception):
class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }
end
We may want to disable CSRF protection for APIs since they are typically designed to be state-less. That is, the request API client will handle the session for you instead of Rails.
⋮
Since Rails 5 you can also create a new class with ::API instead of ::Base:
class ApiController < ActionController::API
end
If you're using Devise, please note that
For Rails 5, protect_from_forgery is no longer prepended to the before_action chain, so if you have set authenticate_user before protect_from_forgery, your request will result in "Can't verify CSRF token authenticity." To resolve this, either change the order in which you call them, or use protect_from_forgery prepend: true.
Documentation
If you only want to skip CSRF protection for one or more controller actions (instead of the entire controller), try this
skip_before_action :verify_authenticity_token, only [:webhook, :index, :create]
Where [:webhook, :index, :create] will skip the check for those 3 actions, but you can change to whichever you want to skip
If you want to exclude the sample controller's sample action
class TestController < ApplicationController
protect_from_forgery except: :sample
def sample
   render json: #hogehoge
end
end
You can to process requests from outside without any problems.
If you are doing an api base website without csrf authentication, just put this in the controller
skip_before_action :verify_authenticity_token
But this expose the api endpoints to everyone
To overcome this issue, in our app(which is api based + front end in react) we are passing this in the headers
.
.
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']").content,
},
.
.
The simplest solution for the problem is do standard things in your controller or you can directely put it into ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
end
in Rails 6, I found a easy way to solve this "Can't verify CSRF token authenticity". Just puts
config.action_controller.default_protect_from_forgery = false # unless ENV["RAILS_ENV"] == "production"
in application.rb
its' convenient in development mode. but not use this in production.

Protect API with CSRF token or other authentication

In a Rails project I am setting up my own API, which I would like to call from the same site with some jQuery calls.
For example, I have a route POST /api/v1/employee.
I want this API call to be available for users of my site, but also for an external client that is calling this API with (for example) basic authentication.
So I would like to protect my API against CSRF when the call is coming from my own site and when it is from an external client I want it to check for the username and password in the header of the request.
I set up jQuery to add the csrf token specified in the meta tags of my page, to the every request it does to the API.
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
}
});
But now I am not sure where to go from here.
In my controller I added the following line that enabled my client to connect to the API without problems, but then I have no protection.
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
If I set up a before_filter for this controller it always checks for the username and password, even when a csrf token is available.
before_filter :authentication_check
^ authentication_check is a method I created which does a simple basic authentication check (just to get this going).
I also tried to do the following:
protect_from_forgery with: :exception, :unless => :valid_user?
But this probably doesn't do what I think. valid_user? is a method that checks the basic authentication and returns the user if correct.
I somehow want to combine these 2 things. If there is no session (so it's and external client connecting) I want to authenticate on username/password and otherwise just use the normal CSRF protection.
UPDATE November 25th 2015
Ok I now have the following solution for POST requests. Still need to add GET, etc, but here it is.
In my controller I have this:
protect_from_forgery with: :exception, unless: :valid_apikey
My :valid_apikey thing method is defined like this:
def valid_apikey
result = authenticate_or_request_with_http_token do |token, options|
!User.find_by_token(token).nil?
end
result == true
end
So, if I am correct, the CSRF checking is skipped when the request supplies a token that is in my database. When the token is not in my database the authenticate_or_request_with_http_token method returns an Array (which is different than true).
I suggest you dont have to put an exception for your protect_from_forgery
You can simply do this
class ApplicationController < ActionController::Base
protect_from_forgery
def handle_unverified_request
# your logic to handle request coming from outside your domain
# if user is not valid then
raise(ActionController::InvalidAuthenticityToken)
# else allow to bypass
end
This way u can control and also for normal requests from your domain, you dont ve to go through if conditions and hitting db everytime.
Ok, got this solved for I think. I also want my GET requests to check for the CSRF token.
In my controller I do the following:
def verified_request?
if !request.headers["X-API-KEY"].nil?
!ApiClient.find_by_token(request.headers["X-API-KEY"]).nil?
else
!protect_against_forgery? ||
valid_authenticity_token?(session, params[request_forgery_protection_token]) ||
valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
end
end
This overrides the verified_request? method of the inherited ApplicationController. I can't just call super, because then my GET requests will not get checked.

Rails: Authenticate before routing

The What.
I'm trying to implement token based Basic authentication on my Rails API. It works fine for existing routes, but here's the catch:
When an unauthenticated user visits a route that does NOT exist, it displays the 404 - Not found page, and not the 401 - Unauthorized. How do I get Rails to check authentication before validating the routes?
Here's my application_controller.rb:
class Api::V1::ApiController < ApplicationController
# Main controller inherited by all API controllers
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
# Enforce site-wide authentication
before_action :authenticate
def authenticate
authenticate_token || render_unauthorized
end
def authenticate_token
# Can we find a user with the authentication token used?
authenticate_with_http_basic do |u, p|
# Only search active, private keys
#api_key = ApiKey.active.find_by(api_key: u, is_secret: true)
#user = #api_key.user if #api_key
return #api_key
end
end
def render_unauthorized
# Displays the Unauthorized message
render json: JSON.pretty_generate({
error: {
type: "unauthorized",
message: "This page cannot be accessed without a valid API key."
}
}), status: 401
end
end
The Why
Think of it this way: someone stops you at the office to ask for directions. Do you ask them to present some ID first or do you just show them the way?
If it were a public office, I'd just show them the way .. but if we were in the restricted Special Projects division at the CIA, I don't care where you're going (even especially if you tell me you're looking for Office 404, which I know doesn't exist): I want to see some ID first.
Edit: Using Basic Auth
I originally mentioned Token based authentication, but it's actually Basic Auth (with "username" = "token").
Using the built-in rails authentication you can't achieve what you want for the very reason #rich-peck explained. Rails first evaluate your routes, then passes the request for the controller.
You have two options. First, do exactly what the Stripe guys are doing. Delegate the authentication to the web server in front of your app server (nginx in their case).
$ curl -I https://api.stripe.com/this/route/doesnt/exist
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Fri, 08 Aug 2014 21:21:49 GMT
Content-Type: application/json;charset=utf-8
Www-Authenticate: Basic realm="Stripe"
Cache-Control: no-cache, no-store
Stripe-Version: 2014-08-04
Strict-Transport-Security: max-age=31556926; includeSubDomains
Or, if you want/need to keep it in ruby-land, you can use a rack middleware which will authenticate before rails loads your routes. You can try https://github.com/iain/rack-token_auth
$ bin/rake middleware
...snip a bunch of middleware...
use Rack::TokenAuth
run Dummy::App::Application.routes
Update: Using HTTP Basic Authentication
In that case, you don't need another library at all, because rack has built-in support for http basic auth through the Rack::Auth::Basic middleware, e.g.
config.middleware.use Rack::Auth::Basic do |username, password|
username == "bob" && password == "secret"
end
Update: Can I apply the authentication to the API namespace?
It depends. Because the rack middleware runs before rails loads your routes, controllers, etc. it has no knowledge of your namespaces. Nonetheless, if your namespaced controllers are also namespaced at the URL level, e.g. all under /api, you can check request.path to whether apply the authentication.
You can create a new rack middlware to decorate Rack::Auth::Basic with this behaviour, e.g.
class MyAPIAuth < Rack::Auth::Basic
def call(env)
request = Rack::Request.new(env)
if request.path =~ /\A\/api\/?/
super
else
#app.call(env)
end
end
end
# change your configuration to use your middleware
config.middleware.use MyAPIAuth do |username, password|
username == "bob" && password == "secret"
end
All you need to do is to append a catch-all route at the end of your routes.rb:
YourApp::Application.routes.draw do
# Standard routes
# ...
# Catch-all
get '*other', to: 'dummy#show'
end
Then have a dummy controller just render 404 - Not Found in show action. Should anyone visit a non-existing route it will be handled by dummy, which will perform authentication first in a before filter and if it fails 401 - Unauthorized is rendered.
I think you're getting past the idea of MVC
When you send a request to Rails, the routing mechanism is meant to take the requested URL & send you to a particular area (controller). The authentication happens inside that controller - allowing you handle routed traffic in a modular way
I think the problem you have is authenticating before you send the user to the respective controller goes against MVC principles - whereby your authentication (pinging the database) will have to happen before you get to the controller / model
Authentication
Have you checked out this Railscast?
It shows how to use token authentication in your controller - allowing you to secure your API
I'm assuming your on Rails 3.X.X just as I was and was receiving the same error (they used Rails 4 for the Code School lesson). Here's what you have to change:
before_action :authenticate
must be changed to
before_filter :authenticate
User.find_by(auth_token: token)
must be changed to
User.find_by_auth_token(token)

Rails Devise - current_user is nil

For some reason, current_user returns nil in my model-less controller (Subscriptions). I have found nothing on the Internet to justify this behavior...
class SubscriptionsController < ApplicationController
def new
...
end
def create
current_user # returns nil
end
end
I have a csrf meta tag :
<meta content="xxx" name="csrf-token">
I can provide more code, but I'm not sure what would be useful.
UPDATE
So thanks to the comments/answers, I have pinpointed the problem to one particular action : create.
if I add #user = current_user to the new, I can show the current user's email in my new view. However, in my create controller, current_user returns nil.
I accessed the create action through a form (submit).
Before the form is submitted, I validate the input and then send a request to Stripe to get a token out of the form. If there are no errors (validation and stripe), I then send the form.
Could that be the cause?
UPDATE 2
In my error message, my session dump is empty, while it should contains the current_user info...
It turned out the AJAX request I was making didn't carry the CSRF token. For that reason, Rails was killing my session.
I added skip_before_filter :verify_authenticity_token in my SubscriptionsController and it is now working. It might not be the most secure solution, but it works for now, so I continue to develop and come back to this issue later.
Note that when you create forms using the form_tag helper, they do not automatically generate the hidden field which holds the token for CSRF authentication. I ran into this same issue with a form I had constructed using the form_tag which I sometimes prefer using.
I fixed the issue by including the following helpers within the form:
<%= hidden_field_tag 'authenticity_token', form_authenticity_token %>
It's basically a manual way of generating the hidden field you need for the CSRF stuff.
for current_user to work you need to add before_filter :authenticate_user! to your class, like:
class SubscriptionsController < ApplicationController
before_filter :authenticate_user!
def new
...
end
def create
curent_user # returns nil
end
end
and the authenticate_user! method will set the current user for you :)
I had a similar issue but I was editing the model. So everytime I updated the model suddenly that would happen:
current_model to nil
After analyzing things, it turns out that if you leave the password in the form, when the user tries to edit some attribute, the person is then forced to write a password.
Once the form is delivered and updated, Devise does the rational thing when someone updates a password, which is to destroy the session and ask the user to sign in again.
So that was why current_model was suddenly turning to nil. Hope this helps, have a great day!

Devise AJAX - POST request : current_user is null

I try to develop a single page application with authentication. I use Devise (Rails) and AngularJS.
For some different reasons my rails application and my angularjs application are not on the same server. So, I have to handle cross domain issues... And I can't send X-CSRF-Token in my header.
I can correctly sign in and sign out and send GET request without problem. In my rails controllers, I can use current_user, it's correctly set.
However, when I send a POST request, current_user is null. It seems my session_id is not sent. The problem is due to cross domain, because if I send my ajax request from the same server, it is OK.
I think, I have 2 solutions :
- Don't use authentication cookie-based, but use token
- Put front-end and back-end on the same server.
Other ideas ? Why current_user is null when I send a POST request from cross domain ?
You could send the CSRF token in the headers HOWEVER it's a bad practice that exposes some security holes (issue thread on github explaining why)
Safest way to go about this is to disable CSRF all together:
class ApplicationController < ActionController::Base
# or use Api::BaseController < ApplicationController if you are namespacing
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
end
AND use token based authentication that you can either implement yourself or use devise's :token_authenticatable. You will have to configure AngularJS to send the token either in params or headers. Here's a snippet I use for having rails figure out if the token is in the headers or params.
class Api::BaseController < ApplicationController
prepend_before_filter :get_auth_token
private
def get_auth_token
if auth_token = params[:auth_token].blank? && request.headers["X-AUTH-TOKEN"]
params[:auth_token] = auth_token
end
end
end
So in the end how it works is:
Client uses login method that you defined to authenticate
Obtains authentication token from the server
Uses the token in each subsequent request for the purpose of authorization

Resources