mcpField
C
convex-security-check

convex-security-check

github.com/waynesuttonセキュリティ4052.7k 回インストール4 週間前に更新
リポジトリ

convex-security-check について

Convex プロジェクトの `convex/` を対象に、公開関数、認証、バリデーション、所有権、シークレット管理を確認するセキュリティチェック用スキルです。チェックリストと grep で一般的な問題を探し、完全な監査の代替にはなりません。

主な機能

  1. 01公開関数と内部関数の区分確認
  2. 02認証とデータ所有権の確認
  3. 03引数と戻り値のバリデーション確認
  4. 04シークレットと HTTP ルートのリスク検出
  5. 05完全な監査へ移行する条件の提示

活用シーン

  1. 01`convex/` の変更をマージする前の確認
  2. 02公開関数の追加や改名後のリスク確認
  3. 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 の公開範囲、ストレージ、スケジューラーの信頼性、レート制限を調査し、所見をまとめたレポートを作成します。このチェックでは行いません。

ドキュメント

コメント