という要件があった。
バグがドラゴンなら、要件は神だろうか…。
ハードルは以下の2つ。
- CSRF Protectionで弾かれる(おなじみの
ActionController::InvalidAuthenticityToken
で500エラー画面。) - iframeで弾かれる(画面遷移しない。
Refused to display 'https://example.com/path' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.
というエラーがChromeのコンソールに出るはず。)
リファラーがGoogle Translateの場合に、
protect_from_forgery
フィルターを無効化X-Frame-Options
ヘッダーを削除
となるようにApplicationController
を変更。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, if: :csrf_protected?
after_action :allow_iframe, if: :iframe_allowed?
private
# For Google Translate
def from_google_translate?
request.referer =~ %r{\Ahttps://translate.googleusercontent.com/}
end
def csrf_protected?
!from_google_translate?
end
def iframe_allowed?
from_google_translate?
end
def allow_iframe
response.headers.except! 'X-Frame-Options'
end
end
確認画面
上記でうまく行くFormもあったんだけど、確認画面があるFormがまたiframeのエラーで失敗。
初回の画面は、Google翻訳のサーバーが返してる(/translate_p?hl=ja&sl=en&tl=ja&u=https://example.com/
のようなURL)けど、遷移後は普通に自分のアプリが返してる画面なので、リファラーがGoogleのものにならない。
これに対応するため、リファラが自分のアプリの時もiframeを許可するように変更。
def from_same_origin?
request.referer =~ /\A#{main.root_url.gsub(/http/, 'https?')}/
end
def iframe_allowed?
from_google_translate? || from_same_origin?
end
一部がエンジン化してあるプロジェクトで、エンジン側のテストがActionController::UrlGenerationError
でこけてたので、main_app.root_url
を使ってるけど、普通はroot_url
でよいと思う。
ApplicationControllerでやってるけど、影響範囲大きすぎるので、特定のコントローラーでやるようにした方がよさそう。
テスト
Controller Spec書いたので、載せておく。
require 'rails_helper'
RSpec.describe ApplicationController do
controller do
def index
render nothing: true
end
end
describe 'CSRF protection' do
before do
ActionController::Base.allow_forgery_protection = true
end
after do
ActionController::Base.allow_forgery_protection = false
end
context 'referer is google translate' do
it 'skips CRSF protection' do
request.env["HTTP_REFERER"] = 'https://translate.googleusercontent.com/'
expect {
post :index
}.to_not raise_error
end
end
context 'referer is same origin' do
it 'does not skip CRSF protection' do
request.env["HTTP_REFERER"] = root_url
expect {
post :index
}.to raise_error(ActionController::InvalidAuthenticityToken)
end
end
context 'referer is example.com' do
it 'does not skip CRSF protection' do
request.env["HTTP_REFERER"] = 'http://example.com/'
expect {
post :index
}.to raise_error(ActionController::InvalidAuthenticityToken)
end
end
end
describe 'iframe protection' do
context 'referer is google translate' do
it 'skips iframe protection' do
request.env["HTTP_REFERER"] = 'https://translate.googleusercontent.com/'
post :index
expect(response.headers['X-Frame-Options']).to eq(nil)
end
end
context 'referer is same origin' do
it 'skips iframe protection' do
request.env["HTTP_REFERER"] = root_url
post :index
expect(response.headers['X-Frame-Options']).to eq(nil)
end
end
context 'referer is example.com' do
it 'does not skip iframe protection' do
request.env["HTTP_REFERER"] = 'http://example.com/'
post :index
expect(response.headers['X-Frame-Options']).to eq('SAMEORIGIN')
end
end
end
end
ちなみに、Google翻訳、httpとhttpsでなんか挙動が違う感じだった。(セーフモードと関係ある?)今回はhttpsしか実際の挙動は確認していない。