本文へスキップ

Firebaseセキュリティルールの例:攻撃した11本

Markdownで表示Claudeで開くChatGPTで開くPerplexityで開く

質問もコピーされます。チャットが空で開いた場合は貼り付けてください。

TL;DR

11通りのFirestoreルールセットに、ゲスト、匿名ユーザー、サインイン済みの3人のユーザーとして同じリクエストを送りました。request.auth != nullでは、見知らぬ人が全員の注文を読み、変更し、削除できました。resourceだけでオーナーを確認するルールでは、ユーザーが注文を他人に渡せました。広い{document=**}のmatchが1つあるだけで、正しいオーナールールがあるコレクションが開いてしまいました。そして、ユーザーが書き込めるドキュメントにロールを保存していると、1回のリクエストで管理者になれました。それぞれの修正を下に載せてあり、修正後のルールは同じリクエストに耐えました。

Firebaseアプリには、ブラウザとデータベースの間に自分のサーバーがありません。プロジェクトの設定はページに含まれて配信され、それを持つ誰かとあなたのデータの間にあるのはセキュリティルールだけです。そこで、正しく見えるルールの一覧ではなく、攻撃を並べたルールの例が欲しくなりました。

テスト方法。 Firebase CLI 15.34.2のFirebase Local Emulator Suiteを、2026年11月3日に使いました。各ルールセットを読み込み、データをリセットし、Authエミュレーターの本物のトークンでFirestore REST APIを呼びました。トークンなし、匿名ユーザー、ユーザーAとB、そしてサインアップしただけの見知らぬ人です。データは架空で、注文a1とa2はAのもの、b1はBのものです。

どのFirebaseルールが漏らす?

ルール リクエスト 結果
if true(テストモード) ゲストが注文を一覧する 200、3件すべて
if true(テストモード) ゲストがBの注文を削除する 200
request.auth != null 匿名ユーザーが注文を一覧する 200、3件すべて
request.auth != null 見知らぬ人がAの注文を削除する 200
resourceだけのオーナー確認 AがBの所有する注文を作成する 200
resourceだけのオーナー確認 Aが自分の注文のownerIdを変更する 200
オーナールールと{document=**}のread AがBの注文を読む 200
ユーザーが自分のusersドキュメントに書ける Aがrole: adminを設定し、Bの注文を読む 200

テストモードは実際に何を開く?

match /{document=**} {
  allow read, write: if true;
}

すべてを、全員に開きます。トークンのないゲストが3件の注文をすべて一覧し、1件を削除し、新しい1件を作成しました。アカウントも漏えいしたシークレットも要りません。必要なのはプロジェクトIDだけです。

テストモードがよく使う期限付きの版は、別の壊れ方をします。if request.time < timestamp.date(2026, 1, 1)でその日付が過去なら、ゲストは403になり、ユーザーAが自分の注文を読むときも403でした。漏えいは終わりますが、アプリも終わります。

request.auth != nullで十分?

いいえ。リクエストの背後に何かしらのアカウントがある、というだけです。

match /orders/{id} {
  allow read, write: if request.auth != null;
}

ゲストは403でした。だから、このルールは簡単なテストでは安全に見えます。ところが、サインアップしただけの見知らぬ人が、3件の注文をすべて一覧し、Bの注文の合計を変更し、Aの注文を削除し、どれも200でした。匿名ユーザーも3件すべてを一覧できました。サインアップが開放されているなら、このルールは「登録する手間をかけた人なら誰でも」という意味になります。Firebaseのドキュメントも、これをよくある危険なパターンとして挙げています。

オーナー確認にresourceとrequest.resourceの両方が要るのはなぜ?

resourceは保存されている状態のドキュメントです。request.resourceは書き込み後になるドキュメントです。前者だけを見るルールは、今その行が誰のものかを確認するだけで、それを何に変えるかについては何も言いません。

match /orders/{id} {
  allow read, update, delete: if resource.data.ownerId == request.auth.uid;
  allow create: if request.auth != null;
}

読み取りは正しく見えました。Aは自分の注文で200、Bの注文で403でした。書き込みはそうではありませんでした。AはownerIdをBにした新しい注文を作成でき、200でした。さらに自分の注文のownerIdをBに変更でき、これも200で、その後はBがそれを読めました。注文が請求書や残高を意味するアプリでは、ユーザーが他人のアカウントに書き込んでいることになります。

次のバージョンは、これをすべて拒否しました。

match /orders/{id} {
  allow read, delete: if request.auth != null
    && resource.data.ownerId == request.auth.uid;
  allow create: if request.auth != null
    && request.resource.data.ownerId == request.auth.uid;
  allow update: if request.auth != null
    && resource.data.ownerId == request.auth.uid
    && request.resource.data.ownerId == resource.data.ownerId;
}
Aのリクエスト 結果
自分の注文を読む 200
Bの注文を読む、更新する、削除する 403
Bの所有する注文を作成する 403
ownerIdをBに変更する 403

それでも、Aは自分の注文のtotalを変更でき、200でした。クライアントにそのフィールドを設定させたくないなら、変更してよいフィールドを指定します。

allow update: if request.auth != null
  && resource.data.ownerId == request.auth.uid
  && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['note']);

これで、totalの変更は403、noteの変更は200になりました。

正しいルールなのにクエリが失敗したのはなぜ?

ルールはフィルターではないからです。上のオーナールールの下で、Aがコレクション全体を要求すると、3件のうち2件がAのものでも403でした。Firestoreは、クエリを実際にある内容ではなく、返しうる内容で判断します。

Aのリクエスト 結果
注文をすべて一覧する 403
ownerId == Aで検索 200、2件
ownerId == Bで検索 403

ルールを厳しくした後に一覧が壊れたら、直すのはクエリです。一覧を動かすためにルールを緩めると、上で見た広すぎるルールができあがります。

広いmatchは正しいルールを打ち消せる?

はい。ルールファイルが長いプロジェクトなら、最初に探すのはこれです。Firestoreは、一致するルールのどれか1つが許せばリクエストを許可します。

match /orders/{id} {
  allow read: if request.auth != null
    && resource.data.ownerId == request.auth.uid;
}
match /{document=**} {
  allow read: if request.auth != null;
}

最初のブロックは良いオーナールールに見えます。2つ目があると、AはBの注文を200で読め、見知らぬ人は3件の注文をすべて一覧し、同じ人がusersにあるAのドキュメントも読めました。最初のブロックには何の警告もありません。広いmatchを削除してください。後のルールでアクセスを取り戻す方法はありません。

ユーザーはどうやって自分を管理者にする?

このパターンは、ロールをドキュメントに持たせると必ず出てきます。

function isAdmin() {
  return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
match /users/{uid} {
  allow read, write: if request.auth.uid == uid;
}
match /orders/{id} {
  allow read: if resource.data.ownerId == request.auth.uid || isAdmin();
}

「ユーザーは自分のプロフィールを編集できる」と「管理者はすべての注文を読める」は、それぞれ筋が通っています。合わせると権限昇格になります。

1回のリクエストで管理者に。ユーザーA。自分のusersドキュメントに書き込める。前:ユーザーA → Bの注文を読む → 403、自分のロールを書く:ユーザーA → role: admin → 200、後:ユーザーA → Bの注文を読む → 200。
図1. Firebaseエミュレーター(CLI 15.34.2)での私のテスト、2026年11月3日。

修正は、クライアントがそのフィールドを書けなくすることです。書き込みルールにrequest.resource.data.role == resource.data.roleを加えると、同じリクエストが403になり、AはBの注文に入れないままでした。サーバーから設定するカスタムクレームも、クライアントが変更できないので、もう1つの方法です。

問題なかった2つのこと

親のルールは、サブコレクションに漏れません。サインイン済みの全員に/orders/{id}のallow readを付けたとき、Aは注文を読めましたが、その下の/orders/b1/notes/n1は403でした。これはドキュメントどおりの動作で、ルールは一致したパスにだけ適用されます。

ゲストは、トークンがないことを使った比較をすり抜けません。resource.data.ownerId == request.auth.uidは、request.auth != nullを別に書かなくても、ゲストに403を返しました。それでも私は確認を書きます。そのほうがルールが意図を語るからです。

自分のルールをどう確認する?

エミュレーターで、ゲスト、見知らぬ人、2人目のユーザーとしてリクエストを実行してください。上のすべてのケースを見つけたのはこれで、ルールを読むだけでは見つかりませんでした。

最初のチェックには、無料のFirebase ルールチェックが使えます。貼り付けたルールを、プロジェクトに触れずに読みます。この記事のルールセットでは、テストモード、サインインのみのアクセス、広いmatch、ユーザーが自分で書けるロールを指摘しました。読むのはテキストだけなので、データがどんな状態か、ルールがデプロイされたかどうかは分かりません。

ページにあるapiKeyは、それ自体は問題ではありません。Firebaseのウェブ設定は設計上公開されており、無料のシークレットスキャナーもそのように表示します。背後のデータを守るのはルールだからです。ファイルには別のルールがあり、ルールでは止められない漏えいもあります。それはFirebase Storageのルールで試しました。

制限。 Firestoreのみ、ローカルのエミュレーター、REST経由、架空のデータです。Realtime Databaseは別のルール言語を使うため、テストしていません。どのアプリがバックエンドを呼べるかを制限するApp Checkは対象外で、ルールの代わりにはなりません。本番は、私が確認していない細部でエミュレーターと違うかもしれません。

ルールが何を通すか、確信がありませんか?

ご一緒に読みます。営業はしません。

お問い合わせ

© 2026 Denys Zaichenko. 元の記事: https://madsalt.tech/ja/blog/firebase-security-rules-examples/