テック技報最新号へ →
セキュリティ

暗号化したデータと復号鍵を同じ場所に置かない(ソースネクスト事例と Supabase の点検項目)

「暗号化して保存していた」のに平文で漏れる典型は、復号鍵に届くシステムが同じ侵入で破られるケース。パスワードは可逆暗号化ではなくハッシュにし、どうしても復号が要る外部 API キーなどは鍵を DB の外に分け、API の戻り値にも載せない。

@MalwareBibleJP · 2026-10-18
#encryption#key-management#password#api#supabase#postgres#rls#incident

事例から分かること

  • ソースネクストの会員情報流出では、パスワードを暗号化して保存していたが、暗号鍵にアクセスできるシステムが不正アクセスを受け、元の文字列が判読できる状態で漏れたと同社が説明している。
  • 直接の原因は会員情報取得 API の不正利用で、対象 2,251 万 9,960 件は API のログから特定した。API の戻り値にパスワードを含めていた理由は、ライセンス管理システムとの連携など、パスワードが必要な処理で使う API だったためとしている。
  • 教訓は 3 つ。①ログイン用パスワードは復号できる形で持たない、②平文が必要な値を API のレスポンスに入れない、③暗号文と鍵が同じ侵入範囲にあると暗号化の意味がない。

対策

  1. ログイン用パスワードはハッシュにする: 復号鍵が存在しない形(Argon2id / bcrypt など)で保存する。パラメータの決め方は パスワードハッシュは「アルゴリズム+コストパラメータ」まで決める(Argon2id / scrypt / bcrypt / PBKDF2) を参照。他システム連携でパスワードが要るなら、パスワードを渡すのをやめて連携用のトークンや OAuth に置き換える。
  2. 復号が必要な秘密は鍵を分離する: 外部連携の API キーや Webhook URL のように送信時に平文が要るものは、AES-256-GCM などで暗号化して DB に置き、鍵は DB の外(実行環境のシークレット、KMS など)に置く。DB のダンプだけ漏れても復号できない状態を作る。
    ts
    import crypto from "node:crypto";
    export function encryptSecret(plain: string, keyHex: string): string {
      const iv = crypto.randomBytes(12); // GCM は 12 バイト IV
      const c = crypto.createCipheriv("aes-256-gcm", Buffer.from(keyHex, "hex"), iv);
      const ct = Buffer.concat([c.update(plain, "utf8"), c.final()]);
      return `enc:v1:${iv.toString("hex")}:${c.getAuthTag().toString("hex")}:${ct.toString("hex")}`;
    }

    鍵をバックアップやリポジトリに入れない、アプリと DB を同じ環境に同居させない、も同じ考え方。

  3. レスポンスに戻さない: 登録済みのキーは本人の設定画面でもマスク表示にし、一覧取得の SELECT から機密列を外す。ブラウザの開発者ツールに流れた時点で漏れたのと同じ。
  4. 大量取得を検知できるログを取る: 事例では API ログから対象件数を特定できた。逆に言えば、平常時から「1 クライアントが短時間に大量の会員情報を取った」を検知できれば被害を小さくできる。

Supabase を使っている場合の点検項目

  • SECURITY DEFINER 関数の実行権限: PostgreSQL の関数は既定で PUBLIC に EXECUTE が付き、public スキーマにあると POST /rest/v1/rpc/<関数名> で誰でも呼べる。サーバー専用の関数は REVOKE EXECUTE ON FUNCTION ... FROM PUBLIC, anon, authenticated; のうえで service_role にだけ付ける。
  • RLS 判定用のヘルパーは private スキーマへ: PostgREST が公開するのは public スキーマだけなので、private.is_project_member(...) のように置けば RLS からは使えて外部の RPC からは見えない。
  • SELECT ... USING (true) を grep する: Security Advisor は書き込み系の WITH CHECK (true) は警告するが、SELECT の USING (true) は意図的な全公開が多いため警告しない。profiles や招待テーブルが全件読める状態になっていないか、マイグレーションの CREATE POLICY を検索して確かめる。
  • 関数の search_path を固定する: SET search_path = public, pg_temp、より厳しくは SET search_path = '' にしてスキーマを完全修飾で書く。固定しないと同名オブジェクトのすり替えで権限昇格されうる。
  • Leaked Password Protection を有効にする: Authentication の Attack Protection にある漏洩パスワード拒否(HaveIBeenPwned 連携)。
  • 拡張機能を extensions スキーマに置く: pg_net などが public に入っていると REST API に露出しうる。

出典

X の元の投稿を開く