MENU
カテゴリー

【Rails】CSP(コンテンツセキュリティポリシー)の設定方法を完全解説|XSS対策の基本

  • URLをコピーしました!
目次

はじめに

Webアプリケーションのセキュリティ対策として、XSS(クロスサイトスクリプティング)は今も昔も重要な課題だ。Railsにはデフォルトでいくつかのセキュリティ機能が備わっているが、その中でも**CSP(Content Security Policy)**は見落とされがちなわりに、効果の高い対策のひとつである。

本記事では、CSPとは何か、なぜ必要なのか、そしてRailsで実際にどのように設定するかを順を追って解説する。

CSPとは何か

CSP(Content Security Policy)とは、ブラウザに対して「このページではどこからリソースを読み込んでいいか」を指示するセキュリティの仕組みだ。

HTTPレスポンスヘッダーに Content-Security-Policy を付与することで実現する。

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com

上記の例では、「スクリプトは自サイトと cdn.example.com からのみ読み込んでよい」と指示している。攻撃者が悪意のあるスクリプトを埋め込もうとしても、許可されていないドメインからのスクリプトはブラウザがブロックする仕組みだ。

なぜCSPが必要なのか

Railsはデフォルトで html_escape によってHTMLエスケープを行うため、基本的なXSS対策はされている。しかし、それだけでは不十分なケースがある。

たとえば以下のような状況だ。

  • rawhtml_safe を使った箇所に脆弱性が生まれた場合
  • サードパーティのJavaScriptライブラリに問題があった場合
  • ユーザー入力を誤ってそのまま出力してしまった場合

CSPはこうした「エスケープをすり抜けてしまったスクリプト」を最終的にブラウザ側でブロックする多層防御の最後の砦として機能する。

RailsでCSPを設定する方法

Rails 5.2以降、CSPを簡単に設定できる content_security_policy のDSLが標準搭載されている。設定ファイルは以下のパスに存在する。

config/initializers/content_security_policy.rb

rails newで生成した場合、このファイルはコメントアウトされた状態で存在しているはずだ。

基本的な設定例

ruby

# config/initializers/content_security_policy.rb

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https

  # 違反レポートの送信先(任意)
  # policy.report_uri "/csp-violation-report-endpoint"
end

各ディレクティブの意味は次の通りだ。

ディレクティブ意味
default_src他のディレクティブが未指定の場合のデフォルト
script_srcJavaScriptの読み込み元
style_srcCSSの読み込み元
img_src画像の読み込み元
font_srcフォントの読み込み元
object_srcFlash等のプラグインの読み込み元
connect_srcfetch・XHR・WebSocketの接続先

よく使うキーワード

設定値には以下のようなキーワードが使える。

  • :self → 同一オリジンのみ許可
  • :none → すべて禁止
  • :https → HTTPSのすべてのドメインを許可
  • :data → data:スキームを許可(画像のインライン埋め込み等)
  • :unsafe_inline → インラインスクリプト・スタイルを許可(非推奨)
  • :unsafe_eval → evalを許可(非推奨)
  • 文字列で "https://cdn.example.com" のように特定ドメインを指定することも可能

:unsafe_inline:unsafe_eval はCSPの効果を大幅に下げるため、なるべく使わないのが原則だ。

インラインスクリプトを許可したい場合はnonceを使う

Railsを使っていると、ビューにインラインのJavaScriptを書くことがある。その場合、:unsafe_inline を使わずに nonce(ノンス) を活用するのがベストプラクティスだ。

nonceとは、リクエストごとにランダムに生成されるトークンのことで、そのトークンがついたスクリプトだけを実行許可する仕組みだ。

nonceの設定方法

ruby

# config/initializers/content_security_policy.rb

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.script_src  :self, :https

  # nonceを生成する
  policy.script_src *policy.script_src, :strict_dynamic, "'nonce-#{request.content_security_policy_nonce}'"
end

# nonceを自動生成する設定
Rails.application.config.content_security_policy_nonce_generator = -> (request) { SecureRandom.base64(16) }

# nonceを適用するディレクティブを指定
Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

ビュー側では nonce: true をつけるだけで自動的にnonceが付与される。

erb

<%= javascript_tag nonce: true do %>
  console.log("このスクリプトは許可される");
<% end %>

または javascript_include_tag でも同様だ。

erb

<%= javascript_include_tag "application", "data-turbo-track": "reload", nonce: true %>

レポートモードで安全にCSPを導入する

本番環境に突然CSPを適用すると、既存の機能が壊れるリスクがある。そのために用意されているのがレポートモードだ。

レポートモードでは、CSPのルールに違反したリクエストをブロックせず、指定したエンドポイントにレポートを送信するだけになる。これにより、実際に適用する前に「どこで違反が起きるか」を把握できる。

ruby

# config/initializers/content_security_policy.rb

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.script_src  :self, :https
  policy.report_uri  "/csp-violation-report"
end

# report-onlyモードにする
Rails.application.config.content_security_policy_report_only = true

content_security_policy_report_onlytrue にすると、Content-Security-Policy ではなく Content-Security-Policy-Report-Only ヘッダーが付与される。ブラウザはブロックせずにレポートだけ送信する。

レポートを受け取るコントローラーの例は以下だ。

ruby

class CspReportsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    report = JSON.parse(request.body.read)
    Rails.logger.warn("CSP Violation: #{report.inspect}")
    head :ok
  end
end

ruby

# config/routes.rb
post "/csp-violation-report", to: "csp_reports#create"

コントローラー・アクション単位での上書き

サービス全体ではなく、特定のコントローラーやアクションだけ別のCSPポリシーを適用したい場合は content_security_policy メソッドを使う。

ruby

class EmbedController < ApplicationController
  # このコントローラーだけCSPを無効化する
  content_security_policy false

  def show
    # ...
  end
end

ruby

class ApiController < ApplicationController
  # このコントローラーだけ別のポリシーを適用する
  content_security_policy do |policy|
    policy.default_src :none
    policy.connect_src :self
  end

  def index
    # ...
  end
end

TurboやStimulusを使っている場合の注意点

HotWireを使っているRailsアプリでCSPを設定する際、TurboやStimulusのインライン処理が引っかかることがある。

基本的にはnonceを正しく設定すれば問題ないが、import-maps を使っている場合は script-src:unsafe-inline が必要になるケースもある。

できればImportmapよりjsbundling-rails(esbuildやVite)と組み合わせて外部ファイルとして配信し、nonceで管理するのが安全だ。

実際にCSPヘッダーを確認する方法

設定が正しく反映されているかは、ブラウザのデベロッパーツールで確認できる。

  1. デベロッパーツールを開く(F12 or ⌘+Option+I)
  2. 「Network」タブを選択
  3. ページをリロードして最初のリクエストをクリック
  4. 「Headers」タブで Content-Security-Policy を探す

また、curl でも確認できる。

bash

curl -I https://your-app.com | grep -i content-security-policy

まとめ

本記事で解説した内容を整理する。

  • CSPはXSS対策の多層防御として非常に有効な仕組みだ
  • Rails 5.2以降は config/initializers/content_security_policy.rb で簡単に設定できる
  • インラインスクリプトは :unsafe_inline を使わずnonceで対応するのがベストプラクティスだ
  • 本番導入前にはレポートモードで問題箇所を洗い出すことを強くすすめる
  • TurboやImportmapを使っている場合は追加の考慮が必要だ

セキュリティ対策は「やって当たり前」になるまで意識し続けることが大切だ。CSPを正しく設定することで、万が一XSS脆弱性が生まれても被害を最小限に抑えることができる。ぜひ既存のRailsプロジェクトにも導入してみてほしい。

よかったらシェアしてね!
  • URLをコピーしました!
目次