Lilith Daemon
Lilith Daemon

Reputation: 1473

Rails Protect From Forgery with Javascript

I am running into a weird CSRF where I am trying to access a javascript file uploaded on my rails server. I have a controller such as:

class SomeController < ApplicationController
  def show
    some_path = "/some/js/file/on/disk.js"
    send_file(some_path, type: "text/javascript", disposition: :inline)
  end
end

However when navigating to http://localhost:3000/somes/1 I get the error message:

Security warning: an embedded tag on another site requested protected JavaScript. If you know what you're doing, go ahead and disable forgery protection on this action to permit cross-origin JavaScript embedding.

Extracted source (around line #225):

    if marked_for_same_origin_verification? && non_xhr_javascript_response?
      logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
      raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
    end
  end

Note that I am accessing this page directly which means that there is no layout so I cannot include a CSRF token in my layout.

Is there something that needs to be done differently to correctly access this resource?

EDIT: Per comment request, I have added the Full Trace below.

actionpack (4.2.6) lib/action_controller/metal/request_forgery_protection.rb:225:in verify_same_origin_request' activesupport (4.2.6) lib/active_support/callbacks.rb:432:inblock in make_lambda' activesupport (4.2.6) lib/active_support/callbacks.rb:239:in block in halting' activesupport (4.2.6) lib/active_support/callbacks.rb:506:in block in call' activesupport (4.2.6) lib/active_support/callbacks.rb:506:in each' activesupport (4.2.6) lib/active_support/callbacks.rb:506:incall' activesupport (4.2.6) lib/active_support/callbacks.rb:92:in __run_callbacks__' activesupport (4.2.6) lib/active_support/callbacks.rb:778:in _run_process_action_callbacks' activesupport (4.2.6) lib/active_support/callbacks.rb:81:in run_callbacks' actionpack (4.2.6) lib/abstract_controller/callbacks.rb:19:inprocess_action' actionpack (4.2.6) lib/action_controller/metal/rescue.rb:29:in process_action' actionpack (4.2.6) lib/action_controller/metal/instrumentation.rb:32:inblock in process_action' activesupport (4.2.6) lib/active_support/notifications.rb:164:in block in instrument' activesupport (4.2.6) lib/active_support/notifications/instrumenter.rb:20:ininstrument' activesupport (4.2.6) lib/active_support/notifications.rb:164:in instrument' actionpack (4.2.6) lib/action_controller/metal/instrumentation.rb:30:inprocess_action' actionpack (4.2.6) lib/action_controller/metal/params_wrapper.rb:250:in process_action' activerecord (4.2.6) lib/active_record/railties/controller_runtime.rb:18:in process_action' actionpack (4.2.6) lib/abstract_controller/base.rb:137:in process' actionview (4.2.6) lib/action_view/rendering.rb:30:inprocess' actionpack (4.2.6) lib/action_controller/metal.rb:196:in dispatch' actionpack (4.2.6) lib/action_controller/metal/rack_delegation.rb:13:indispatch' actionpack (4.2.6) lib/action_controller/metal.rb:237:in block in action' actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:74:indispatch' actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:43:in serve' actionpack (4.2.6) lib/action_dispatch/journey/router.rb:43:inblock in serve' actionpack (4.2.6) lib/action_dispatch/journey/router.rb:30:in each' actionpack (4.2.6) lib/action_dispatch/journey/router.rb:30:inserve' actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:817:in call' bullet (5.1.1) lib/bullet/rack.rb:12:incall' warden (1.2.6) lib/warden/manager.rb:35:in block in call' warden (1.2.6) lib/warden/manager.rb:34:incatch' warden (1.2.6) lib/warden/manager.rb:34:in call' rack (1.6.4) lib/rack/etag.rb:24:in call' rack (1.6.4) lib/rack/conditionalget.rb:25:in call' rack (1.6.4) lib/rack/head.rb:13:incall' actionpack (4.2.6) lib/action_dispatch/middleware/params_parser.rb:27:in call' actionpack (4.2.6) lib/action_dispatch/middleware/flash.rb:260:in call' rack (1.6.4) lib/rack/session/abstract/id.rb:225:in context' rack (1.6.4) lib/rack/session/abstract/id.rb:220:incall' actionpack (4.2.6) lib/action_dispatch/middleware/cookies.rb:560:in call' activerecord (4.2.6) lib/active_record/query_cache.rb:36:incall' activerecord (4.2.6) lib/active_record/connection_adapters/abstract/connection_pool.rb:653:in call' activerecord (4.2.6) lib/active_record/migration.rb:377:in call' actionpack (4.2.6) lib/action_dispatch/middleware/callbacks.rb:29:in block in call' activesupport (4.2.6) lib/active_support/callbacks.rb:88:in run_callbacks' activesupport (4.2.6) lib/active_support/callbacks.rb:778:in _run_call_callbacks' activesupport (4.2.6) lib/active_support/callbacks.rb:81:in run_callbacks' actionpack (4.2.6) lib/action_dispatch/middleware/callbacks.rb:27:in call' actionpack (4.2.6) lib/action_dispatch/middleware/reloader.rb:73:incall' actionpack (4.2.6) lib/action_dispatch/middleware/remote_ip.rb:78:in call' actionpack (4.2.6) lib/action_dispatch/middleware/debug_exceptions.rb:17:incall' web-console (2.3.0) lib/web_console/middleware.rb:28:in block in call' web-console (2.3.0) lib/web_console/middleware.rb:18:incatch' web-console (2.3.0) lib/web_console/middleware.rb:18:in call' actionpack (4.2.6) lib/action_dispatch/middleware/show_exceptions.rb:30:incall' railties (4.2.6) lib/rails/rack/logger.rb:38:in call_app' railties (4.2.6) lib/rails/rack/logger.rb:20:inblock in call' activesupport (4.2.6) lib/active_support/tagged_logging.rb:68:in block in tagged' activesupport (4.2.6) lib/active_support/tagged_logging.rb:26:in tagged' activesupport (4.2.6) lib/active_support/tagged_logging.rb:68:in tagged' railties (4.2.6) lib/rails/rack/logger.rb:20:incall' quiet_assets (1.1.0) lib/quiet_assets.rb:27:in call_with_quiet_assets' request_store (1.3.1) lib/request_store/middleware.rb:9:incall' actionpack (4.2.6) lib/action_dispatch/middleware/request_id.rb:21:in call' rack (1.6.4) lib/rack/methodoverride.rb:22:incall' rack (1.6.4) lib/rack/runtime.rb:18:in call' activesupport (4.2.6) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in call' rack (1.6.4) lib/rack/lock.rb:17:in call' actionpack (4.2.6) lib/action_dispatch/middleware/static.rb:120:incall' rack (1.6.4) lib/rack/sendfile.rb:113:in call' railties (4.2.6) lib/rails/engine.rb:518:incall' railties (4.2.6) lib/rails/application.rb:165:in call' rack (1.6.4) lib/rack/content_length.rb:15:incall' puma (3.5.0) lib/puma/configuration.rb:225:in call' puma (3.5.0) lib/puma/server.rb:569:inhandle_request' puma (3.5.0) lib/puma/server.rb:406:in process_client' puma (3.5.0) lib/puma/server.rb:271:inblock in run' puma (3.5.0) lib/puma/thread_pool.rb:116:in `block in spawn_thread'

Upvotes: 14

Views: 3691

Answers (5)

Felix Livni
Felix Livni

Reputation: 1244

The issue is that the check in Rails done in verify_same_origin_request that you quote in your question does not actually check the origin. Instead it only checks if (1) it is a GET request, and (2) it is not an XHR request. Your use case satisfies both of these, so the error is raised.

I don't know why the code works in this way. But it does.

Rather than skip the forgery protection completely, you can just skip the call to verify_same_origin_request.

You can do so by adding this to the top of your controller:

class SomeController < ApplicationController
   skip_after_action :verify_same_origin_request

Upvotes: 0

Gui LeFlea
Gui LeFlea

Reputation: 835

I won't ask why you're using a controller to send a javascript file to the browser even though that doesn't seem like a good idea. I hope these suggestions help.

You could try

class SomeController < ApplicationController
  def show
    some_path = "/some/js/file/on/disk.js"

    respond_to do |format|
      format.js {
        send_file(some_path, type: "text/javascript", disposition: :inline) 
      }
      format.html {
        "Html request from browser. Try sending a js request to get <Javascript>"
      }
    end
  end
end

The other answer is to change the CSRF handling. This is similar to the answer Michal already suggested,

    class SomeController < ApplicationController
        protect_from_forgery except: :show 
        ...
    end

In my opinion changing CSRF handling approach is much broader in scope. Disabling CSRF for a given method in the controller exposes things that you may not want exposed.


Here are some additional suggestions.

It may be old fashioned, but curl enables one to gain complete control of the HTTP request headers as well as seeing the full HTTP response. By calling curl -H "Content-Type: application/javascript" http://someurl/here/1 you will be able to see exactly what's happening and why your browser is unable to serve the requested javascript file, or if there is a workaround.

Lastly, if you're trying to serve up static (javascript) files in Rails, there is a lot of extra overhead and potential security risk using a controller to perform that action. Unless there is a very good reason for using the controller, a simpler solution would be to store the files in a sub-directory of the ./public directory on the server, so that anyone and everyone can read the file(s). When you deploy the application to a production environment this could save even more overhead, but that's beyond the scope of your original question.

Good luck!

Upvotes: 3

Benjamin
Benjamin

Reputation: 1070

Checking out the conditions that fire the error:

marked_for_same_origin_verification? && non_xhr_javascript_response?

I went to the source and found:

  # GET requests are checked for cross-origin JavaScript after rendering.
  def mark_for_same_origin_verification!
    @marked_for_same_origin_verification = request.get?
  end

  # If the `verify_authenticity_token` before_action ran, verify that
  # JavaScript responses are only served to same-origin GET requests.
  def marked_for_same_origin_verification?
    @marked_for_same_origin_verification ||= false
  end

So that seems to come back true if it's a GET request.

Meanwhile,

  def non_xhr_javascript_response?
    content_type =~ %r(\Atext/javascript) && !request.xhr?
  end

Which seems to describe your response--a non XHR request yielding a file with text/javascript content.

I suppose that you could avoid this (other than by skipping verify_authenticity_token) by forking a branch of rails and changing one of those conditions, or else introducing some layout so the response isn't just javascript.

Upvotes: 1

Michał Młoźniak
Michał Młoźniak

Reputation: 5556

As error message says, you need to disable forgery protection for this action.

class SomeController < ApplicationController
  skip_before_action :verify_authenticity_token, only: :show

  def show
    some_path = "/some/js/file/on/disk.js"
    send_file(some_path, type: "text/javascript", disposition: :inline)
  end
end

Upvotes: 2

vitomd
vitomd

Reputation: 806

Some suggestions:

1) Make sure to add <%= csrf_meta_tag %>in your layout

2) Make sure that you are including the csrf-token hidden field. For instance if you are using a form in the show view. Normally the form builders do it automatically.

3) Set application/javascript" in the send_file

if request.format.js?
   send_file(assetfilename, type: 'application/javascript')
else
   send_file(assetfilename)
end

Upvotes: 1

Related Questions