convex-security-check について
Convex プロジェクトの `convex/` を対象に、公開関数、認証、バリデーション、所有権、シークレット管理を確認するセキュリティチェック用スキルです。チェックリストと grep で一般的な問題を探し、完全な監査の代替にはなりません。
主な機能
- 01公開関数と内部関数の区分確認
- 02認証とデータ所有権の確認
- 03引数と戻り値のバリデーション確認
- 04シークレットと HTTP ルートのリスク検出
- 05完全な監査へ移行する条件の提示
活用シーン
- 01`convex/` の変更をマージする前の確認
- 02公開関数の追加や改名後のリスク確認
- 03本番環境へのデプロイ前の簡易確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-security-checkインストール後、Claude Code で/convex-security-checkを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlassets/small-logo.svg
詳細
Convex セキュリティチェック
convex/ を10分ほど確認し、よくある見落としを見つけます。ルールは1つです。エクスポートされた query、mutation、action はすべて、誰でも任意の引数で呼び出せる公開エンドポイントです。そのため、各関数で本人確認と所有権の確認を行うか、意図的に匿名で利用できるようにする必要があります。
各 grep を実行し、結果を確認して、修正するか報告してください。このスキルで見つかるのは問題点であり、システム全体の構成ではありません。全体を把握するには、convex-security-audit に引き継いでください。
これを使うタイミング
convex/に変更を加えたプルリクエストをマージする前- 公開関数を追加または改名した後
- ユーザーが「手早くセキュリティチェックして」「バックエンドの健全性を確認して」「明らかな問題がないか見て」と依頼したとき
- 完全な監査を行う時間がない状態で、開発環境から本番環境へデプロイする前
チェックリスト
プロジェクトのルートからコマンドを実行します。すべての grep で _generated を除外します。
1. 公開関数と内部関数
Convex 関数、スケジューラー、cron、Webhook からのみ呼び出されるものは、internalQuery、internalMutation、または internalAction にする必要があります。
rg -n "export const \w+ = (query|mutation|action)\(" convex --glob '!**/_generated/**'
rg -n "\bapi\.\w+\.\w+" convex --glob '!**/_generated/**'
2つ目の grep では、バックエンド内で使われている api.* を見つけます。サーバーコードで公開関数をスケジュールまたは実行している場合、その関数は内部関数にすべきであることがほとんどです。
- すべての公開関数に、それを呼び出す必要のあるクライアントがある
-
ctx.scheduler.*、ctx.run*、crons.*内にapi.*がない - ロール変更、クレジット付与、全ユーザーにまたがる削除など、管理者限定の操作は内部関数である
2. 認証
rg -l "= (query|mutation|action)\(" convex --glob '!**/_generated/**' \
| xargs rg -L "getUserIdentity|getCurrentUser|authedQuery|authedMutation"
一覧にあるファイルには公開関数が含まれていますが、認証呼び出しがまったくありません。各ファイルを開いて確認してください。匿名で利用できる公開投稿一覧なら問題ありません。ユーザーデータを返したり書き込んだりする関数は、そうではありません。
- すべての公開関数が本人確認を行うか、意図的に匿名で利用できることを明記している
- 認証ヘルパーは、本人確認情報がない場合に例外をスローするか、早期リターンする
- ロールはクライアント引数からではなく、必ず
usersテーブルから取得する
3. バリデーション
rg -nU "(query|mutation|action)\(\{\s*handler" convex --glob '!**/_generated/**'
rg -n "v\.any\(\)" convex --glob '!**/_generated/**'
最初の grep では、定義が handler から始まる関数、つまり args がない関数を見つけます。2つ目では、該当フィールドのバリデーションを無効にする v.any() を見つけます。
- すべての関数に
argsとreturnsのバリデーターがある - データベースまたは外部 API に渡される引数に
v.any()がない - 返却バリデーターでフィールドを明示的に列挙し、
passwordHash、stripeCustomerId、内部フラグが漏えいしないようにしている
4. 所有権と ID
v.id("tasks") によって、その文字列がそのテーブルの有効な ID であることは確認できます。しかし、呼び出し元がそのドキュメントを所有していることは確認できません。
rg -n "ctx\.db\.(get|patch|delete|replace)\(args\." convex --glob '!**/_generated/**'
rg -n "userId: v\.(id|string)\(" convex --glob '!**/_generated/**'
最初の grep の各該当箇所について、読み取りと書き込みの間に所有権またはメンバーシップの比較があるか確認してください。2つ目については、呼び出し元自身の userId を引数として受け取る公開関数は、クライアントが申告する本人情報を信頼していることになります。
- クライアント提供 ID によるすべての
get、patch、delete、replaceの後に、所有権またはメンバーシップの確認がある - 呼び出し元の本人情報は
ctx.authから取得し、引数として受け取らない - リスト取得では、全件スキャンしてフィルターするのではなく、所有者フィールドに
withIndexを使う
5. シークレット
rg -n -i "(sk_live|sk_test|whsec_|AKIA[0-9A-Z]{16}|-----BEGIN|api[_-]?key\s*[:=]\s*['\"][A-Za-z0-9])" convex src --glob '!**/_generated/**'
rg -n "process\.env\." src
src/ で process.env または import.meta.env を読み込むものは、ブラウザーに配布されます。そこに含めてよいのは、デプロイ URL と公開クライアント ID のみです。
-
convex/、src/、テスト、フィクスチャにシークレットのリテラルがない - シークレットは、それを使用するアクションまたは HTTP アクション内で
process.envから読み込む - 開発環境と本番環境のデプロイで異なるキーを使用する
6. HTTP とストレージ
rg -n "http\.route|httpAction\(" convex/http.ts
rg -n "storage\.(generateUploadUrl|getUrl)" convex --glob '!**/_generated/**'
- すべての
http.tsルートで、本文を解析する前に呼び出し元を検証する(Webhook 署名、Bearer トークン、またはgetUserIdentity) - ルートでは
api.*ではなくinternal.*を呼び出す -
generateUploadUrlでは認証を必須にする -
getUrlには、呼び出し元が所有するドキュメントから読み取ったストレージ ID を渡し、クライアントから渡されたストレージ ID を使わない
修正前と修正後
最もよくある問題は、認証は確認しているものの、所有権を確認していないことです。
修正前:
export const updateTask = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError("Sign in required");
// any signed in user can rename any task
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});
修正後:
import { mutation } from "./_generated/server";
import { v, ConvexError } from "convex/values";
import { getCurrentUser } from "./lib/auth";
export const updateTask = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx); // throws when signed out
const task = await ctx.db.get(args.taskId);
// same error for missing and not owned, so IDs cannot be probed
if (!task || task.userId !== user._id) {
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
}
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});
getCurrentUser は、identity.tokenIdentifier を使って by_tokenIdentifier インデックスからユーザーを検索し、identity がない場合はエラーをスローします。convex/lib/auth.ts で一度だけ定義し、すべての場所で使用してください。
よくある間違い
| 間違い | 問題が起きる理由 | 代わりに行うこと |
|---|---|---|
| React コンポーネント内でのみ認証を確認する | ダッシュボードやスクリプトから誰でも関数を呼び出せます | ハンドラー内で確認する |
「現在のユーザー」を引数 v.id("users") で指定する | クライアントは任意のユーザーの ID を渡せます | ctx.auth.getUserIdentity() から特定する |
所有権を identity.email と比較する | メールアドレスは再利用されたり、未確認だったりする可能性があります | user._id と比較する |
internalMutation ならそれだけで安全だと考える | 公開された呼び出し元が未検証の ID を渡す可能性があります | 呼び出し元も確認する |
| 該当箇所を 1 つだけ修正して終わりにする | 同じパターンが類似箇所にもよく現れます | grep で見つかった箇所をすべて修正する |
convex-security-audit に引き継ぐ
次のいずれかに該当する場合は、作業を止めて完全な監査を実行してください。
- 手順 2 または 4 で 2、3 件を超える問題が見つかった場合。パターンがシステム全体に及んでいます
- アプリでマルチテナントデータ(組織、チーム、ワークスペース)を扱っている場合
http.tsに Webhook ルート、またはユーザーデータを返すルートがある場合- ファイルをアップロードして配信している場合
- ユーザーからレポート、リリース前レビュー、またはインシデント後レビューを求められた場合
監査では、関数ごとの認証、テーブルごとのデータアクセス、HTTP の公開範囲、ストレージ、スケジューラーの信頼性、レート制限を調査し、所見をまとめたレポートを作成します。このチェックでは行いません。