TL;DR 忙しい人のための要約
- Rails アプリのセキュリティチェックに Brakeman は便利
- ただし Brakeman は コードパターンで検出できる脆弱性 が中心
- 認証・認可の漏れ は Brakeman では検出されにくい
- RSpec で Request spec を書きながら controller を見直していたら、認証必須にすべきアクションが素通りになっていたことを発見
- 静的解析 + テストの合わせ技 でセキュリティの網羅性が上がる、という学び
はじめに
個人でアプリを開発しながらプログラミングを学習しています。
パンの消費期限や在庫管理をサポートするアプリを開発中です!
使用技術
Ruby 3.2.2
Rails 7.1.6
Docker
その他略
本格運用前に脆弱性チェックをしようと思い、まずは定番の Brakeman を回しました。
※Brakemanの説明は後ほど記述します。
結果はErrors0件で、警告で出ているのはRubyとrailsのEOL(End Of Life:サポート終了)のものでした。
ひとまずコード由来のセキュリティ警告はなさそうなので、そこまで悪くないのかなと思いました。
ところがその後、RSpec の Request spec を書こうとして controller を順に眺めていたら、Brakeman では検出されない脆弱性 に気づきました。
初心者がやりがちなミスをしてしまったのでその体験を共有します。
1. Brakeman とは
Brakeman は Rails 専用の静的解析ツールです。「静的解析」とは、プログラムを 実行せずに ソースコードを読んで問題を見つける方法のこと。
代表的な検出対象:
- SQLインジェクション(生のSQL文字列に変数を埋め込む書き方)
-
XSS(
rawやhtml_safeの危険な使い方) - CSRF(保護機構の無効化)
- Mass Assignment(Strong Parameters の漏れ)
- 依存ライブラリのEOL(Ruby/Rails 自体のサポート切れ)
- その他多数
導入方法は
Gemfileに追加し、インストール、実行するだけです。
# Gemfile
group :development, :test do
gem "brakeman", require: false
end
bundle install
bundle exec brakeman
最低限まずこれを入れるだけでも、典型的な脆弱性がないか確認できます。
2. Brakeman で「検出しにくい」もの
Brakeman は ロジックや意図に依存する脆弱性 は得意ではありません。
代表例
認証と認可の漏れ
認証(Authentication):あなたは誰? → ログインしてるか?
認可(Authorization) :あなたは何ができる? → そのリソースを操作する権限あるか?
Brakeman は「ログインを必須にすべき controller に authenticate_user! が書かれていない」のような 設計判断にまたがる問題 は基本検出しません。
なぜならツールは「この controller がログイン必須なのか、公開ページなのか」までは判断できないからです。
IDOR(Insecure Direct Object Reference)
※breadにはパンの情報(消費期限や入り数)が入っています。
# Brakeman は警告しない
def edit
@bread = Bread.find(params[:id]) # 他人のbreadも取得できてしまう
end
このコード自体は完全に正しい Rails コード。Brakeman 視点では「find を使っている、正常」となります。
しかし運用上は「自分のbreadだけ編集できる」べきで、本当は
@bread = current_user.breads.find(params[:id])
にする必要があります。
これらのルール決めは設計者が決めることなのでBrakemanでは検出しにくいです。
3. 実体験:RSpec を書いていて見つけた認証漏れ
ここからが本題の体験談になります。
状況
Rspecを実装するため、すでに実装済みのcontrollerを確認していました。
そして次のような controller に出会いました。
class NotificationSettingsController < ApplicationController
before_action :reject_guest_user
def show
end
end
ぱっと見、問題はなさそうです。ゲストユーザーは通知設定が行えない ようにする before_action が入っています。
通知機能自体を会員登録を必須とした設計にした為です。
しかしよく見ると、ゲストユーザーは弾いていますが、そもそもの未会員ユーザーを弾いていない ことに気づきました。
# ApplicationController内
def reject_guest_user
if user_signed_in? && current_user.guest?
redirect_to guest_signup_prompt_path,
alert: "この機能は会員登録が必要です"
end
end
reject_guest_user の中身を見ると、user_signed_in? が false(未ログイン)のときは 何もしない 構造になってしまっていました。
未ログイン → reject_guest_user は素通り → show が動く ❌
ログイン中(通常)→ reject_guest_user は素通り → show が動く ✅
ログイン中(ゲスト)→ reject_guest_user がブロック → サインアップ誘導 ✅
未ログインでも通知設定画面が見れてしまう 状態だったのです。
アクセスしてくるユーザーは、ゲストか会員登録したユーザーのどちらかであるという思い込みがあったため抜けてしまいました。
問題
このままだと、認証が必要なページに未ログインでアクセスできる状態 ということになります。
これは設計通りと言えません。
修正
慌ててauthenticate_user! を before_action に追加しました。
class NotificationSettingsController < ApplicationController
before_action :authenticate_user! # ← 1行追加
before_action :reject_guest_user
def show
end
end
before_action は 書いた順番に実行される ので、認証チェックを先に置きます。
未ログイン → authenticate_user! でログイン画面へリダイレクト ✅
ログイン中(通常)→ どちらの before_action も素通り → show が動く ✅
ログイン中(ゲスト)→ reject_guest_user がブロック → サインアップ誘導 ✅
テストで担保
修正後の挙動を Request spec で固定します。
- 未ログインの場合→ログイン画面にリダイレクト
- ログイン中の場合→ページを表示
- ゲストとしてログイン中の場合→会員登録を促す画面へ
の3パターンをテストします。
テスト実装時にコードを見直したことで、問題に気づいて修正できました。
4. 教訓:静的解析だけでは足りない
この体験で得た学びを整理します。
学び①:認可と認証を区別する
- 認証(authenticate_user!):そもそもログインしてるか
- 認可(current_user.xxx で絞り込む等):そのリソースを触る権限があるか
両方が必要で、片方だけでは不十分。Brakeman は両方とも完全には見てくれないので自分で確認が必要。
学び②:Rspecの実装をすることで、ミスを見つけることがある
Rspecは機能実装と違って、地味で面倒というイメージがあったのですが、しっかりやらないとこういうミスに気がつけないということを体験できました。
なんでもAI任せにせず、セキュリティホールに自分が気づけるように、コードや設計を読み解く力をつけていきたいです。
