convex-security-audit
認可ロジック、データアクセス境界、アクション分離、レート制限、機密操作の保護など、Convexアプリケーションのセキュリティを多角的に深くレビューします。認証・認可の抜け漏れや不適切なデータアクセスが懸念される場面で活用してください。
description の原文を見る
Deep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations
SKILL.md 本文
Convex Security Audit
Convex アプリケーションの包括的なセキュリティレビューパターン。認可ロジック、データアクセス境界、アクション分離、レート制限、機密操作の保護を含みます。
ドキュメントソース
実装前に、仮定せず最新のドキュメントを取得してください:
- プライマリ: https://docs.convex.dev/auth/functions-auth
- 本番環境セキュリティ: https://docs.convex.dev/production
- より広い背景: https://docs.convex.dev/llms.txt
実装手順
セキュリティ監査領域
- 認可ロジック - 誰が何ができるか
- データアクセス境界 - ユーザーが見られるデータ
- アクション分離 - 外部API呼び出しの保護
- レート制限 - 悪用防止
- 機密操作 - 重要な機能の保護
認可ロジック監査
ロールベースアクセス制御 (RBAC)
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";
type UserRole = "user" | "moderator" | "admin" | "superadmin";
const roleHierarchy: Record<UserRole, number> = {
user: 0,
moderator: 1,
admin: 2,
superadmin: 3,
};
export async function getUser(ctx: QueryCtx | MutationCtx): Promise<Doc<"users"> | null> {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query("users")
.withIndex("by_tokenIdentifier", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
}
export async function requireRole(
ctx: QueryCtx | MutationCtx,
minRole: UserRole
): Promise<Doc<"users">> {
const user = await getUser(ctx);
if (!user) {
throw new ConvexError({
code: "UNAUTHENTICATED",
message: "Authentication required",
});
}
const userRoleLevel = roleHierarchy[user.role as UserRole] ?? 0;
const requiredLevel = roleHierarchy[minRole];
if (userRoleLevel < requiredLevel) {
throw new ConvexError({
code: "FORBIDDEN",
message: `Role '${minRole}' or higher required`,
});
}
return user;
}
// パーミッションベースのチェック
type Permission = "read:users" | "write:users" | "delete:users" | "admin:system";
const rolePermissions: Record<UserRole, Permission[]> = {
user: ["read:users"],
moderator: ["read:users", "write:users"],
admin: ["read:users", "write:users", "delete:users"],
superadmin: ["read:users", "write:users", "delete:users", "admin:system"],
};
export async function requirePermission(
ctx: QueryCtx | MutationCtx,
permission: Permission
): Promise<Doc<"users">> {
const user = await getUser(ctx);
if (!user) {
throw new ConvexError({ code: "UNAUTHENTICATED", message: "Authentication required" });
}
const userRole = user.role as UserRole;
const permissions = rolePermissions[userRole] ?? [];
if (!permissions.includes(permission)) {
throw new ConvexError({
code: "FORBIDDEN",
message: `Permission '${permission}' required`,
});
}
return user;
}
データアクセス境界監査
// convex/data.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getUser, requireRole } from "./lib/auth";
import { ConvexError } from "convex/values";
// 監査: ユーザーは自分自身のデータのみ表示可能
export const getMyData = query({
args: {},
returns: v.array(v.object({
_id: v.id("userData"),
content: v.string(),
})),
handler: async (ctx) => {
const user = await getUser(ctx);
if (!user) return [];
// セキュリティ: userId でフィルタリング
return await ctx.db
.query("userData")
.withIndex("by_user", (q) => q.eq("userId", user._id))
.collect();
},
});
// 監査: 機密データ返却前に所有権を確認
export const getSensitiveItem = query({
args: { itemId: v.id("sensitiveItems") },
returns: v.union(v.object({
_id: v.id("sensitiveItems"),
secret: v.string(),
}), v.null()),
handler: async (ctx, args) => {
const user = await getUser(ctx);
if (!user) return null;
const item = await ctx.db.get(args.itemId);
// セキュリティ: 所有権を確認
if (!item || item.ownerId !== user._id) {
return null; // アイテムが存在するかどうかを明かさない
}
return item;
},
});
// 監査: アクセスリスト付き共有リソース
export const getSharedDocument = query({
args: { docId: v.id("documents") },
returns: v.union(v.object({
_id: v.id("documents"),
content: v.string(),
accessLevel: v.string(),
}), v.null()),
handler: async (ctx, args) => {
const user = await getUser(ctx);
const doc = await ctx.db.get(args.docId);
if (!doc) return null;
// 公開ドキュメント
if (doc.visibility === "public") {
return { ...doc, accessLevel: "public" };
}
// 非公開の場合は認証が必須
if (!user) return null;
// 所有者は完全なアクセス権
if (doc.ownerId === user._id) {
return { ...doc, accessLevel: "owner" };
}
// 共有アクセスを確認
const access = await ctx.db
.query("documentAccess")
.withIndex("by_doc_and_user", (q) =>
q.eq("documentId", args.docId).eq("userId", user._id)
)
.unique();
if (!access) return null;
return { ...doc, accessLevel: access.level };
},
});
アクション分離監査
// convex/actions.ts
"use node";
import { action, internalAction } from "./_generated/server";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";
import { ConvexError } from "convex/values";
// セキュリティ: APIキーをレスポンスで露出させない
export const callExternalAPI = action({
args: { query: v.string() },
returns: v.object({ result: v.string() }),
handler: async (ctx, args) => {
// ユーザーが認証されていることを確認
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Authentication required");
}
// 環境からAPIキーを取得(ハードコードしない)
const apiKey = process.env.EXTERNAL_API_KEY;
if (!apiKey) {
throw new Error("API key not configured");
}
// 監査証跡のために使用をログに記録
await ctx.runMutation(internal.audit.logAPICall, {
userId: identity.tokenIdentifier,
endpoint: "external-api",
timestamp: Date.now(),
});
const response = await fetch("https://api.example.com/query", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: args.query }),
});
if (!response.ok) {
// 外部APIのエラー詳細を露出させない
throw new ConvexError("External service unavailable");
}
const data = await response.json();
// レスポンスを返却前にサニタイズ
return { result: sanitizeResponse(data) };
},
});
// 内部アクション - クライアントに露出しない
export const _processPayment = internalAction({
args: {
userId: v.id("users"),
amount: v.number(),
paymentMethodId: v.string(),
},
returns: v.object({ success: v.boolean(), transactionId: v.optional(v.string()) }),
handler: async (ctx, args) => {
const stripeKey = process.env.STRIPE_SECRET_KEY;
// Stripe で決済を処理
// これは公開アクションとして露出されるべきではない
return { success: true, transactionId: "txn_xxx" };
},
});
レート制限監査
// convex/rateLimit.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
const RATE_LIMITS = {
message: { requests: 10, windowMs: 60000 }, // 1分あたり10回
upload: { requests: 5, windowMs: 300000 }, // 5分あたり5回
api: { requests: 100, windowMs: 3600000 }, // 1時間あたり100回
};
export const checkRateLimit = mutation({
args: {
userId: v.string(),
action: v.union(v.literal("message"), v.literal("upload"), v.literal("api")),
},
returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),
handler: async (ctx, args) => {
const limit = RATE_LIMITS[args.action];
const now = Date.now();
const windowStart = now - limit.windowMs;
// ウィンドウ内のリクエスト数をカウント
const requests = await ctx.db
.query("rateLimits")
.withIndex("by_user_and_action", (q) =>
q.eq("userId", args.userId).eq("action", args.action)
)
.filter((q) => q.gt(q.field("timestamp"), windowStart))
.collect();
if (requests.length >= limit.requests) {
const oldestRequest = requests[0];
const retryAfter = oldestRequest.timestamp + limit.windowMs - now;
return { allowed: false, retryAfter };
}
// このリクエストを記録
await ctx.db.insert("rateLimits", {
userId: args.userId,
action: args.action,
timestamp: now,
});
return { allowed: true };
},
});
// ミューテーションで使用
export const sendMessage = mutation({
args: { content: v.string() },
returns: v.id("messages"),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError("Authentication required");
// レート制限をチェック
const rateCheck = await checkRateLimit(ctx, {
userId: identity.tokenIdentifier,
action: "message",
});
if (!rateCheck.allowed) {
throw new ConvexError({
code: "RATE_LIMITED",
message: `Too many requests. Try again in ${Math.ceil(rateCheck.retryAfter! / 1000)} seconds`,
});
}
return await ctx.db.insert("messages", {
content: args.content,
authorId: identity.tokenIdentifier,
createdAt: Date.now(),
});
},
});
機密操作の保護
// convex/admin.ts
import { mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { requireRole, requirePermission } from "./lib/auth";
import { internal } from "./_generated/api";
// 危険な操作の二要素確認
export const deleteAllUserData = mutation({
args: {
userId: v.id("users"),
confirmationCode: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// スーパーアドミンが必須
const admin = await requireRole(ctx, "superadmin");
// 確認コードを検証
const confirmation = await ctx.db
.query("confirmations")
.withIndex("by_admin_and_code", (q) =>
q.eq("adminId", admin._id).eq("code", args.confirmationCode)
)
.filter((q) => q.gt(q.field("expiresAt"), Date.now()))
.unique();
if (!confirmation || confirmation.action !== "delete_user_data") {
throw new ConvexError("Invalid or expired confirmation code");
}
// 再利用を防ぐため確認を削除
await ctx.db.delete(confirmation._id);
// 削除をスケジュール(インラインで実行しない)
await ctx.scheduler.runAfter(0, internal.admin._performDeletion, {
userId: args.userId,
requestedBy: admin._id,
});
// 監査ログ
await ctx.db.insert("auditLogs", {
action: "delete_user_data",
targetUserId: args.userId,
performedBy: admin._id,
timestamp: Date.now(),
});
return null;
},
});
// 機密操作の確認コード生成
export const requestDeletionConfirmation = mutation({
args: { userId: v.id("users") },
returns: v.string(),
handler: async (ctx, args) => {
const admin = await requireRole(ctx, "superadmin");
const code = generateSecureCode();
await ctx.db.insert("confirmations", {
adminId: admin._id,
code,
action: "delete_user_data",
targetUserId: args.userId,
expiresAt: Date.now() + 5 * 60 * 1000, // 5分
});
// 本番環境では、安全なチャネル(メール、SMS)でコードを送信
return code;
},
});
使用例
完全な監査証跡システム
// convex/audit.ts
import { mutation, query, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { getUser, requireRole } from "./lib/auth";
const auditEventValidator = v.object({
_id: v.id("auditLogs"),
_creationTime: v.number(),
action: v.string(),
userId: v.optional(v.string()),
resourceType: v.string(),
resourceId: v.string(),
details: v.optional(v.any()),
ipAddress: v.optional(v.string()),
timestamp: v.number(),
});
// 内部: 監査イベントをログに記録
export const logEvent = internalMutation({
args: {
action: v.string(),
userId: v.optional(v.string()),
resourceType: v.string(),
resourceId: v.string(),
details: v.optional(v.any()),
},
returns: v.id("auditLogs"),
handler: async (ctx, args) => {
return await ctx.db.insert("auditLogs", {
...args,
timestamp: Date.now(),
});
},
});
// 管理者: 監査ログを表示
export const getAuditLogs = query({
args: {
resourceType: v.optional(v.string()),
userId: v.optional(v.string()),
limit: v.optional(v.number()),
},
returns: v.array(auditEventValidator),
handler: async (ctx, args) => {
await requireRole(ctx, "admin");
let query = ctx.db.query("auditLogs");
if (args.resourceType) {
query = query.withIndex("by_resource_type", (q) =>
q.eq("resourceType", args.resourceType)
);
}
return await query
.order("desc")
.take(args.limit ?? 100);
},
});
ベストプラクティス
- 明示的に指示された場合を除き、
npx convex deployを実行しない - 明示的に指示された場合を除き、git コマンドを実行しない
- 多層防御を実装する(複数のセキュリティレイヤー)
- 機密操作すべてを監査証跡のためにログに記録
- 破壊的な操作には確認コードを使用
- ユーザー向けエンドポイントすべてにレート制限を実装
- 内部API キーやエラーを露出させない
- アクセスパターンを定期的にレビュー
よくある落とし穴
- 単一障害点 - 複数の認証チェックを実装
- 監査ログの欠落 - 機密操作はすべてログに記録
- クライアントデータの信頼 - サーバー側で常に検証
- エラー詳細の露出 - エラーメッセージをサニタイズ
- レート制限なし - 常にレート制限を実装
参照
- Convex ドキュメント: https://docs.convex.dev/
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
- Functions Auth: https://docs.convex.dev/auth/functions-auth
- 本番環境セキュリティ: https://docs.convex.dev/production
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- waynesutton
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/waynesutton/convexskills / ライセンス: Apache-2.0
関連スキル
hugging-face-trackio
Trackioを使用してMLトレーニング実験を追跡・可視化できます。トレーニング中のメトリクスログ記録(Python API)、トレーニング診断のアラート発火、ログされたメトリクスの取得・分析(CLI)が必要な場合に活用してください。リアルタイムダッシュボード表示、Webhookを使用したアラート、HF Space同期、自動化向けのJSON出力に対応しています。
btc-bottom-model
ビットコインのサイクルタイミングモデルで、加重スコアリングシステムを搭載しています。日次パルス(4指標、32ポイント)とウィークリー構造(9指標、68ポイント)の2カテゴリーにわたる13の指標を追跡し、0~100のマーケットヒートスコアを算出します。ETFフロー、ファンディングレート、ロング/ショート比率、恐怖・貪欲指数、LTH-MVRV、NUPL、SOPR(LTH+STH)、LTH供給率、移動平均倍率(365日MA、200週MA)、週次RSI、出来高トレンドに対応します。市場サイクル全体を通じて買いと売りの両方の推奨を提供します。ビットコインの底値拾い、BTCサイクルポジション、買い時・売り時、オンチェーン指標、MVRV、NUPL、SOPR、LTH動向、ETFの流出入、ファンディングレート、恐怖指数、ビットコインが過熱状態か、マイナーコスト、暗号資産市場のセンチメント、BTCのポジションサイジング、「今ビットコインを買うべきか」「BTCが天井をつけているか」「オンチェーン指標は何を示しているか」といった質問の際にこのスキルを活用します。
protein_solubility_optimization
タンパク質の溶解性最適化 - タンパク質の溶解性を最適化します。タンパク質の特性を計算し、溶解性と親水性を予測し、有効な変異を提案します。タンパク質配列の特性計算、タンパク質機能の予測、親水性計算、ゼロショット配列予測を含むタンパク質エンジニアリング業務に使用できます。3つのSCPサーバーから4つのツールを統合しています。
research-lookup
Parallel Chat APIまたはPerplexity sonar-pro-searchを使用して、最新の研究情報を検索できます。学術論文の検索にも対応しています。クエリは自動的に最適なバックエンドにルーティングされるため、論文の検索、研究データの収集、科学情報の検証に活用できます。
tree-formatting
ggtree(R)またはiTOL(ウェブ)を使用して、系統樹の可視化とフォーマットを行います。系統樹を図として描画する際、ツリーレイアウトの選択、分類学に基づく枝やラベルの色付け、クレードの折りたたみ、サポート値の表示、またはツリーへのオーバーレイ追加が必要な場合に使用してください。系統推定(protein-phylogenyスキルを使用)やドメイン注釈(今後の独立したスキル)には使用しないでください。
querying-indonesian-gov-data
インドネシア政府の50以上のAPIとデータソースに接続できます。BPJPH(ハラール認証)、BOM(食品安全)、OJK(金融適正性)、BPS(統計)、BMKG(気象・地震)、インドネシア中央銀行(為替レート)、IDX(株式)、CKAN公開データポータル、pasal.id(第三者法MCP)に対応しています。インドネシア政府データを活用したアプリ開発、.go.idウェブサイトのスクレイピング、ハラール認証の確認、企業の法的適正性の検証、金融機関ステータスの照会、またはインドネシアMCPサーバーへの接続時に使用できます。CSRF処理、CKAN API使用方法、IP制限回避など、すぐに実行可能なPythonパターンを含んでいます。