Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Content-Security-Policyの検証方法と、SeleniumとCupriteの比較(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: コンテンツセキュリティポリシー (CSP) - HTTP | MDN

Rails: Content-Security-Policyの検証方法と、SeleniumとCupriteの比較(翻訳)

その昔、RailsEventStoreの熱心な愛好家がissueを報告してくれたことがありました(#1062)。それによって、RES::Browserコンポーネントが、彼らのRailsアプリで使われている十分筋の通ったContent-Security-Policy(CSP)と互換性がないことが判明したのです。このレポートは興味深い議論に発展しました。最終的に、あるプルリクの後で当プロジェクトは新たなコントリビュータを迎えることとなり、CSPにより適したセットアップになりました。

手動テストを行わずに、この改良が今後のリリースで壊れないことを保証するにはどうすればよいと思いますか?続きをお読みください。

Content-Security-Policyについて

Content-Security-Policyについて簡単におさらいしておきましょう。

Content-Security-Policyは、最近のブラウザがドキュメント(Webページ)のセキュリティを強化するために使うHTTPレスポンスヘッダの名前。Content-Security-Policyヘッダーは、JavaScript、CSSなど、ブラウザが読み込むリソースを制限できる。
what this CSP thing isより

要するに、Content-Security-PolicyヘッダーはXSSやインジェクション攻撃を防ぐのに役立ちます

たとえば、Webサーバーが「インラインスクリプトを実行してはならない」とブラウザに指示するとします。ブラウザはレスポンスで以下のHTTPヘッダーを受け取ることで、この指示を認識します。

content-security-policy: script-src 'self'

これにより、ブラウザはレスポンスのHTML body内で以下のようなインラインスクリプトを見つけても一切実行しなくなります。

<script type="text/javascript">
  alert("spanish inquisition");
</script>

代わりにエラーが発生してログに保存されます。インラインスクリプトが正当なものか攻撃者が注入したものかどうかは無関係です。このポリシーはインラインスクリプトの実行を厳格に禁止します。

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-b1No4u4UwgH6M1mNU7GPc4D3Fc2lJ26AvLJAgCR+lvE='), or a nonce ('nonce-...') is required to enable inline execution.

Content-Security-Policy違反の検出方法

この時点では、ポリシーを決定するのがアプリケーションまたはWebサーバーであることが既にわかっています。そして、Webブラウザはこのポリシーを検証して強制する「エンジン」を搭載しています。したがって、テストではヘッドレスブラウザを用いるようにし、ブラックボックスの中は覗き込まないようにするのがベストでしょう。

望ましいContent-Security-Policyを検証するには、最初にエンジンの振る舞いをエミュレーションする必要があります。RES::Browserは技術的にはRackアプリケーションの1つで、Railsアプリにマウントされるか単独で実行されます。前者が最も多いユースケースなので、ここでは前者に注目しましょう。

RES::Browserがマウントされると、RES::BrowserはRailsのContent-Security-Policyヘッダーに依存するようになります。テストではアプリケーション全体をこれに巻き込む必要はありません。Content-Security-Policyヘッダーを追加する小さなRackミドルウェアを使えば十分エミュレーションできます。

class CspApp
  def initialize(app, policy)
    @app = app
    @policy = policy
  end

  def call(env)
    status, headers, response = @app.call(env)

    headers["content-security-policy"] = @policy
    [status, headers, response]
  end
end

このミドルウェアでRES::Browserコンポーネントをラップします。これで、CapybaraCupriteで動くWebブラウザがroot URLにアクセスすると、受信したContent-Security-Policyヘッダーを実際に配信されたHTMLと比較します。問題がある場合は即座に"異論"を唱えます。テストシステムの外にある実際のWebブラウザでも同じように動作するでしょう。

session =
  Capybara::Session.new(
    :cuprite,
    CspApp.new(
      RubyEventStore::Browser::App.for(event_store_locator: -> { event_store }),
      "style-src 'self'; script-src 'self'",
    ),
  )

session.visit("/")

問題が発生したかどうかは、どうすれば知ることができるでしょうか?ページの一部が正常に読み込まれなくなることをアサーションでチェックできます。これは動的コンテンツのチェックに最適な方法です。

expect(session).to have_content("RubyEventStore v2.5.1")

しかしポリシー制約のせいでインラインCSSが読み込めない場合はどうでしょうか?HTMLコンテンツだけをチェックしていては検出できないかもしれません。以下のようにWebブラウザのログにエラーがあるかどうかを調べるという、より普遍性の高い方法も考えられます。

expect(logger.messages.select { |m| m["params"]["entry"]["level"] == "error" }).to be_empty

このloggerはどこから来るのでしょうか?Cupriteの場合はドライバに渡せます。loggerputsメソッドに応答しなければならないことになっています。1個のテストを実装するのに十分なテストは以下のような感じになるでしょう。

logger =
  Class.new do
    attr_reader :messages

    def initialize
      @messages = []
    end

    def puts(message)
      _, _, body = message.strip.split(" ", 3)
      body = JSON.parse(body)

      @messages << body if body["method"] == "Log.entryAdded"
    end
  end.new

Capybara.register_driver(:cuprite_with_logger) { |app| Capybara::Cuprite::Driver.new(app, logger: logger) }

Cuprite vs Selenium

ごく最近になって、ChromedriverはCupriteを動かすのに必須ではないことを知りました。このことだけで、試してみようという気になりました(依存性を減らしたくない人はいませんよね?)。それにChromedriverは更新が頻繁で、Chromeブラウザと常にバージョンを揃えて検疫(quarantine)を解除する必要がある点が依存関係として厄介です。

以前のRailsEventStoreではSeleniumとヘッドレスChromeを使っていました。このセットアップではブラウザログの検査方法が異なっていました。ロガーを明示的に渡す必要もなく、既にドライバのインターフェイスに公開されていました。

expect(session.driver.browser.manage.logs.get(:browser).select { |le| le.level == "SEVERE" }).to be_empty

SeleniumからCupriteへの移行はb6ec85cのコミットで最も詳しく見ることができます。この小さなサンプルセットでは、まだCupriteの欠点は見つかっていません。

Happy hacking!

お知らせ: 5600人以上のRailsエンジニアが購読しているメールマガジン

元記事末尾のフォームに登録いただくと、Arkencyのベテランプログラマーによる磨き抜かれた洞察や知恵を結晶したメールマガジンを配信いたします。

私たちは皆さんのメールボックスにスパムを決して送信することのないよう最大限に配慮しています。フォームを送信いただくと確認メールが届きます。

関連記事

Rails: フレームワークの機能をテストする価値がある場合(翻訳)

Rails: 5年前のアドバイザリーロック実装が突然おかしくなった話(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。