security について
Rails Security Expert は、入力処理からセッション、シークレット、ログまでを扱う Rails 開発向けのセキュリティガイダンススキルです。Rails の機能を実装またはレビューする開発者を対象に、標準の保護機能を維持し、機密情報の露出を抑える方法を示します。
主な機能
- 01CSRF と XSS の防止を案内
- 02パラメーター化クエリと入力検証を推奨
- 03strong parameters で書き込み項目を制限
- 04認証と認可の実装方法を提示
- 05セッション、シークレット、CSP、安全なログを扱う
活用シーン
- 01ユーザー入力を受け付けるコントローラーや API の実装
- 02ユーザー生成コンテンツやファイルアップロードの処理
- 03認証、セッション、アクセス制御の設定
- 04Rails のセキュリティ問題のレビューや修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill securityインストール後、Claude Code で/securityを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-threats.mdauthentication.mdcredentials.mdcsp-headers.mdcsrf.mdfile-uploads.mdscanning-tools.mdsessions.mdsql-injection.mdxss.md詳細
Rails Security Expert
デフォルトで安全な Rails コードを書きます。セキュリティ上のミスは、エージェントが犯し得る最も危険なミスです。XSS の脆弱性や SQL インジェクションがあれば、すべてのユーザーが瞬時に危険にさらされる可能性があります。判断に迷ったら、より安全な方法を選んでください。
基本方針
基本原則:
- デフォルトで安全にする — Rails には優れた組み込みの保護機能があります。明示的かつ文書化された正当な理由がない限り、決して無効にしないでください
- 多層防御 — 保護を重ねてください。単一の仕組みに頼らないでください
- 拒否リストより許可リストを使う — 入力検証、URL スキーム、HTML タグ、リダイレクト先には、常にブロックリストより許可リストを優先してください
- ユーザー入力を決して信頼しない — すべてのパラメーター、ヘッダー、Cookie、URL のセグメントは、安全が確認されるまで攻撃者が制御できるものとして扱ってください
- 安全側に倒して失敗させる — 認可の判断が曖昧な場合はアクセスを拒否してください。入力が疑わしい場合は拒否してください
- 露出を最小限にする — ログ、公開、保存する情報を減らしてください。機密性の高いパラメーターをフィルターし、スタックトレースの漏えいを避け、保存データを暗号化してください
このスキルを使う場面
- 認証または認可のロジックを追加する
- ユーザー入力を受け付けるコントローラーを書く
- ビューでユーザー生成コンテンツをレンダリングする
- API エンドポイントを構築する
- ファイルのアップロードまたはダウンロードを扱う
- セッション、Cookie、CSRF 保護を設定する
- シークレットと認証情報を管理する
- HTTP セキュリティヘッダーまたは CSP を設定する
- セキュリティの脆弱性についてコードをレビューする
- 報告された CVE またはセキュリティ上の問題を修正する
手順
ステップ 1:既存のセキュリティ対策を監査する
変更する前に、すでに導入されている対策を確認してください — 既存のセキュリティ対策を把握しておけば、意図せず弱体化させるのを防げます。
# Check CSRF configuration
rg "protect_from_forgery" app/controllers/
# Check strong params usage
rg "params\.permit\|params\.require" app/controllers/
# Check for dangerous patterns
rg "\.html_safe|raw\(|raw " app/views/ app/helpers/
rg "\.where\(\".*#\{" app/ --type ruby
rg "redirect_to.*params" app/controllers/
# Check security headers
rg "content_security_policy" config/
rg "force_ssl" config/
# Check filter_parameters
cat config/initializers/filter_parameter_logging.rb
# Check credentials setup
ls config/credentials* config/master.key 2>/dev/null
ステップ 2:CSRF 保護
CSRF 保護を決して無効にしないでください。 Rails ではデフォルトで有効になっており、クロスサイトリクエストフォージェリに対する主要な防御策です。
# ApplicationController — this should ALREADY be here
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
仕組み:
- GET 以外のすべてのリクエストには認証トークンが必要です
- Turbo/Ajax 用に、レイアウトの
<head>に<%= csrf_meta_tags %>を含めてください - トークン認証を使う API コントローラーでは CSRF をスキップできる場合があります。ただし、Cookie ベースのセッションを使わない場合に限ります(Cookie セッションと CSRF 保護の不在が組み合わさると、攻撃者がリクエストを偽造できます)
- ブラウザーセッションを提供するコントローラーでは、
skip_forgery_protectionを決して使わないでください
# API-only controllers with token auth — acceptable to skip
class Api::BaseController < ActionController::API
# No cookie session = no CSRF needed
# But MUST authenticate via Authorization header
end
# WRONG — disabling CSRF on a browser-facing controller
class PaymentsController < ApplicationController
skip_forgery_protection # NEVER DO THIS
end
ステップ 3:XSS の防止
Rails はデフォルトですべての ERB 出力を自動エスケープします。 これは XSS に対する主要な防御策です。これを回避すると、ユーザーが制御できる文字列によって、他のユーザーのブラウザーにスクリプトが挿入される可能性があります。
# SAFE — auto-escaped (default)
<%= user.name %>
<%= user.bio %>
# DANGEROUS — bypasses escaping. Only use for trusted, sanitized HTML
<%= raw(content) %> # AVOID
<%= content.html_safe %> # AVOID
<%== content %> # AVOID
# CORRECT way to render trusted HTML — sanitize first
<%= sanitize(user.bio, tags: %w[p br strong em a ul ol li], attributes: %w[href title]) %>
html_safe を使う際の注意:
- ユーザー入力に
.html_safeを決して呼び出さないでください。これは即座に XSS の脆弱性につながります - ユーザーデータを補間して作成した文字列に
.html_safeを決して呼び出さないでください。補間された値はエスケープを回避します .html_safeは、完全に自分で制御できる文字列リテラルにのみ使ってください- ヘルパーで HTML を構築する場合は、
tagビルダーまたはcontent_tagを使ってください。これらは自動でエスケープを処理します
# WRONG — XSS vulnerability
def greeting(user)
"<h1>Hello, #{user.name}</h1>".html_safe
end
# CORRECT — use tag helpers
def greeting(user)
tag.h1("Hello, #{user.name}")
end
# CORRECT — escape manually if you must build strings
def greeting(user)
"<h1>Hello, #{ERB::Util.html_escape(user.name)}</h1>".html_safe
end
sanitize() の使い方:
- 許可するタグと属性を明示的に指定してください。デフォルトの許可リストは、必要以上に広い場合があります
- リッチテキストには、サニタイズを自動で処理する Action Text を優先してください
ステップ 4:SQL インジェクションの防止
ユーザー入力を SQL 文字列に決して補間しないでください。 補間を使うと、攻撃者が任意の SQL を挿入できます。これにより、データベース内のあらゆるデータを読み取り、変更、削除されるおそれがあります。
# DANGEROUS — SQL injection
User.where("name = '#{params[:name]}'")
User.where("email LIKE '%#{params[:q]}%'")
Order.where("status = '#{status}' AND user_id = #{user_id}")
# SAFE — parameterized queries
User.where("name = ?", params[:name])
User.where("email LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")
Order.where(status: status, user_id: user_id)
# SAFE — named parameters
User.where("name = :name AND role = :role", name: params[:name], role: params[:role])
# SAFE — hash conditions (preferred)
User.where(name: params[:name])
LIKE クエリには特別な処理が必要です:
# WRONG — user can inject % and _ wildcards
User.where("name LIKE ?", "%#{params[:q]}%")
# CORRECT — sanitize LIKE wildcards
User.where("name LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")
見落としやすいインジェクション経路 — 次のメソッドも文字列をそのまま SQL に渡します:
order()— ユーザー入力をそのまま渡さないでください(攻撃者がサブクエリを挿入する可能性があります)pluck()、select()、group()、having()、joins()に文字列引数を渡す場合find_by_sql()、connection.execute()Arel.sql()— 信頼できる文字列にのみ使い、ユーザー入力には決して使わないでください
# WRONG — order injection
Post.order(params[:sort])
# CORRECT — permit-list of allowed columns
allowed = %w[created_at title updated_at]
direction = %w[asc desc].include?(params[:dir]) ? params[:dir] : "asc"
column = allowed.include?(params[:sort]) ? params[:sort] : "created_at"
Post.order("#{column} #{direction}")
ステップ 5:マスアサインメント保護
すべてのモデル書き込みでストロングパラメーターを使用してください。 生のパラメーターを使うと、攻撃者は管理者フラグ、ロール、関連付けなど、本来変更できない属性まで設定できてしまいます。
# WRONG — mass assignment vulnerability
User.create(params[:user])
@user.update(params[:user])
User.new(params.permit!) # permit! allows everything
# CORRECT — strong parameters
def user_params
params.require(:user).permit(:name, :email, :bio)
end
@user.update(user_params)
ストロングパラメーターのガイドライン:
- すべてのコントローラーに、プライベートな
*_paramsメソッドを定義します - ユーザーが設定できる属性のみを許可します
- ユーザー入力から
:role、:admin、:is_admin、:verifiedなどの権限属性を絶対に許可しないでください。権限昇格はこれによって発生します - ネストした属性には、明示的な許可が必要です
def post_params
params.require(:post).permit(
:title, :body, :published,
tags: [],
images_attributes: [:id, :url, :caption, :_destroy]
)
end
ステップ 6:認証
パスワードレスのメールコード認証(Basecamp、Fizzy、Herald): Rails 8 のパスワードジェネレーターは使用しないでください。magic-link-auth スキルに従ってください。人間にはコード、マシンにはセッション/保留中/アクセストークンを使用します。
パスワードベースのアプリ: Rails 8 以降の認証ジェネレーターをベースにします。
bin/rails generate authentication
認証の主な実践事項:
has_secure_passwordを使用します。bcrypt によるハッシュ化を適切に処理しますauthenticate_byを使用します。タイミングセーフなため、応答時間の測定によって有効なアカウントを列挙する攻撃を防ぎます- ログイン後に
reset_sessionを呼び出して、セッション固定攻撃(攻撃者がセッション ID を事前に設定する手口)を防ぎます - パスワードやメールアドレスの変更時には、現在のパスワードを必須にします
- 一般的なエラーメッセージを使用します。「メールアドレスまたはパスワードが無効です」(「ユーザーが見つかりません」ではありません)。具体的なメッセージは、どのアカウントが存在するかを明らかにします
# Session creation — correct pattern
class SessionsController < ApplicationController
def create
if user = User.authenticate_by(
email_address: params[:email_address],
password: params[:password]
)
reset_session # Prevent session fixation
start_new_session_for user
redirect_to root_path
else
redirect_to new_session_path, alert: "Invalid email or password."
end
end
end
ログイン試行のレート制限:
class SessionsController < ApplicationController
rate_limit to: 10, within: 3.minutes, only: :create
end
ステップ 7:認可
クエリを現在のユーザーの範囲に限定してください。 範囲を限定しないと、認証済みユーザーは URL の ID を変更するだけで、あらゆるレコードにアクセスできます。
# WRONG — any user can access any project by changing the ID
@project = Project.find(params[:id])
# CORRECT — scope to current user
@project = Current.user.projects.find(params[:id])
# CORRECT — use policy objects (Pundit, Action Policy, etc.)
@project = authorize Project.find(params[:id])
認可チェックリスト:
- すべてのクエリを、認証済みユーザーの権限の範囲に限定します
- すべてのアクションで認可を確認します(before_action)
- 保護対象のすべてのリソースに
before_action :authenticateを使用します - デフォルト拒否を優先します。ApplicationController の
before_actionと選択的なskip_before_actionの組み合わせは、オプトインのonly:より安全です(新しいアクションは誤って公開されず、デフォルトで保護されます)
class ApplicationController < ActionController::Base
before_action :require_authentication
# All controllers require auth unless explicitly skipped
end
class PublicPagesController < ApplicationController
skip_before_action :require_authentication, only: [:home, :about]
end
ステップ 8:セッションのセキュリティ
# config/environments/production.rb
config.force_ssl = true # HSTS + secure cookies + HTTPS redirect
# Session cookie settings (Rails defaults are good, but verify)
Rails.application.config.session_store :cookie_store,
key: "_myapp_session",
secure: Rails.env.production?,
httponly: true,
same_site: :lax
セッションのガイドライン:
- セッションには ID のみを保存します。Cookie 内の機密データは署名されていても読み取れます
- ログイン後に
reset_sessionを呼び出します(セッション固定を防ぎます) - セッションの有効期限(絶対タイムアウト+アイドルタイムアウト)を実装します
- 本番環境では
force_ssl = trueを設定します。これがないと、セッション Cookie が暗号化されていない HTTP で送信され、公共ネットワーク上で盗まれるおそれがあります
ステップ 9:シークレットと認証情報
シークレットには Rails credentials または環境変数を使用してください。 ハードコードされたシークレットはバージョン管理に含まれ、リポジトリにアクセスできる人(または流出したバックアップを入手した人)に API キーを知られてしまいます。
# Edit credentials
EDITOR=vim bin/rails credentials:edit
# Environment-specific credentials
EDITOR=vim bin/rails credentials:edit --environment production
# Access credentials
Rails.application.credentials.secret_key_base
Rails.application.credentials.dig(:aws, :access_key_id)
# Use bang version to fail loud if missing
Rails.application.credentials.some_api_key!
キー管理:
config/master.keyやconfig/credentials/*.keyは絶対にコミットしないでください。これらはすべてのシークレットを復号します.gitignoreに追加します(Rails ではデフォルトで設定されていますが、確認してください)- 本番環境では
ENV["RAILS_MASTER_KEY"]を使用します - エラーメッセージで認証情報をログに記録したり、公開したりしないでください
- シークレットが漏えいした可能性がある場合は、直ちにローテーションしてください
ステップ 10:コンテンツセキュリティポリシー
# 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.connect_src :self, :https
policy.frame_ancestors :none
policy.report_uri "/csp-violation-report-endpoint"
end
# Use nonces instead of 'unsafe-inline'
Rails.application.config.content_security_policy_nonce_generator =
->(request) { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
レイアウトに含める必要があります:
<head>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_include_tag "application", nonce: true %>
<%= stylesheet_link_tag "application", nonce: true %>
</head>
ステップ 11:ログのセキュリティ
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp,
:ssn, :credit_card, :card_number, :cvv, :authorization
]
ログへの記録を避ける項目(ログファイル、エラートラッカー、場合によってはサードパーティサービスにも表示されます):
- パスワード、トークン、API キー、シークレット
- クレジットカード番号、SSN、PII
- 機密データを含む可能性のあるリクエストボディ全体
ステップ 12:リダイレクトのセキュリティ
# WRONG — open redirect vulnerability
redirect_to params[:return_to]
# CORRECT — only allow internal paths
redirect_to safe_redirect_path(params[:return_to])
# Helper implementation
def safe_redirect_path(path)
uri = URI.parse(path.to_s)
if uri.relative? && uri.path.start_with?("/")
uri.path
else
root_path
end
rescue URI::InvalidURIError
root_path
end
# SIMPLEST — use url_from (Rails 7+)
redirect_to url_from(params[:return_to]) || root_path
ステップ 13:ファイルアップロードのセキュリティ
# Validate content type
class Document < ApplicationRecord
has_one_attached :file
validates :file, content_type: %w[application/pdf image/png image/jpeg],
size: { less_than: 10.megabytes }
end
# NEVER serve uploads from your app domain in production
# Use a separate domain/CDN or Active Storage's redirect mode
# NEVER use user-provided filenames for storage
# Active Storage handles this — it uses random keys
ファイルアップロードのガイドライン:
- コンテンツタイプとファイル拡張子の両方を検証します。攻撃者はどちらか一方を偽装できますが、両方を偽装することはほとんどありません
- 大容量アップロードによるサービス拒否を防ぐため、最大ファイルサイズを設定します
- アップロードファイルは Web ルートの外に保存します(Active Storage では自動的に行われます)
- アップロードされたファイルは絶対に実行しないでください
- 画像やメディアは非同期で処理します。画像処理ライブラリには脆弱性がある可能性があります
- 機密性の高いアプリケーションでは、マルウェアをスキャンします
クイックリファレンス
grep で検索する危険なパターン
# XSS: rg "\.html_safe|raw\(|<%==" app/
# SQL injection: rg '\.where\(".*#\{' app/ --type ruby
# Order injection: rg '\.order\(params' app/ --type ruby
# Mass assignment: rg 'params\.permit!' app/controllers/
# Open redirects: rg 'redirect_to.*params' app/controllers/
# CSRF disabled: rg 'skip_forgery_protection' app/controllers/
# Hardcoded secrets: rg '(api_key|secret|password|token)\s*=' app/ config/ -i
# Command injection: rg 'system\(|exec\(|`.*#\{' app/ --type ruby
正規表現に関する落とし穴
# WRONG — ^ and $ match LINE boundaries in Ruby, not string boundaries
validates :url, format: { with: /^https?:\/\/.+$/ }
# CORRECT — use \A and \z for string boundaries
validates :url, format: { with: /\Ahttps?:\/\/.+\z/ }
アンチパターン — いずれも脆弱性につながります
- ユーザー入力に対する
html_safeの使用 — 即座にXSSにつながります。攻撃者がスクリプトを注入し、他のユーザーのブラウザーで実行させます。 - SQLでの文字列補間 — SQLインジェクションにより、攻撃者がデータベース全体の読み取り、変更、削除を行えるようになります。
params.permit!— すべてのパラメーターを許可するため、攻撃者が管理者フラグ、ロール、その他任意の属性を設定できるようになります。- ブラウザー用コントローラーでの
skip_forgery_protectionの使用 — 悪意のあるサイトが、ユーザーに代わってフォームを送信できるようになります。 redirect_to params[:url]— オープンリダイレクトにつながります。攻撃者は、自分のサイトに見えるものの、フィッシングページにリダイレクトするリンクを作成できます。- ソースコードへのシークレットのハードコード — リポジトリにアクセスできる人(または流出したバックアップを入手した人)は、API キーを取得できます。
- ユーザー入力を使った
Kernel#open—open("| rm -rf /")はシェルコマンドを実行します。代わりにFile.open、URI.open、またはIO.openを使用してください。 - 本番環境での
force_sslの無効化 — セッション Cookie が平文で送信されるため、共有ネットワーク上で盗まれる可能性があります。 - 機密データのログへの記録 — ログはエラートラッカー、ログ集約ツール、開発者のマシンに保存されます。
- クライアント側の検証を信頼すること — 攻撃者は JavaScript を簡単に回避できます。実際の防御となるのはサーバー側の検証です。
セキュリティ監査チェックリスト
機能をリリースする前に、次の項目を確認してください。
- CSRF 保護が有効になっている(スキップされていない)
- すべてのコントローラーアクションでストロングパラメーターを使用している
- ユーザーが入力したコンテンツに
.html_safeやraw()を使用していない - すべての SQL でパラメーター化クエリを使用している
- クエリが現在のユーザーの範囲に限定されている(認可)
- リダイレクトが検証されている(オープンリダイレクトがない)
- 機密性の高いパラメーターがログから除外されている
- ファイルのアップロードが検証されている(種類、サイズ)
- ソースコードにシークレットがハードコードされていない
- 本番環境で
force_ssl = trueになっている - CSP ヘッダーが設定されている
- 認証でタイミング攻撃に耐性のある比較を使用している
- ログイン後にセッションがリセットされている
- ログインや機密性の高いエンドポイントにレート制限が設定されている