13
votes

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'

4
You are trying to access this resource from where and how? What's the name of the file that source comes from?Leonel Galán
@Leito I am not really sure how the file name is relevant, but it is called sketch.js and stored utilizing CarrierwaveChris Britt
I meant the file where the error is occurring. The one where line #225 is?Leonel Galán
@Leito Idk then, its some file in rails I am assuming. (The shown code isn't mine, and it didn't give any further information.)Chris Britt
@Leito I added the full trace in case that helps. Its causing an error somewhere in rails itself.Chris Britt

4 Answers

2
votes

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
2
votes

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!

1
votes

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
1
votes

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.