はじめに
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対策はされている。しかし、それだけでは不十分なケースがある。
たとえば以下のような状況だ。
rawやhtml_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_src | JavaScriptの読み込み元 |
style_src | CSSの読み込み元 |
img_src | 画像の読み込み元 |
font_src | フォントの読み込み元 |
object_src | Flash等のプラグインの読み込み元 |
connect_src | fetch・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_only を true にすると、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ヘッダーを確認する方法
設定が正しく反映されているかは、ブラウザのデベロッパーツールで確認できる。
- デベロッパーツールを開く(F12 or ⌘+Option+I)
- 「Network」タブを選択
- ページをリロードして最初のリクエストをクリック
- 「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プロジェクトにも導入してみてほしい。
