🔑

Supabaseのセキュリティ対策をまとめてみた

2023/01/20に公開

SupabaseはBaaS(Backend as a Service)として、データベース、認証、ストレージなどを提供しています。
簡単に導入できる一方、セキュリティ対策をしないと危険です。

今回はSupabaseを使用する上で必要なセキュリティ対策についてまとめてみました。

なぜセキュリティ対策が必要なのか

JavaScriptを使用して認証、データ操作をするには anonkey(client key) が必要です。anonは匿名という意味合いがあります。

Supabaseではデフォルトでテーブル作成した場合、anonユーザーがCRUD操作を行えるようになっています。つまり、anonkeyを使用して誰でもアクセスできてしまいます。
このような状態でデータを扱うと、悪意のあるユーザーによるデータ改ざん、削除などのリスクがあります。

以上のことから、セキュリティ対策をしっかりと行うことが重要です。

テーブル権限変更

先ほど申し上げましたが、テーブルを作成するとデフォルトではanonkeyを使えば誰でもCRUD操作ができるようになっています。
これを変更するためには、テーブル権限を変更する必要があります。

いわゆる、テーブル単位のアクセス制御になります。

既存権限を確認する

まずは既存のロール、テーブル、何が許可されているかを確認しましょう。

SupabaseのSQL Editorで以下のSQLを入力します。

SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
ORDER BY grantee, table_name, privilege_type;

何も対策していなければgranteeanonがあります。
この権限が付与されているテーブルはもれなく、CRUD操作が可能になっている状態です。

どうすればテーブルアクセス権限を認証ユーザーのみに制限できるでしょうか。
以下のSQLを実行し、anonの権限を剥奪していきます。

REVOKE all privileges
ON all tables
IN SCHEMA public
FROM anon;

これで認証ユーザー以外がテーブルにアクセスできなくなります。

今後のデフォルト権限に備える

既存のテーブルに対しては権限を剥奪しましたが、新規テーブルを作成した場合はanon権限が付与されてしまいます。
それを見越して、テーブル作成時などにデフォルトで未認証ユーザーのアクセス権を無くすようにします。

Supabaseのデフォルト権限設定からanon権限を全て取り除くようにします。テーブル、関数、シーケンスに関わる権限です。

ALTER DEFAULT privileges IN SCHEMA public REVOKE all ON tables FROM anon;
ALTER DEFAULT privileges IN SCHEMA public REVOKE all ON functions FROM anon;
ALTER DEFAULT privileges IN SCHEMA public REVOKE all ON sequences FROM anon;

anon権限を復元したくなった場合

削除した権限を復元したくなる場面があります(投稿一覧は匿名でも閲覧できるといった具合に)。

例として、postsテーブルに対してSELECT文を使用する時にanon権限を付与する場合はこのようにします。

GRANT SELECT ON posts TO anon;

Row Level Security(RLS)

PostgresSQLを使用した行単位のセキュリティになります。
この機能を使用することで、認証ユーザーのみデータを作成、更新ができるといった制御が可能になります。

いわゆる、データ行単位のアクセス制御になります。

出てくる用語

SQLでポリシー作成するにあたり、理解しておくべき用語があります。

  • USING expression
    ここで指定した条件を満たす行を操作できるようになります。言い換えるならSQLでいうWHERE句になります。
  • WITH CHECK expression
    登録、更新をする際に指定した条件を満たす行のみ操作できるようになります。追加の条件を指定したい場合はこちらを使用します。
    満たされない場合はエラーが返されます。

また、ポリシーで使える便利な関数が存在します。

auth.uid() // ログイン中のユーザーIDを取得
auth.jwt() // ログイン中のJWTを取得

データ取得

全ユーザーにデータ取得を許可する

誰でもデータを取得できるようにするには以下のようなポリシーになります。
USING trueに注目してください。これはすべての行データを取得できるようになります。

CREATE policy "Allow public read access"
ON todos
FOR select
USING ( true );

認証ユーザーのデータのみ取得する

認証されたユーザーのデータのみ取得できるようにするには以下のようなポリシーになります。

CREATE policy "View own todos." 
ON todos
FOR select
USING ( auth.uid() = user_id );

クライアント側からリクエストを受けて、実際に実行されるSQLは以下のようになります。

SELECT *
FROM todos
WHERE auth.uid() = todos.user_id;

これの何が嬉しいかというと、クライアント側でSQLを組み立てる必要がなくなります。
JavaScriptを例に出すと、以下のように書けば自身のデータのみ取得できるようになります。

const { data, error } = await supabase
  .from('todos')
  .select('*')
  // eqは不要になる

データ登録

登録ではWITH CHECKを使用します。
認証ユーザーIDとデータのユーザーIDが一致する場合のみ登録できるようになります。こうすることで、他のユーザーのデータを登録することを防ぎます。

CREATE policy "Register own todo."
ON todos
FOR insert
TO authenticated
WITH CHECK ( ( auth.uid() = user_id ) );

データ更新

更新ではUSINGWITH CHECKを使用します。
更新する行データの絞り込みを行い、その後に自身のデータのみ更新できるようになります。(WITH CHECKは省略できます)

CREATE policy "Update own todo."
ON todos
FOR update
TO authenticated
USING ( ( auth.uid() = user_id ) )
WITH CHECK ( ( auth.uid() = user_id ) );

データ削除

削除ではUSINGを使用します。
以下では、認証ユーザーの行データの絞り込みを行い削除できるようになります。

CREATE policy "Delete own todo."
ON todos
FOR delete
TO authenticated
USING ( ( auth.uid() = user_id ) );

さいごに

Supabaseは簡単に導入できますが、セキュリティ対策をしなければ危ういということが伝わったでしょうか。

いまいちピンとこない方は、一度テーブルを作成し、anonユーザーがCRUD操作できるか確認してみましょう。
テーブル権限を考えること、RLSを使用する意義の理解が深まります。

間違った説明などありましたらコメントいただけると助かります。

参考記事

この記事作成にあたり、下記の記事には大変お世話になりました。

https://zenn.dev/suin/scraps/23d4355df14a42
https://zenn.dev/hrtk/scraps/ed6e10fc462393

Discussion