🍆

Supabaseの .rpc() を活用して SELECT の取得範囲を制御する方法を考える【暴力的なスクレイピングの対策をしよう】

に公開

https://zenn.dev/masa5714/articles/40883d972ab2c7

上記は数日前に書いた「SupabaseでフロントエンドでSELECTを使うのはやめた方がいいかもしれない。スクレイピング対策しないとかなりマズイ」という内容の記事です。まずは上記記事をご覧頂いてから本記事をお読みください。

この問題の解決策を考えてみましたので記事として共有します。(ポスグレ初心者で手探りで触ってるので、セキュリティ的にマズイなどの指摘してください!)

2023/10/16追記-->

【追記】Max rowsの設定で20件前後にしておけばいいだけでした...。【部分的に問題解決】

散々騒いでおいて申し訳ないのですが、Supabaseの Project Settings -> API -> Max rowsの設定で呼び出し件数を制御することができます。これを20や30ぐらいに抑えておけば、大量データ取得されることは無さそうです。

※ローカル環境のSupabaseのGUIではこの設定項目は表示されません。tomlファイルから変更する必要があります。

※テーブル毎にMaxRows設定の件数を超えて取得したい場合は、この記事の内容は役に立つかもしれません。

※テーブル構造がバレてしまうのが嫌な場合もこの記事が役に立つかと思います。カラム名でデータベース初心者感が出てしまうはずなので、攻撃者のモチベーションを高めることになるんじゃないかと妄想しています。あっ、こいつ雑魚だ!と思われてしまったら一生懸命に穴探してきそう。(知らんけど。)

<--追記終了

データ取得系の処理は .rpc() 経由で行う方がいい。

.rpc() とは、PostgreSQLに登録した関数を実行するためのものです。

まずは .rpc() のおさらい

PostgreSQLの例
CREATE FUNCTION hello_world() RETURNS text AS
$$
BEGIN
  RETURN 'Hello world';
END;
$$
LANGUAGE plpgsql;

この関数を .rpc() 経由で動かす場合、下記のように書きます。

Next.jsで動かす例
import { createBrowserClient } from "@supabase/ssr";

...中略...

const supabase = createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);

const { data } = await supabase.rpc("hello_world");
console.log(data);

console.logでは、PostgreSQLの hello_world() 関数の実行結果である「Hello world」が出力されます。

関数を実行するだけなので戻り値を自分で制御できる

フロントエンドで SELECT を実行すると、フロント側で欲しいデータを指定したり、件数を指定したり、かなり自由度が高すぎて第三者による不正なデータ取得の懸念があります。

ところが、 .rpc() 経由で関数を実行するのであれば、自分が関数を作り戻り値を定義するわけですから、一括でデータを取られるような暴力的なスクレイピングから身を守れると思いませんか?

僕はそう感じたので、データ取得系は全て .rpc() 経由で行おうと考えた次第です。

auth.uid() = id のようにユーザー自身のデータだけが取得できるようなものについては .rpc() で取得する必要は無いかなと思います。他ユーザーのデータを持っていかれることはないので!

.rpc() 使うのはわかったが、具体的に何をすればいいの?

それでは、実際に何に気をつけていけばいいか、何をすればいいかを見ていきましょう!

1. テーブルの RLS のポリシーで SELECT オペレーションを問答無用で FALSE を指定しちゃおう!

シンプルに SELECT のクエリを全て拒否してしまいましょう。
上記のように設定すると、フロントエンドから .from('users').select(*) などのリクエストが来てもデータを返さなくなります。

これで暴力的なスクレイピングを阻止できるようになりましたが、
今度はデータ取得ができなくなってしまったので、それを解決していきましょう。

2. rpc() で使う用のPostgreSQL関数を作ろう!

※下記の関数はただの書き方の例です。
 ガバガバなので実際に使うのは絶対にやめてください。

public.usersからデータを取得する例
CREATE OR REPLACE FUNCTION public.get_the_user()
    RETURNS TABLE (user_name text) -- user_name カラムの値を返してくれる
    LANGUAGE plpgsql
    SECURITY DEFINER SET search_path = public -- 関数登録を行ったユーザーと同じ権限で実行される
AS $function$
BEGIN
    -- publicスキーマのusersテーブルのuser_nameカラムのデータを取得
    RETURN QUERY SELECT users.user_name FROM public.users LIMIT 1;
END;
$function$;

このような感じで関数を作成します。

ポイントは SECURITY DEFINER という記述です。
この記述が無いと、ポリシーがそのまま効いてしまい .rpc() 経由であってもデータ取得ができません。フロントエンドから関数を実行する際に、public.get_the_user() 関数を作ったときのPostgreSQLユーザーと同じ権限で実行することを許可する記述です。

また、 SET search_path = public によって public スキーマ以外へのアクセスを制限することで比較的安全になります。

おわり

これで一括取得するような暴力的なスクレイピング対策をしつつ、フロントエンドだけでデータ取得ができるようになったかなと思います!

INSERT や UPDATE、DELETEについてはこの方法ではなく、Supabaseが用意してくれている方法で実行すればいいと思います。(ポリシー設定はしっかり!)

バックエンドと組み合わせればこんなこと考えなくてもいいのですが、フロントエンドだけで済ませたいという欲求には勝てず考えた結果です!

指摘は大歓迎!

どんなことでも構いません。
重箱の隅をつつくような指摘でもなんでも大歓迎です!
とにかく一人で情報収集していると何が正しいのかが分からず不安なので、間違った指摘でも頂けると様々な視点で考えるきっかけになるのでドンドン欲しいです!

Discussion