---
title: "Firebaseセキュリティルールの例：攻撃した11本"
description: "エミュレーターで試したFirebaseセキュリティルールの例。各ルールがゲストや他のユーザーに何を許したか、開いたままの穴、それを塞いだルールを紹介します。"
url: "https://madsalt.tech/ja/blog/firebase-security-rules-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/ja/about/"
language: "ja"
published: 2026-11-03
---

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

TL;DR

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

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

**テスト方法。** 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のドキュメントも、これを[よくある危険なパターン](https://firebase.google.com/docs/rules/insecure-rules)として挙げています。

## オーナー確認に`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`になりました。

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

[ルールはフィルターではない](https://firebase.google.com/docs/firestore/security/rules-query)からです。上のオーナールールの下で、Aがコレクション全体を要求すると、3件のうち2件がAのものでも`403`でした。Firestoreは、クエリを実際にある内容ではなく、返しうる内容で判断します。

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

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

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

はい。ルールファイルが長いプロジェクトなら、最初に探すのはこれです。Firestoreは、[一致するルールのどれか1つが許せばリクエストを許可します](https://firebase.google.com/docs/firestore/security/rules-structure)。

```
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。](https://madsalt.tech/charts/ja/firebase-security-rules-examples.svg)

*図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`でした。これは[ドキュメントどおりの動作](https://firebase.google.com/docs/firestore/security/rules-structure)で、ルールは一致したパスにだけ適用されます。

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

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

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

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

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

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

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

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

[お問い合わせ](https://madsalt.tech/ja/#contact)
