CSRF stands for Cross-site request forgery. It is a technique hackers use to hack into a web application.
- 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 formhttp://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 likehttp://www.mybank.com/transfer?to=123456;amount=10000
(where123456
is the number of their Cayman Islands account and10000
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.
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.
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
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.
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.
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.
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.