Avoid InvalidCrossOriginRequest Exceptions when Having a Catch-all Route

#Ruby, #Rails, #How-To


We recently deployed a CMS-like Rails application, and immediately got hit by a lot of ActionController::InvalidCrossOriginRequest exceptions, all containing the following description:

Security warning: an embedded <script> 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.

These exceptions got triggered by various bots and crawlers requesting javascript URLs like /wp-includes/js/wpdialog.js. These URLs sure don’t exist in our app, so why didn’t Rails just respond with a “404 Not Found” status?

Turns out the root cause for this was that our app has a catch-all route like this:

1
2
3
4
# routes.rb

# … (various more specific routes) …
match "/(*page_path)", to: "pages#show", as: :page, page_path: ""

This route is accompanied by a controller action that renders a matching page, or a 404 error page if no page was found:

1
2
3
4
5
6
7
8
9
10
class PagesController < ApplicationController
  def show
    @page = Page.find_first_by(:page_path, params[:page_path])
    if @page
      render_page(@page)
    else
      render status: 404, file: "#{Rails.root}/public/404.html", layout: false
    end
  end
end

In principle this works fine: If you request a non-existing URL, the 404 error page gets rendered. However, if the URL ends in .js, an InvalidCrossOriginRequest exception is raised instead.

A Closer Look Into Rails’ Request Forgery Protection

Looking into ActionController::RequestForgeryProtection (source) reveals that for this exception to be raised, three conditions have to be met:

  1. Request forgery protection must be active (protect_from_forgery, docs).
  2. The request must have been a non-XHR GET request.
  3. The response must have a content-type of text/javascript.

Now we can’t really do anything about the first two points (disabling request forgery protection would be a no-go), but what about the third? Why does our response even have a content-type of text/javascript when we’re rendering a perfectly normal HTML template?

Turns out that if the response’s content-type is not explicitly set when rendering the template. Rails infers it based on the requested format. And for URLs ending .js, the requested format is text/javascript.

This led to the obvious and easy fix:

The Fix

Simply add an explicit content-type to the action rendering the error template:

1
2
3
4
5
6
7
8
9
10
11
class PagesController < CmsController
  def show
    # (unchanged)
    if @page
      # (unchanged)
    else
      # Note the addition of `content_type: 'text/html'`:
      render status: 404, file: "#{Rails.root}/public/404.html", layout: false, content_type: 'text/html'
    end
  end
end

And voilà: Requesting non-existent JavaScript URLs no longer results in any exceptions, but just renders the 404 error page as expected.


Your Rails app needs to be upgraded or extended? We’ve been working with Rails since the release of Rails 0.9.3 in 2005.

Talk to us!