3
votes

I'm working in a legacy rails application, I'm trying to clean up some CSRF vulnerabilities. If I remove the hidden CSRF field from the form I can still successfully submit the form.

The only indication that something is amiss is a warning in the logs: WARNING: Can't verify CSRF token authenticity.

There are some pages where the protect_from_forgery will catch the request and the app will crash if there is no csrf token but its hit and miss depending on the page: eg. the login page works without the token but the update user page does not.

I've tried coming up with a custom strategy (suggested by Marc Gauthier) for protect_from_forgery, something like:

protect_from_forgery with: :MyStrategy

class MyStrategy
  byebug
  def initialize(controller)
    @contriller = controller
  end

  def handle_unverified_request
    puts "HELLO!"
    Rails.logger.warn [
      "handle_unverified_request",
      "#{@controller.controller_name}-#{@controller.action_name}"
    ].join(" - ")

  end
end

This didn't seem to do anything when starting the app the byebug call will pause but I never get the puts message or the error log.

I've also tried the normal strategies such as with: :exception but nothing changes, some pages it works and some it doesn't, but they are consistent.

2
I'm concerned about the "hit and miss" behaviour you mention. Are you saying that the same form will sometimes submit successfully without CSRF token, and will sometimes will fail? It seems impossible! Is this really what you're saying? Sorry, but I believe you must be mistaken. But perhaps I haven't understood what you mean by "hit and miss".Les Nightingill
The same form will either always work or not work, but depending on the page the CSRF protection will either work or not work. So for example, with the login page and the forgot password page the token won't work (I can remove it and submit the form with no issues) but on the edit user details page if I remove the token the form won't submit and the app will crash (as expected/desired).tfantina
Could you share a little more code? E.g. the full code of the ApplicationController (which is presumably where you call protect_from_forgery) could be helpful.Clemens Kofler
Unfortunately, being a legacy client app the ApplicationController is over 400 lines long and I wouldn't feel comfortable sharing that much from their application. Is there something specific I should look for or a particular method you'd like to see? I realize that's not very helpful, I'm just hoping somebody may have encountered something similar.tfantina
Focus on login. What's the controller, I'll call it LoginController since I don't know. There is a line in LoginController or ApplicationController that is defeating the CSRF check. Start deleting everything that is not related to receiving the login form, section by section, method by method, whilst trying the form submit after each deletion. Eventually you'll delete the line that was causing the problem. Without seeing your code that's the best I can suggest. If you're debugging someone else's 400-line ApplicationController, this may be the best way.Les Nightingill

2 Answers

1
votes

Usually it is important to protect logged-in users session from CSRF, so in many apps it's customary to disable the protection on login/logout to prevent legitimate users from getting errors. In most cases there's no much harm that can be done by RF if the session is going to be terminated/reset anyway.

Look for skip_before_action :verify_authenticity_token disabling the action that's being installed by protect_from_forgery

Check your testing method - hidden form field is not always necessary because rails also uses X-CSRF-Token headers for ajax forms. To properly test for forgery protection - do an actual forgery attempt, for example using curl.

Check that config.action_controller.allow_forgery_protection is not disabled for development/production, and controller or it's ancestors do not have allow_forgery_protection overloaded and returning false for the request in question. Very unlikely, but app may have some other parts of forgery protection overloded, see request_forgery_protection.rb

PS. byebug in class context is not very useful for this case, more visible way is to raise "Hello CSRF" in handle_unverified_request

0
votes

It seems like Rails was doing what it was supposed to, just resetting the session, which wouldn't really matter on a login form or a forgotten password form. Since we only had protect_from_forgery (with: :exception did not become the default until sometime in Rails 5).

One would think that adding with: :exception would work, but it does not. The workaround is to extract the contents of the Exception class and put them in the application_controller:

 def handle_unverified_request
     raise ActionController::InvalidAuthenticityToken
  end

This works for the login and forgot password forms, the other forms (which were correctly not allowing bad requests through) remain the same, which is kind of odd because I would assume that both would fail with the InvalidAuthenticityToken raised.

This, of course, leaves the question, why is protect_from_forgery with: :exception not working?