In this chapter we will look into what tools Rails provides in combating CSRF
attacks.
CSRF token authenticity
Let's see how Rails behaves when a request is sent, and the CSRF token is not
sent:
Now let's submit a curl request:
Rails prevented the POST request from making any changes. This is the default
behavior of Rails. By default, Rails expects all non GET requests to have CSRF
token.
Let's look under the hood to see how things work.
How Rails implements forgery protection
Our application_controller
inherits from ActionController::Base
:
The
official codebase
for ActionController::Base
, has the following lines of code in it:
The key statement here is protect_from_forgery with: :exception
. Let's see how
the method protect_from_forgery
is implemented in the
official codebase:
Notice that Rails adds a before_action :verify_authenticity_token
.
In the same file there is method
verified_request?
:
As we can see above Rails does not check for CSRF protection if it is a GET
request or if it is a HEAD
request.
Last method we need to look at is
request_authenticity_tokens
:
As we can see Rails is looking for CSRF token at the following two places:
CSRF protection when plain vanilla Rails form is used
In Rails view when a form is built using form_for
or form_tag
then Rails
generates a hidden input field like this:
When this form is submitted then authenticity_token
is also submitted. On the
server, Rails retrieves the token using params[:authenticity_token]
. Rails
checks if the token has been tampered with and if everything is fine then that
request proceeds.
If our application is making AJAX calls using remote: true
option in
form_for
then Rails automatically takes care of everything as long as we are
using jquery-rails gem.
How CSRF token works when we are using React.js
If we are using React.js, Angular.js, jQuery etc then in those case we do not
use form_for
or form_tag
to create the form. In these cases we do not get
the hidden input field. So how do we handle cases like this.
When a brand new Rails application is created then Rails also creates a layout
file called application.html.erb
. If we open up this file then we will see
following line in the head section:
When the page is rendered it looks like this:
Now when React.js, jQuery or any other technology is making a request to the
server then they need to read the CSRF token value from the meta tag. This can
be done like this:
My application does not use Rails layout
Above solution will only work if our application uses application.html.erb
layout. If our application is 100% using React.js, Angular.js or similar
technology then the application does not have csrf_meta_tags
.
In such cases we can make use of
form_authenticity_token
helper method:
Skipping CSRF protection
There are valid cases when CSRF protection is not needed. Let's say that our
application is 100% React.js application and all frontend code is in React.js.
The application needs user to be authenticated for the user to do anything.
Since the application requires user to be authenticated and since this is 100%
pure React.js application then for each request the application is sending
X-AUTH-TOKEN
to identify who is logged in.
In such case we can ignore CSRF protection because the hacker would not know
what X-AUTH-TOKEN to send.
We saw earlier that forgery protection is done by Rails by adding a
before_action
. If we want to skip the CSRF protection then we can skip that
before_action
:
We can also use protect_from_forgery
. It offers except
, only
and
some more options:
Handling unverified requests
If a request fails the forgery check then we have three ways to handle it:
- Raise an exception
- Reset the session
- null session for the duration of the request.
Raise an exception
In this case if a request is submitted and forgery check fails then
ActionController::InvalidAuthenticityToken
exception is raised. We can rescue
this exception and we can take whatever action we want to take.
Reset the session
In this case if a request is submitted and forgery check fails then session is
completely reset.
Let's say that in our application user is logged in, and it has many pages and
each page has a form. Let's assume that in one of the forms the developer forgot
to send CSRF value. When a logged-in user submits that form then Rails will
detect that no CSRF token is sent. In this case Rails will reset the session.
Resetting the session means that user is no longer logged in. So the end result
is that after submitting the form which does not send CSRF token user will be
logged out.
Note that in this case Rails is not preventing the request from going through.
It's just setting the session as empty. Now if the intended controller and the
action expects a person to be logged in then the request will fail with a
different error.
Empty session during the request
In this case if a request is submitted and forgery check fails then Rails
provides an empty session. But the important thing here is that the empty
session is only for the duration of the call. After the request is processed
then the old session is restored.
Let's say that in our application user is logged in, and it has many pages and
each page has a form. Let's assume that in one of the forms the developer forgot
to send CSRF value. When a logged-in user submits that form then Rails will
detect that no CSRF token is sent. In this case Rails will provide an empty
session for the duration of the call. Once the request is processed then user is
still logged in.
Note that in this case Rails is not preventing the request from going through.
It's just setting the session as empty. Now if the intended controller and the
action expects a person to be logged in then the request will fail with a
different error.
There is nothing to commit in this chapter since all we had done was
learning the basics of CSRF attack prevention.