---
title: "Appwrite vs Supabaseの安全性：同じ攻撃で比較"
description: "Appwrite 2.3.0とローカルのSupabaseに同じ攻撃を実行。新規テーブルはどちらも閉じていますが、失敗の仕方とキーの権限が違います。"
url: "https://madsalt.tech/ja/blog/appwrite-vs-supabase/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/ja/about/"
language: "ja"
published: 2026-10-30
---

# Appwrite vs Supabaseの安全性：同じ攻撃で比較

TL;DR

どちらも新しいテーブルはデフォルトで閉じています。Appwriteはゲストにもサインイン済みユーザーにも`401`を返し、SupabaseはテーブルをAPIロールにgrantするまで`401`/`403`を返します。違いが出るのは、ルールが間違っているときや、リクエストがユーザーをまたぐときです。Supabaseでは、他のユーザーの行へのupdateが`200 []`を返し、緩いinsertポリシーのせいでユーザーが別の会社に参加できました。Appwriteでは、私のテストでユーザーをまたぐ試みはすべて`401`か`404`で終わり、ユーザーは自分の行を、自分自身か全員以外の誰にも付与できませんでした。Appwriteのサーバーキーにはscopeがあり、Supabaseのsecret keyはすべてのポリシーを素通りします。

Denysです。フルスタック・AIエンジニアで、私が作る、あるいは直すアプリのほとんどはSupabaseで動いています。バックエンドをまだ選んでいるクライアントからは、Appwriteはセキュリティ面でどうなのかとよく聞かれます。機能の一覧では答えが出ないので、両方に同じリクエストを送りました。

**テスト方法。** Appwrite 2.3.0を2026年10月30日にDockerでセルフホストし、REST API経由で、ゲスト、サインイン済みの2人のユーザーAとB、2つのサーバーキーとして実行しました。Supabase側は、2026年10月24日にローカルスタック（CLI 2.109.1、Postgres 17.6）で同じロールを使って行った私のテストで、詳細は[Supabase RLSポリシー例](https://madsalt.tech/ja/blog/supabase-rls-policy-examples/)にあります。どちらも架空の行です。

## AppwriteとSupabaseはデータをどう守る？

SupabaseはPostgresでテーブルを守ります。grantがAPIロールがそもそもテーブルに届くかを決め、SQLで書くrow level securityポリシーが、各リクエストが見られる行と書き込める行を決めます。Appwriteは権限をデータの隣に持ちます。テーブルには、読み取り、作成、更新、削除を許可するロールのリストがあり、row securityをオンにすると各行が自分のリストを持ちます。[Appwriteの権限のドキュメント](https://appwrite.io/docs/products/databases/tablesdb/permissions)では、アクセスは付与されるだけで拒否されることはなく、ユーザーにはテーブルレベルか行レベルのどちらかで必要で、両方は要りません。

つまり、問題はどちらにアクセス制御があるかではありません。両方にあります。問われるのは、それぞれがどんな間違いを許してしまうかです。

## 新しいテーブルをそれぞれどう扱う？

| リクエスト                       | Appwrite、権限の設定なし                          | Supabase、RLSオン、grantなし（10月24日） |
| -------------------------------- | ------------------------------------------------- | ---------------------------------------- |
| ゲストが行を一覧する             | `401` user_unauthorized                           | `401` permission denied                  |
| サインイン済みユーザーが行を一覧 | `401` user_unauthorized                           | `403` permission denied                  |
| サインイン済みユーザーが行を追加 | `401` No permissions provided for action 'create' | 未テスト                                 |

作ったばかりのテーブルは、どちらでも閉じています。Supabaseではこれは新しい挙動です。2026年10月30日以降に作られたテーブルには、Data APIのgrantが自動では付かなくなりました。これは[Supabase 10月30日のgrant変更：壊れる点と確認方法](https://madsalt.tech/ja/blog/supabase-october-30-grant-change/)で扱いました。

## テーブルが全員に開いているとどうなる？

2つの「開いた」設定は同じものではありません。Appwriteでは、テーブルに`read("any")`という権限を1つだけ与えました。Supabaseで最もよくある開いた状態は、RLSがオフで、デフォルトのgrantが残っている状態です。

| ゲストのリクエスト | Appwrite、`read("any")` | Supabase、RLSオフ、デフォルトgrant（10月24日） |
| ------------------ | ----------------------- | ---------------------------------------------- |
| 行を一覧           | `200`、2行とも          | `200`、2行とも                                 |
| 行を削除           | `401`                   | `200`、2行とも削除された                       |

Appwriteの「公開」は、書き込みも付与しない限り読み取り専用です。RLSがオフのSupabaseテーブルは、publishable keyを持つ誰でも読み書きできます。だから「RLS disabled in public」が最初に確認する項目なのです。

## 「自分の行」はそれぞれどう動く？

Appwriteでは、row securityをオンにして、テーブルに`create("users")`という権限を1つだけ与えました。Aが権限を指定せずに行を作ると、Appwriteが自分で権限を設定しました。ユーザーAだけの`read`、`update`、`delete`です。Supabaseで同じ意図を実現するには、`(select auth.uid()) = user_id`に対するポリシーが4つ必要です。

| リクエスト       | Appwrite（10月30日） | Supabase（10月24日）       |
| ---------------- | -------------------- | -------------------------- |
| ゲストが一覧     | `200`、0行           | `401`（`anon`にgrantなし） |
| Aが行を一覧      | `200`、Aの2行        | `200`、Aの2行              |
| AがBの行を読む   | `404` row_not_found  | 一覧には返らない           |
| AがBの行をupdate | `401`                | `200 []`                   |
| AがBの行をdelete | `401`                | `200 []`                   |

![同じリクエスト、2つのバックエンド。ユーザーAが、それぞれのREST API経由でユーザーBの行を編集。Supabase、所有者専用ポリシー：ユーザーA → Bの行を更新 → 200 \[\] → 0行、エラーなし、Appwrite、row securityオン：ユーザーA → Bの行を更新 → 401 → エラーが返る。](https://madsalt.tech/charts/ja/appwrite-vs-supabase.svg)

*図1. 私のテスト：ローカルのSupabaseスタック（CLI 2.109.1）、2026年10月24日、セルフホストのAppwrite 2.3.0、2026年10月30日。*

どちらもBの行は漏らしませんでした。違いは応答です。Supabaseはポリシーが隠す行を除外するので、何にも一致しなかったupdateは、ゼロ行の成功になります。「エラーなし」を「保存できた」とみなすアプリは、編集できたとユーザーに伝えます。Appwriteは、アプリが見逃せないエラーで拒否します。

## ユーザーは自分のデータを他人に渡せる？

生成されたコードが最も間違えやすいところなので、両方で試しました。

| Aのリクエスト             | Appwrite                                               | Supabase（10月24日）                                      |
| ------------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
| ユーザーBが読める行を作る | `401`, Permissions must be one of: any, users, user:A… | ポリシー次第                                              |
| 自分の行をBの所有に変更   | `401`、同じメッセージ                                  | 完全な`with check`なら`403`、`with check (true)`なら`200` |
| 全員が読める行を作る      | `201`、その後ゲストが一覧できた                        | ポリシーが許す場合のみ                                    |

Appwriteは、ユーザーが付与できるものを、自分自身が持つロール（`any`、`users`、自分のユーザー、自分のチーム）に限定します。これにより、私がルールを書かなくても「自分の行を他人に渡す」操作は止まりました。ただし、Aが`read("any")`で自分の行を全員に公開することは止められませんでした。行を絶対に公開してはいけないなら、権限を設定するのをクライアントにさせず、サーバー関数から作成してください。

Supabaseでは、結果はポリシーの書き方しだいです。完全な`with check`は移動を`403`で拒否しました。一部のジェネレーターが書く`with check (true)`では、AがBのアカウントへ行を移せました。

## ユーザーは他人の会社に参加できる？

Supabaseでは、チームのアクセスは通常`memberships`テーブルとポリシーで実現します。私のテストでは、多くのチュートリアルが勧めるinsertポリシー`with check (user_id = auth.uid())`のせいで、Aは自分を会社2に追加して`201 Created`を得て、その給与プロジェクトを読めました。

Appwriteにはチーム機能が組み込まれています。Bがチームを作り、Aはメンバーとして、そしてオーナーとして自分を追加しようとしました。どちらのリクエストも`404 team_not_found`を返しました。Aはチームを見ることすらできなかったのです。チームへの所属はサーバーが決めるもので、あなたが書くルールではないため、この漏洩は起こりえません。ただし、行を共有するには`read("team:…")`権限が必要で、それは自分で設定します。

## サーバーキーは何を開く？

| キー                            | Appwrite                       | Supabase（anon vs service roleのテストより） |
| ------------------------------- | ------------------------------ | -------------------------------------------- |
| フルのサーバーキー              | `200`、両ユーザーの4行すべて   | 3行すべて、RLSを素通り                       |
| 狭いscopeのキー（`users.read`） | `401`, missing scope rows.read | scope付きキーはない                          |

Supabaseのsecret keyは`service_role`に対応し、`BYPASSRLS`を持ちます。何が開くかは[anon keyとservice role keyの違い](https://madsalt.tech/ja/blog/supabase-anon-key-vs-service-role-key/)にあります。Appwriteのキーも行の権限を無視しますが、作成時のscopeに限られるため、ユーザーを管理する処理用のキーでは、あなたのテーブルは読めません。どちらのキーも、ブラウザのバンドルに入れば、届く範囲すべてが漏れます。

Appwriteの小さな発見が2つあります。テーブルの一覧を求めたゲストとサインイン済みユーザーは、どちらも`401`でした（この呼び出しには`tables.read`が必要で、それを持つのはキーだけです）。そして`read("users")`は、サインイン済みのあらゆるアカウントを意味します。サインアップが開いているSupabaseでの`auth.uid() is not null`と同じ落とし穴です。

## どちらが安全？

どちらもデフォルトで安全とも危険とも言えません。どちらも閉じた状態から始まりました。違いは、間違いが潜む場所です。

| 質問                            | Appwrite                           | Supabase                             |
| ------------------------------- | ---------------------------------- | ------------------------------------ |
| アクセスルールの置き場所        | テーブルと行のロールのリスト       | SQLのgrantとポリシー                 |
| 拒否されたユーザー間の書き込み  | エラー（`401` / `404`）            | updateとdeleteは0行の`200`           |
| 行を他者に付与                  | ユーザーが持つロールにのみ         | ポリシーが許す範囲                   |
| 列の値、MFAレベルに基づくルール | 権限にはない。サーバーコードが必要 | ポリシーで可能。例：機密行に`aal2`   |
| サーバーキー                    | scope付き                          | secret keyはすべてのポリシーを素通り |

Appwriteのモデルのほうが狭く、私のテストでは、その狭いモデルが、何も書かなくてもユーザー間のあらゆる手口を拒否しました。SupabaseのモデルはSQLなので、Appwriteの権限では表せないルール、たとえば「MFAのときだけ」や「自分が所属する会社の行だけ」を表現できますが、間違ったルールも表現できてしまいます。チームがポリシーを読まないなら、Appwriteのデフォルトのほうが許容範囲が広いです。データに依存するルールが必要なら、Supabaseでは書けますが、そのあとテストが必要です。私の方法は[Supabase RLSのテスト方法](https://madsalt.tech/ja/blog/how-to-test-supabase-rls/)にあります。

## Supabaseプロジェクトをどう確認する？

無料の[Supabaseセキュリティチェック](https://madsalt.tech/ja/tools/supabase-security-check/)は、プロジェクトを読み取り専用で読み、RLSがオフのテーブル、常に真になるポリシー、ゲスト向けの書き込みポリシー、ポリシーが示す以上の権限を与えるgrantを指摘します。Supabaseが守ってくれる範囲と、あなたに任される範囲は[Supabaseは安全？](https://madsalt.tech/ja/blog/is-supabase-secure/)にあります。Appwriteに相当するものはありません。Appwriteでは、上のリクエストを2人のユーザーとして実行してください。Firebaseについては、同じ種類の攻撃を[Firebaseセキュリティルールの例](https://madsalt.tech/ja/blog/firebase-security-rules-examples/)で試しました。

**制限。** ノートPC上のセルフホストのAppwrite 2.3.0を1つ、ローカルのSupabaseスタックを1つ、6日の間隔で、架空の行で試しました。Appwrite Cloudはセルフホストと異なる可能性があります。Storage、関数、realtime、認証設定、コンプライアンス認証は、ここでは比較していません。

非公開データのあるアプリのバックエンドを選んでいますか？

アクセスモデルを一緒に見ることができれば嬉しいです。売り込みはありません。

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