という要件があった。

バグがドラゴンなら、要件は神だろうか…。

ハードルは以下の2つ。

  1. CSRF Protectionで弾かれる(おなじみのActionController::InvalidAuthenticityTokenで500エラー画面。)
  2. iframeで弾かれる(画面遷移しない。Refused to display 'https://example.com/path' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.というエラーがChromeのコンソールに出るはず。)

リファラーがGoogle Translateの場合に、

  1. protect_from_forgeryフィルターを無効化
  2. 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しか実際の挙動は確認していない。

参考