14

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'

6
  • You are trying to access this resource from where and how? What's the name of the file that source comes from? Commented Sep 12, 2016 at 16:14
  • @Leito I am not really sure how the file name is relevant, but it is called sketch.js and stored utilizing Carrierwave Commented Sep 14, 2016 at 18:13
  • I meant the file where the error is occurring. The one where line #225 is? Commented Sep 15, 2016 at 19:06
  • @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.) Commented Sep 16, 2016 at 1:14
  • @Leito I added the full trace in case that helps. Its causing an error somewhere in rails itself. Commented Sep 16, 2016 at 19:44

5 Answers 5

3

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!

Sign up to request clarification or add additional context in comments.

1 Comment

btw, the mime type text/javascript has been standardized as application/javascript. See stackoverflow.com/questions/876561/…
2

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

1 Comment

This is a current workaround that I am using, however it potentially exposes the application to a csrf attack. Some of the javascript files can potentially be sensitive, so I don't want to leave it open to this.
1

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

2 Comments

The OP is accessing the file directly via GET localhost:3000/some/1 which invokes send_file so there is no layout here.
As @KyleDecot stated, there is no layout as it is just using sendfile with disposition: :inline in the controller
1

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.

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.