前回の動画では、RLS(Row Level Security)が未設定の場合、フロントエンドから直接データベースへアクセスされてしまうという問題を扱いました。RLSポリシーを設計すればそこをガードでき、侵入を防げるというお話でした。
今回は、さまざまな脆弱性リスクを知っていただくために、バックエンドのAPIを用意しました。API経由でフロントエンドとバックエンドを通してデータベースを操作する構成で、そこに潜む脆弱性を紹介します。
IDORとは
今回紹介するのは IDOR(Insecure Direct Object Reference) という脆弱性です。API経由であっても、APIに認可チェックを入れていない場合、そしてRLSポリシーを設定していない場合(あるいは設定していてもサービスロールキーを使っている場合はRLSをバイパスできてしまうため)、IDORが成立します。
通常、AIはきちんと認可チェックも実装し、RLSポリシーも設定してくれますし、サービスロールキーも使わないことが多いので過度に気にする必要はありません。ただ、こうした危険性があるという一例としてご覧ください。
脆弱な実装を確認する
今回のAPIはNext.jsのApp Router配下、api/todos/[id]/route.ts にあります。これは脆弱性を含んだ状態の実装です。
- まずサービスロールキーを使っています。これはRLSをバイパスして実行してしまうキーなので、本来は使ってはいけません(今回はサンプルのために使用しています)。
- PATCH(編集)やDELETE(削除)など、いくつものAPIを用意しており、作成も削除もできます。
たとえばTodoを編集する関数では .eq でタスクIDの照合をしています。Todo IDと、これから編集しようとしているTodo IDが一致した場合のみ編集できるようにしているのですが、編集しようとしているユーザーが誰なのかを指定していません。これが認可チェック漏れの実装です。
攻撃を試してみる
他人のTodo IDが分かっている場合、fetchリクエストで直接そのデータを乗っ取ることができます。たとえば別のユーザーのTodoに対して書き換えのリクエストを送ると、リロード後にはそのデータが書き換えられてしまいます。
認可チェックを入れていないため第三者が直接編集でき、さらにサービスロールキーを使っているのでRLSを設定していてもそれが意味をなしません。この状態ではデータの書き換えも削除もすべて可能になってしまいます。
認証と認可の違い
修正の前に、よく似た2つの用語を整理します。認証(Authentication)は「誰がログインしているか」を確認すること、認可(Authorization)は「そのユーザーがその操作をしてよいか」を確認することです。AIと壁打ちしながら学ぶと、この違いがつかめてくると思います。
認可チェックを実装して修正する
脆弱性が分かったので、AIに認可チェックを実装してもらいます。修正後のコードでは次の流れになります。
- まず認証で、現在アプリにログインしているユーザーのIDを取得する。
- 次に認可チェックとして
.eqを追加し、対象データの user_id がログイン中のユーザーIDと一致するかを照合する。
これにより第三者による書き換えを防げます。修正後に再度同じ攻撃を実行すると、今度は Unauthorized となり認可されず、乗っ取ることはできません。
多層防御の考え方
アプリのセキュリティを強固にする方法はいくつもあります。
- フロントエンドでフィルターやバリデーションをかけ、悪意のある文字列を受け付けないようにする。
- API を経由する。Supabase は直接データベースにアクセスできてしまうため、多層防御の観点からAPIを作ったほうがよいです。
- APIを作ったから安心というわけではなく、認可チェックは必ず行う。行わないと外部からAPIを叩かれてしまいます。
- Supabase を使っているなら RLS の設定も行う。
このように防御をいくつ重ねるかという多層防御の考え方は非常に大切なので、頭に入れておいてください。
XSSとdangerouslySetInnerHTML
XSSやCSRFといった昔ながらの攻撃は、ReactやNext.jsのフレームワークを使えばデフォルトで守ってくれるので、基本的には安心です。
ただしXSSについて一点注意があります。dangerouslySetInnerHTML というプロパティを使うと、HTML要素に直接プログラムコードを書き込めてしまいます。ブログサービスや、コードを実行させる系のサービスを作るとき、AIがこのプロパティを使って実装してしまう場合があります。そうなると悪意のあるサイトへ誘導するプログラムなどを直接埋め込めてしまうため、コードを実行する系のサービスを作る場合は気をつけてください。
ここまでで3つの脆弱性を紹介しました。脆弱性の怖さがなんとなく分かっていただけたのではないかと思います。