Skip to content

Instantly share code, notes, and snippets.

@chans-me
Created February 24, 2022 11:33
Show Gist options
  • Save chans-me/768bf24e122c09b1a5ca322054f668f5 to your computer and use it in GitHub Desktop.
Save chans-me/768bf24e122c09b1a5ca322054f668f5 to your computer and use it in GitHub Desktop.
What is CSRF Tokens and How does it works in Rails?

CSRF stands for Cross-site request forgery. It is a technique hackers use to hack into a web application.

Cross-Site Request Forgery (CSRF) in simple words

  • Assume you are currently logged into your online banking at www.mybank.com
  • Assume a money transfer from mybank.com will result in a request of (conceptually) the form http://www.mybank.com/transfer?to=<SomeAccountnumber>;amount=<SomeAmount>. (Your account number is not needed, because it is implied by your login.)
  • You visit www.cute-cat-pictures.org, not knowing that it is a malicious site.
  • If the owner of that site knows the form of the above request (easy!) and correctly guesses you are logged into mybank.com (requires some luck!), they could include on their page a request like http://www.mybank.com/transfer?to=123456;amount=10000 (where 123456 is the number of their Cayman Islands account and 10000 is an amount that you previously thought you were glad to possess).
  • You retrieved that www.cute-cat-pictures.org page, so your browser will make that request.
  • Your bank cannot recognize this origin of the request: Your web browser will send the request along with your www.mybank.com cookie and it will look perfectly legitimate. There goes your money!

This is the world without CSRF tokens.

Preventing CSRF attacks

In order to prevent CSRF attacks from happening Rails uses authenticity_token.

If you look at source code of any form generated by Rails you will see that form contains following code

<input name="authenticity_token" type="hidden"
   value="/BgNtznwUYpTazCtmMIYXfefewgrwthyntymtymIA==" />

The exact value of the authenticity_token will be different for you. When form is submitted then authentication_token is submitted and Rails checks the authenticity_token and only when it is verified the request is passed along for further processing.

In a brand new rails application the application_controller.rb has only one line.

class ApplicationController < ActionController::Base
  protect_from_forgery
end

That line protect_from_forgery checks for the authentication of the incoming request.

Here is code that is responsible for generating csrf_token.

# Sets the token value for the current session.
def form_authenticity_token
  session[:_csrf_token] ||= SecureRandom.base64(32)
end

Since this csrf_token is a random value there is no way for hacker to know what the "csrf_token" is for my session. And hacker will not be able to pass the correct "authenticity_token".

Do keep in mind that this protection is applied only to POST, PUT and DELETE requests by Rails. Rails states that GET should not be changing database in the first place so no need for check for authenticity of the token.

Skipping CSRF protection

There are valid cases when CSRF protection is not needed. We saw earlier that forgery protection is done by Rails by adding a protect_from_forgery in ApplicationController. If we want to skip the CSRF protection then we can skip that before_action

class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token
end

We can also use protect_from_forgery. It offers except, only

class ApplicationController < ActionController::Base
  protect_from_forgery except: [:create]
  protect_from_forgery only: [:update]
end

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

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

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

class ApplicationController < ActionController::Base
  protect_from_forgery with: :reset_session
end

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

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment