Railsの権限昇格(IDOR)対策|params改ざんで他人のデータが見える脆弱性を防ぐ方法

  • このエントリーをはてなブックマークに追加

スポンサーリンク

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選で土台を作りつつ、本記事の認可を最初から組み込んでおくのがおすすめです。

スポンサーリンク

  • このエントリーをはてなブックマークに追加