Railsは、SQLインジェクションやXSSのような代表的な攻撃は、かなりの部分をフレームワーク側で防いでくれます。では「ログイン中のユーザーが、他人のデータを勝手に見たり書き換えたりできてしまう」問題はどうでしょうか?
これは権限昇格(Privilege Escalation)、Webの文脈ではIDOR(Insecure Direct Object Reference/安全でない直接オブジェクト参照)と呼ばれる脆弱性で、Railsが自動では守ってくれない代表例です。自分でコードを書いて防ぐ必要があります。この記事では、なぜ起きるのか・どう直すのか・どうやって検出するのかを、コード付きで整理します。
権限昇格(IDOR)とは
権限昇格とは、本来アクセスできてはいけないデータに、不正な操作でアクセスできてしまう脆弱性です。もっともよくあるのが、URLの id などのパラメータを書き換えて、他人のリソースを参照してしまうパターンです。
公式のRailsガイドにも、次のように書かれています。
たった1つのパラメータを変更しただけで、ユーザーが不正な権限でアクセス可能になることがあります。パラメータは、たとえどれほど難読化・隠蔽したとしても、変更される可能性が常にあることを肝に銘じてください。
Railsセキュリティガイド 6.6 権限昇格
危険なコードと攻撃のされ方
たとえば、ユーザーが自分のプロジェクトを管理するアプリを考えます。プロジェクト詳細をこう書いたとしましょう。
# 全プロジェクトから id で探している(危険)
@project = Project.find(params[:id])一見おかしく見えませんが、params[:id] はURLに対応しているため、ユーザーが自由に書き換えられます。/projects/1 を /projects/50 に変えるだけで、本来は他人のものであるプロジェクトまで参照できてしまいます。
ログインは突破していなくても(=認証は通っていても)、「このユーザーがそのデータを操作してよいか」という認可のチェックが抜けているのが問題です。認証(本人確認)と認可(権限の確認)は別物だと意識すると、対策の場所が見えてきます。
解決方法:取得する範囲をユーザーに絞る
いちばん確実なのは、最初からログインユーザーの持ち物の中だけを検索することです。current_user が使えるなら、関連(アソシエーション)経由で取得します。
# ログインユーザーのプロジェクトの中から id で探す(安全)
@project = current_user.projects.find(params[:id])こうすると、悪意のあるユーザーが id を50に書き換えても、検索対象が「自分のプロジェクト」に限定されているため見つからず、ActiveRecord::RecordNotFound が発生します。Railsはこれを自動で404として返すので、他人のデータの存在すら相手に悟らせません。
この「スコープを絞る」書き方は、find だけでなく update や destroy でも同じです。id を受け取る全てのアクションで、誰の持ち物かを必ず起点にする——これを徹底するだけで、IDORの大半は防げます。
認可ロジックが複雑なら gem を使う
「管理者なら全件見られる」「共同編集者だけ編集できる」のように条件が増えてくると、スコープを絞るだけでは表現しきれません。そんなときは認可用のgemでルールを一元管理します。
Pundit(おすすめ)
Punditは、リソースごとに「ポリシークラス」を作り、アクション単位で許可・拒否を素のRubyで書けるgemです。ロジックがコントローラから切り離されるので、テストもしやすくなります。
class ProjectPolicy < ApplicationPolicy
# 管理者か、自分が所有するプロジェクトだけ参照を許可
def show?
user.admin? || record.user_id == user.id
end
endコントローラ側では authorize @project と書くだけで、上記 show? が呼ばれ、拒否時は例外が上がります。
CanCanCan
CanCanCanは、1つの「Abilityクラス」に全権限ルールをまとめて書くスタイルのgemです。なお、元の cancan はすでにアーカイブ(開発終了)されているので、後継のコミュニティ版 CanCanCan を使います。
class Ability
include CanCan::Ability
def initialize(user)
can :read, Project, public: true
return unless user.present? # ログインユーザーは自分のプロジェクトを閲覧可
can :read, Project, user: user
return unless user.admin? # 管理者は全プロジェクトを閲覧可
can :read, Project
end
endどちらを選ぶかは好みですが、ポリシーを小さく分けたいならPundit、権限を1か所で俯瞰したいならCanCanCan、というのが目安です。
Brakemanで自動的に検出する(Rails 8は標準装備)
人の目だけに頼らず、Brakemanでコードを静的スキャンするのも有効です。BrakemanはXSS・SQLインジェクション・不正なマスアサインメント・安全でないリダイレクトなどの潜在リスクを、コードを実行せずに検出してくれます。
うれしいことに、Rails 7.1以降(Rails 8の既定)では rails new した時点で Brakeman が同梱されます。Gemfileに gem "brakeman", require: false が入り、.github/workflows/ci.yml にもセキュリティスキャン用のジョブ(scan_ruby)が自動生成されるため、以前のように自分でCI設定を書く必要はほぼありません。
手元でサッと実行するなら、次のコマンドだけ覚えておけば十分です。
# プロジェクト全体をスキャン(Rails標準のCIジョブも中でこれを実行している)
bin/brakeman --no-pager古いRailsアプリで同梱されていない場合だけ、bundle add brakeman --group development で追加してください。
まとめ
- 権限昇格(IDOR)は、
params[:id]の書き換えで他人のデータにアクセスできてしまう脆弱性。Railsは自動では守ってくれない - 基本対策は
current_user.projects.find(params[:id])のように取得範囲をユーザーに絞ること(不正なidは404になる) - 条件が複雑なら Pundit / CanCanCan で認可ルールを一元管理する
- Brakeman(Rails 8標準)で自動検出。
bin/brakemanをCIで回すと安心
認証そのものの実装をどうするか迷っている方は、Rails 8の認証ジェネレータとDeviseはどっちを使う?もあわせて読むと、認証と認可の役割分担がはっきりします。Deviseを使っているならDeviseをカスタマイズしてプロフィール更新とパスワード変更を分ける方法も参考になります。これから機能を作る段階の方はrailsブログ機能の実装方法7選で土台を作りつつ、本記事の認可を最初から組み込んでおくのがおすすめです。
スポンサーリンク
