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:
- Request forgery protection must be active (
protect_from_forgery
, docs). - The request must have been a non-XHR GET request.
- 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.