mcpField
R
rails-security-multitenancy

rails-security-multitenancy

github.com/marckohlbruggeセキュリティ73059 回インストール20 週間前に更新
リポジトリ

rails-security-multitenancy について

セキュリティを重視する Rails 開発とテナント境界のレビュー向けスキルです。Fizzy と Campfire の実践をもとに、テナント分離、認証、認可、SSRF 対策をまとめています。

主な機能

  1. 01テナントとユーザーでクエリを限定
  2. 02認証とレート制限の強化
  3. 03ユーザー指定 URL への防御
  4. 04ブロードキャストとセッションの分離
  5. 05CSRF、キャッシュ、認可への対応

活用シーン

  1. 01マルチテナント Rails のデータアクセスをレビュー
  2. 02マジックリンクや API トークン認証を設計
  3. 03Webhook やプッシュ URL を保護
  4. 04テナント間の配信とストレージ認可を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marckohlbrugge/37signals-skills --skill rails-security-multitenancy

インストール後、Claude Code で/rails-security-multitenancyを入力して読み込まれたか確認します。

詳細

Rails のセキュリティとマルチテナント

セキュリティに慎重を要する Rails の作業や、テナント境界のレビューに使用します。Fizzy(パスベースのマルチテナント SaaS)と Campfire(シングルテナント、ボット API)のパターンを紹介します。

基本ルール

  • すべてのテナントデータへのアクセスを、テナント/ユーザーの所有権境界でスコープします。
  • テナントを考慮するフローでは、Model.find(params[:id]) をそのまま信頼しないでください。
  • リアルタイムのブロードキャストとストリーム名を、テナント/アカウントでスコープします。
  • 認証エンドポイントや不正利用されやすいエンドポイントにレート制限を設定します。
  • ユーザーが指定した URL は信頼できない入力として扱います。
  • アクセスを証明できない場合は、アクセスを拒否します(head :forbidden)。

テナントアーキテクチャ(パスベース、Fizzy 方式)

  • ミドルウェアが PATH_INFO からアカウントのプレフィックスを SCRIPT_NAME に抽出し、アカウントを読み込み、リクエストを Current.with_account(account) でラップします。URL ヘルパーは、ActiveStorage や script_name: を使った Webhook ペイロード内の URL も含め、テナントに適した状態を自動的に保ちます。
  • 3 層の認証:Identity → Session → アカウントスコープの User。 セッションは Identity に紐づきます。各リクエストで identity.users.find_by(account: Current.account) により Current.user を解決します。アカウント A の有効なセッションがアカウント B で利用されることは決してありません。ActionCable の Connection#connect でも同じ解決処理を適用します。
  • Current のセッターは連動します。session を設定すると identity が解決され、identity を設定すると現在のアカウントに対する user が解決されます。
  • 認証ルート(ログイン、サインアップ、マジックリンク)では、テナントスコープを明示的に無効化(disallow_account_scope)し、テナントプレフィックス付き URL からリダイレクトします。
  • 同時に複数のテナントへのログインをサポートする場合は、セッション Cookie のパスをアカウントのスラッグに設定します(path: account.slug)。
  • 定期ジョブはリクエストコンテキスト外で実行されるため、テナントを明示的に反復処理します(with_each_tenant)。Current.account をジョブのペイロードにシリアライズします(rails-jobs を参照)。
  • Rack::MockRequest を使ってテナントミドルウェアを単独でテストします。結合テストでは、フィクスチャのアカウントから default_url_options[:script_name] を設定します。

スコープ付き検索(多層防御)

パラメーターで選択するのは、すでに認可された集合内の どの レコードかであり、アクセス権を確立するものではありません。

@card = Current.user.accessible_cards.find_by!(number: params[:card_id])   # access graph
@membership = Current.user.memberships.find_by!(room_id: params[:room_id]) # join model
@user = Current.account.users.find(params[:user_id])                       # tenant association
  • シングルテナントのコードでも関連付け経由でスコープし、誤った ID は自然に 404 になります。
  • 公開共有では、不透明なトークン(Publication レコードでの has_secure_token :key)を使用し、内部 ID は使いません。
  • ActiveStorage では、Blob をアカウントに紐づけ、ドメインモデルを通じて Blob/表現コントローラーを認可します(blob → attachment → record.accessible_to?(user))。公開コンテンツには、明示的な publicly_accessible? 経路を設けます。
  • アクセス権を取り消す際は、スコープ付きの非同期ジョブで派生データ(メンション、通知、ウォッチ)を削除し、境界を越えた状態が残らないようにします。

認証の強化

  • マジックリンク/コードは、1 回限り(使用時に行を削除)、有効期間を短くし、secure_compare で比較し、検証済みの保留中認証 Cookie を通じてメールアドレスに紐づけます。
  • アカウントの有無を推測させないため、未知のメールアドレスにも実際のアカウントと同じ偽のフロー/UX を返します。
  • API トークンには HTTP メソッド単位の権限を設定します(読み取り専用トークンでは POST できません)。生成したシークレットは有効期間の短いメッセージ検証器(約 10 秒)を使って一度だけ表示し、それ以降は表示しません。
  • ボット/自動化の認証は明示的なモードにします。ボットキー認証の場合に限り CSRF をスキップし、デフォルトではすべての場所でボットを拒否します(deny_bots と allow_bot_access only:)。
  • Rails の組み込み機能でレート制限を設定し、レスポンスを各エンドポイントの仕様に合わせます。
rate_limit to: 10, within: 15.minutes, only: :create,
  with: -> { redirect_to ..., alert: "Try again in 15 minutes." }
  • セッションの記録処理の書き込みを抑制します(last_active_at の更新は最大でも 1 時間に 1 回)。
  • パスワード以外の機密パラメーターもフィルタリングします。メッセージ本文、プッシュ通知エンドポイント、トークンなどが対象です。

SSRF 防御の基本方針

Webhook、プッシュエンドポイント、URL プレビューなど、ユーザーの影響を受ける URL には、次の対策を適用します。

  • リクエスト前に DNS を解決し、接続先 IP を検証します。ループバック、プライベート、リンクローカル、IPv4-mapped-IPv6 の範囲をブロックします(リンクローカルはクラウドのメタデータを指します)。
  • DNS リバインディングを防ぐため、リクエストを検証済み IP に固定します(Net::HTTP.new(host, port, ipaddr: resolved_ip))。
  • 作成時と実行時の両方で検証します。
  • リダイレクトの各ホップで、再解決して再検証します。リダイレクトチェーンは典型的な回避手段です。
  • メモリ DoS を防ぐため、レスポンスサイズに上限を設けます(content-length の事前チェックとチャンク読み込み時の上限)。
  • 接続先の範囲が既知の場合は、許可リストを追加します(Web Push の場合、許可されたベンダーのホストサフィックスに加え、パブリック IP への解決を確認します)。

CSRF とキャッシュ

  • フォームや CSRF トークンをレンダリングするページは、HTTP キャッシュに保存しないでください。
  • オリジンチェックに加えて Sec-Fetch-Site(same-origin/same-site)の検証を追加します。まずレポート専用モードで導入し、Vary ヘッダーに Sec-Fetch-Site を追加します。
  • 厳格な最低限のポリシー(object_src :none、base_uri :none、frame_ancestors :self)を設定した CSP を適用します。適用前に、report_uri を指定したレポート専用モードで検証します。
  • プライベートアプリでは、X-Robots-Tag: none を送信します。

認可のデフォルト

  • モデルに述語メソッドを定義します(card.editable_by?(user)、user.can_administer_board?(board))。コントローラーで確認し、head :forbidden を返します。
  • 散在する条件分岐ではなく、宣言的なコントローラーマクロ(allow_unauthenticated_access、ensure_can_administer)を使用します。
  • 共通のガードロジックは、小さな Concern に集約します。

不正利用への対応

  • ユーザーを BAN する際は、関連する処理も実施します。ユーザーのセッション IP を BAN に変換し、ActionCable をリモートから切断し、セッションを削除します。コンテンツの削除は UI へのブロードキャストを伴う非同期処理で実行します。
  • IP BAN は、データを変更するリクエスト(POST/PUT/PATCH/DELETE)にのみ適用します。
  • BAN 対象がパブリック IP であることを検証します。ループバック/プライベート範囲を BAN できるようにしてはいけません。
  • 無効化されたユーザーを切断します:ActionCable.server.remote_connections.where(current_user: user).disconnect(reconnect: false)。

受信 Webhook

  • 署名を検証し(例:Stripe::Webhook.construct_event)、ペイロードの内容や順序を信用せず、送信元の API から正規の状態を再取得します。

危険信号

  • テナントでスコープされていないブロードキャストチャンネルやストリーム名。
  • 所有権を確認せず、リクエストパラメーターから推定されるテナントデータ。
  • アカウントのスコープを適用せず、セッションからグローバルな User を検索する処理(ID とユーザーの混同)。
  • 未検証の接続先への Webhook またはフェッチリクエスト。再検証せずにリダイレクトを追跡する処理。
  • ドメイン認可を迂回する ActiveStorage URL。
  • コントローラー全体に場当たり的な条件分岐として隠されたセキュリティ制御。
  • 管理 UI で常時表示されるシークレット。
  • レート制限のない認証エンドポイント。

コメント