今日じゅうに
コードを直す必要がある項目です。今この瞬間に誰でも実行できるので、一日を超えないでください。
A
緊急
一部のテーブルにしかアクセスルールがかかっていません
何が起きるか
データベースには「誰がどの行を見られるか」というルールがテーブルごとに個別にかかります。あるテーブルにはかかっていて、あるテーブルにはありません。ルールのないテーブルは、ブラウザに公開された鍵だけで全部読み出せます。その中に連絡先や注文が入っていれば個人情報の漏えいであり、法定の届出義務につながります。
誰が、どうやってできるか
- 必要なもの
- ブラウザに公開された anon キー。アプリを開けば誰でも持っています
- 難易度
- ブラウザの開発者ツールからのリクエスト一つ
- 影響範囲
- ルールの抜けたテーブルの全行
どう確認したか
スキーマファイルの create table 文と enable row level security 文を、テーブル単位で照合します。表記の違う同じテーブルは一つとして数えます。Firebase アプリならルールファイルから、条件が true のものやログインの有無しか見ていないルールを探します。シークレットスキャナと依存関係スキャナの検出対象ではありません
Supabase のスキーマファイル
公開アプリ 87 件の検査で最も多く出た形です。SQL でテーブルを作る 60 件のうち 6 件がこれに該当し、その 6 件にルールのないテーブルが 50 個残っていました。最も鮮明だった一件はテーブル 82 個にルールをかけ、3 個を取りこぼしました。82 回きちんとやった人が 3 回逃したということです。「ダッシュボードで有効にしたのだろう」という説明はこの形には通りません — 同じファイルで別のテーブルには有効にしているからです。一社の問題ではありません。同じ日に公開 Firebase アプリ 60 件を別途検査したところ、ルールファイルをリポジトリに置いている 25 件のうち 8 件に条件が true のルールがあり、3 件は書き込みまで開いていました。4 件はアカウントさえあれば全体に届きます。同じ 60 件の中に、範囲がきちんと指定されたルールが 302 個あります。ただしリポジトリのルールファイルが実際に配備されたルールだという保証はないため、Firebase の数値はリポジトリに何が入っているかまでしか語りません。
create table public.orders (...);
alter table public.orders enable row level security;
create table public.contacts (...);
-- 三行上にはあるルール文が、ここにはありません
どう直すか
ルールを有効にする文だけを先に入れないでください。ポリシーなしにルールだけ有効になると、すべての参照が空の結果になり、アプリはその場で止まります。下の指示が両方を同じファイルに入れる理由です。
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
Supabase のスキーマでアクセスルールが抜けているテーブルを探し、
ルールとポリシーを一緒に入れてほしい。
重要: ルールを有効にする文とポリシーは、必ず同じマイグレーションファイルに
入れること。ポリシーなしにルールだけ有効になった瞬間、すべての参照が
空の結果になりアプリが止まる。
作業:
1. create table はあるが enable row level security がないテーブルを
すべて列挙する。表記が混在していれば public.x と x は同じテーブルと見なす
2. 各テーブルに所有者条件のポリシーを追加 — ログインユーザーが自分の行だけ
select/insert/update できるようにする
3. 公開で読まれるべきテーブルは、ルールを切るのではなく読み取り専用ポリシーで開く
4. anon ロールに残っている不要な GRANT を回収する
5. auth・storage など Supabase 管理スキーマには触れない
保つべき動作: ログインユーザーの現在の画面と API レスポンスは同一であること
変更禁止: アプリケーションのロジック、既存のカラム構造
確認: ログインしていないリクエストと、ユーザー A のトークンでユーザー B の行を
参照 → どちらも空の配列
直ったかをどう確かめるか
ログインせずに参照 → 空の結果。ユーザー A でユーザー B の行を参照 → 空の結果。そのうえでスキーマファイルの create table の数と enable row level security の数を数えて一致するか見てください。この検査で引っかかった 6 件は、すべて二つの数が違いました。
B
緊急
管理者キーをブラウザに出るファイルで使っています
何が起きるか
データベースのすべてのルールを無視するマスターキーがあります。その鍵を参照するコードが「ブラウザで実行」と印のついたファイルに入っています。このファイルはウェブページのソースに載って出ていきます。
誰が、どうやってできるか
- 必要なもの
- 変数名が NEXT_PUBLIC_ で始まっていれば何も要りません。ページのソースを開けば見えます
- 難易度
- ブラウザでページのソースを見る
- 影響範囲
- 名前が公開形ならデータベース全体 — 読み取り・変更・削除。そうでなければ、その機能が権限なしに静かに失敗します
どう確認したか
コードを読んで発見。「ブラウザで実行」の印とマスターキーの参照が同じファイルにあるかを照合します
「use client」の付いたモジュール
どちらの場合も直す必要があります。変数名が NEXT_PUBLIC_ で始まっていると Next.js がその値をバンドルにそのまま埋め込むので、鍵が外に出ます。付いていなければブラウザでは値が空になり、管理者権限が必要な動作が権限なしで動くコードになります。前者は漏えい、後者は静かな故障です。
'use client'
import { createClient } from '@supabase/supabase-js'
export const admin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // ← ブラウザに出るファイルです
)
どう直すか
鍵のローテーションは上の「いま 10 分で」にあります。ローテーションはデプロイ中のアプリを即座に止めるので、その項目の順序の説明を先にお読みください。
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
service_role キーが 'use client' モジュールから参照されている。
サーバー専用に移してほしい。
作業:
1. 'use client' の付いたファイルから service_role の参照をすべて除去
2. ブラウザ用クライアントは anon キーだけを使うよう分離
(lib/supabase-client.ts)
3. サーバー専用クライアントを作り、SUPABASE_SERVICE_ROLE_KEY
(NEXT_PUBLIC_ なし) を使う
→ lib/supabase-server.ts、サーバーコンポーネントと API route からのみ import
4. 管理者権限が必要な動作は API route に移し、その中で呼び出し元をまず認証する
保つべき動作: 現在の画面と API レスポンスが同一であること
変更禁止: データベースのスキーマ、ビジネスロジック
確認: npm run build のあと grep -r "service_role" .next/static → 結果なし
直ったかをどう確かめるか
直す前に npm run build のあと grep -r "service_role" .next/static をまず回してみてください。いま検索に出るなら、鍵はすでに外に出ています。修正後は同じコマンドが何も見つけないはずです。そのうえで古い鍵で API を呼び、拒否されるか確認してください — これがローテーションが実際に効いた証拠です。
C
緊急
支払わずに注文を完了できます
何が起きるか
決済が終わったという信号を受け取るアドレスがあるのに、その信号が本物の決済事業者から来たのかを確認していません。アドレスさえ分かれば誰でも偽の信号を送り、お金を払わずに注文を完了させられますし、金額も好きなように書けます。
誰が、どうやってできるか
- 必要なもの
- 決済通知のアドレス一つ。よくある経路なので推測できます
- 難易度
- コマンド一行
- 影響範囲
- 無料の注文、金額の改ざん、同じ注文の繰り返し処理
どう確認したか
コードを読んで発見。決済通知を受けるハンドラで、署名の検証・重複防止・金額の再確認の三つをそれぞれ探します。シークレットスキャナと依存関係スキャナの検出対象ではありません
決済 webhook のハンドラ
三つが揃って抜けています。署名の検証なし — 決済事業者から来たリクエストかを確認していません。重複防止なし — 同じ通知を 100 回送れば 100 回処理されます。金額の再確認なし — サーバーで実際の決済金額を照会し直していません。
export async function POST(req: Request) {
const body = await req.json()
await fulfillOrder(body.orderId, body.amount)
return Response.json({ ok: true })
}
どう直すか
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
決済 webhook に署名の検証、重複防止、金額の再確認を追加してほしい。
作業:
1. 署名の検証: 決済事業者が送った署名ヘッダを raw body で検証する。失敗時は 401
(Stripe なら stripe.webhooks.constructEvent、他社ならドキュメントの検証方式)
2. 冪等性: 処理した event id を保存するテーブルを作り、
すでにあれば 200 だけ返して終了する
3. 金額の再確認: body の amount を使わず、決済事業者の API でその決済を
照会し直し、実際の承認金額と注文金額が一致するか確認する。
不一致なら中断してログを残す
保つべき動作: 正常な決済は今と同じく注文が完了すること
変更禁止: 注文完了後のビジネスロジック (fulfillOrder の内部)
確認:
1. 署名ヘッダなしで POST → 401
2. 有効なイベントを 2 回送信 → 注文は 1 回だけ処理
3. body の amount を改ざんして送信 → 処理を拒否
直ったかをどう確かめるか
署名ヘッダなしでリクエスト → 401 で拒否。同じイベントを 2 回送信 → 1 回だけ処理。金額を改ざんして送信 → 拒否。
D
緊急
コミットされたファイルに本物の鍵が入っています
何が起きるか
環境変数ファイルが値を含んだままリポジトリに入っています。リポジトリを見られる人は誰でも、その鍵で該当サービスを使えます。リポジトリが公開なら自動収集プログラムが常に走査しているので、鍵はすでに収集されたと考えるほうが安全です。
誰が、どうやってできるか
- 必要なもの
- リポジトリの読み取り権限。公開リポジトリなら何も要りません
- 難易度
- ファイルを開く
- 影響範囲
- その鍵が開けるサービス全体
どう確認したか
シークレット検査で検出したあと、ファイルを一つずつ開いて本物の値かドキュメント・例・フィクスチャかを分けます
リポジトリにコミットされた環境変数ファイル
公開アプリ 87 件でのシークレット検査の生の検出は 23 件でした。README や API ドキュメント、テストのフィクスチャ、.env.example を取り除くと 11 件が残り、そのうち最も疑わしい 4 件を直接開いたところ、本物の漏えいは 1 件でした。コミットされた .env.save 一つに JWT の秘密鍵と Redis REST トークン、データベースの接続文字列が一緒に入っていました。残りの 7 件は開いていません。同じ 4 件から出た誤検出のほうが重要です。リリーススクリプトで「Supabase キーの漏えい」として報告された値を開き、JWT をデコードすると role が anon でした。ブラウザに載って出ていくために作られた公開鍵です。そのままレポートに移していたら、お客様は何ともない鍵をローテーションするためにアプリを止めていたはずです。
# .env.save — リポジトリにコミットされています
JWT_SECRET=eyJhbGciOiJIUzI1NiIs...
UPSTASH_REDIS_REST_TOKEN=...
DATABASE_URL=postgresql://postgres:...@db...supabase.co:5432/postgres
どう直すか
ローテーションの順序は上の「いま 10 分で」と同じです。ファイルを消すだけでは何も解決しません — 履歴にそのまま残り、すでに出た鍵は有効なままです。
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
コミットされたファイルに本物の鍵が入っている。
ローテーションして履歴から消してほしい。
作業:
1. 実際の値が入ったファイル(.env.save など)をリポジトリから削除し
.gitignore に追加する
2. 各サービスのダッシュボードで露出した鍵をすべてローテーションする
3. 新しい鍵はデプロイ先の環境変数にだけ入れる。リポジトリに再びコミットしない
4. 過去のコミット履歴から除去する (git filter-repo または BFG)。履歴の
書き換えなので、共同作業者に先に知らせ、強制プッシュの時点を合意する
5. 例が必要なら .env.example にプレースホルダだけを残す
保つべき動作: デプロイされたアプリが新しい鍵で動き続けること
変更禁止: アプリケーションのロジック
確認: リポジトリ全体で露出していた鍵の文字列を検索 → 結果なし。
古い鍵で API 呼び出し → 拒否
直ったかをどう確かめるか
リポジトリ全体で露出していた鍵の文字列を検索し、結果がないか確認してください。そのうえで古い鍵で各サービスの API を呼び、すべて拒否されるか確認してください。ファイルだけ消してローテーションを飛ばせば、何も変わりません。
今週
上の項目を処理したあと、続けて直してください。
E
高
AI 呼び出しに出力の上限がありません
何が起きるか
AI 機能を使うたびに料金が出ていくのに、応答の長さにもユーザーごとの呼び出し回数にも上限がありません。繰り返し呼び出しを自動化すれば料金は上がり続けます。セキュリティ事故ではありませんが、金銭的な損失は同じです。
誰が、どうやってできるか
- 必要なもの
- ログイン。認証の確認がなければログインなしでも可能です
- 難易度
- 繰り返しリクエストのスクリプト
- 影響範囲
- AI の利用料。gpt-4 の出力単価は 100 万トークンあたり $60 で、上限を 4,000 トークンに置くとリクエスト 1 件あたり最大 $0.24 です。毎分 60 回の自動呼び出しなら 1 時間で最大 $864 です。アカウント等級ごとの毎分トークン制限に当たれば実際の上限はこれより低くなります — ダッシュボードで現在の等級をご確認ください
どう確認したか
コードを読んで発見。LLM の呼び出し地点に出力の上限があるか、そして上限をかけるコードが別のファイルにあるかまで見ます。シークレットスキャナと依存関係スキャナの検出対象ではありません
LLM の呼び出し地点
ユーザーごとの呼び出し制限、日次の上限、予算の通知がすべてありません。認証の確認もありません。出力の上限を別のファイルでかけている場合があるので、この項目は呼び出し地点だけを見て判定せず、上限をかけるコードがどこにもないことを確認してから上げます。
どう直すか
予算上限の設定は上の「いま 10 分で」にあります。コードとは別に、それが最後の防御線です。
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
LLM を呼び出す API route に使用量の制限を追加してほしい。
作業:
1. ユーザーごとの rate limit: 毎分 5 回、日次 50 回 (ユーザー id + 日付で計数)
2. 超過時は 429 と案内メッセージ
3. 出力の上限(max_tokens)を 1000 と明示する
4. 認証されていないリクエストは 401 (現在は誰でも呼び出せる)
5. 日次の全体呼び出しがしきい値を超えたら機能を自動停止する kill switch
保つべき動作: 通常のユーザーの一般的な利用が妨げられないこと
変更禁止: プロンプトの内容、モデル選択のロジック
確認: 同じユーザーで毎分 6 回呼び出し → 6 回目が 429。認証なしで呼び出し → 401
直ったかをどう確かめるか
同じユーザーで毎分 6 回呼び出し → 6 回目が 429。認証なしで呼び出し → 401。そのうえで応答が上限で切れる場合がどれくらいあるかをログで確認してください — 頻繁に切れるなら上限が低いということで、それは別に調整する問題です。
F
高
既知の脆弱性がある依存関係が溜まっています
何が起きるか
インストールされているライブラリの中に、公開された脆弱性があるバージョンが残っています。公開された脆弱性は攻撃コードも一緒に出回っていることが多く、バージョンを見るだけで試せます。ほとんどはバージョンを上げれば終わります。
誰が、どうやってできるか
- 必要なもの
- lockfile。公開リポジトリなら誰でも読めます
- 難易度
- 公開された脆弱性一覧との照合
- 影響範囲
- パッケージによります。認証・リクエスト処理の系統なら迂回につながります
どう確認したか
依存関係検査
lockfile
公開アプリ 87 件のうち 83 件で lockfile を測定しました。脆弱性のある依存関係はリポジトリあたり中央値 81 件、最大 253 件でした。CVSS 9.0 以上を一つでも持つ所が 83 件中 46 件です。この項目を上の四つと同じ重さで数えない理由は、ほとんどが更新で解決し、判断が要らないからです。
どう直すか
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
既知の脆弱性がある依存関係を上げてほしい。
作業:
1. ロックファイルを基準に、既知の脆弱性の一覧を作る
2. CVSS 9.0 以上から上げる。メジャーアップグレードが必要なものは別に印を付ける
3. 一度に一つずつ上げ、ビルドと既存の動作を確認してから次に進む
4. 上位パッケージに縛られて上げられないものは、理由を書いて残す
保つべき動作: ビルドが通り、既存の画面・API が同一に動作すること
変更禁止: アプリケーションのロジック
確認: 同じ検査をもう一度回して CVSS 9.0 以上が 0 件
直ったかをどう確かめるか
同じ検査をもう一度回して CVSS 9.0 以上が 0 件か確認してください。残ったものがあれば、なぜ上げられなかったかを一行書き残してください — 次の検査で同じ項目を判断し直さずに済みます。
このままだと漏れ続けるお金
上の項目と違い、リリースを止めません。代わりに毎月出ていきます。機能を変えずに減らせるものだけを書きました。
| 何を | いま |
変えると | 差 |
AI モデル LLM の呼び出し地点 |
gpt-4
出力 100 万トークンあたり $60
|
gpt-4o
出力 100 万トークンあたり $10
|
6 分の 1 1 リクエスト $0.24 → $0.04 |
応答の長さ 同じ呼び出し |
上限なし
|
max_tokens 1000
チャットの応答に 4000 トークンはほとんど使われません
|
4 分の 1 4,000 トークン基準 |
二つを一緒に変えると AI リクエスト 1 件あたり $0.24 が $0.01 を下回ります。モデルと応答の長さを同時に下げるからです。上の E(呼び出し上限)とは別の問題です。E は暴走を止めるもので、こちらは平常時の単価を下げるものです。両方とも必要です。
アプリの中では減らせない費用
転送量の課金は上の二行とは違います。コードで単価を下げる話ではなくダッシュボードで上限を有効にする話で、その項目は上の「いま 10 分で」の 3 番にあります。このレポートは実際の請求内容と転送量の推移を見ていません。ダッシュボードの読み取り権限があって確認できます。
変える前に確認いただくこと
モデルを変えると応答が変わることがあります。お使いのプロンプトをいくつか両方のモデルに入れてみて、結果が使えるかをご自身で確かめてから変えてください。費用が 6 分の 1 でも答えが使えなければ節約ではありません。単価は 2026-08-15 に OpenAI の公開価格表で確認した値で、上の金額はその価格表で計算した値です。価格は変わるので、適用時点で改めてご確認ください。
どう直すか
下を Claude Code・Cursor・Codex にそのまま貼りつけてください。
LLM 呼び出しの単価を下げてほしい。
作業:
1. model を 'gpt-4' から 'gpt-4o' に置き換える
2. max_tokens を 1000 と明示する (現在は上限なし)
3. 応答が切れる場合に備えて finish_reason が 'length' なら
ログを残すよう追加する (上限が実際に足りないかを確認するため)
保つべき動作: リクエスト・レスポンスの形式と、画面に見える結果の構造はそのまま
変更禁止: プロンプトの内容
確認: 同じプロンプト 5 個を変更の前後で呼び出し、応答を並べて比較。
使えるなら維持、だめなら gpt-4o のまま max_tokens だけ上げて再比較