Convex security audit
/SKILLDeep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations
--- name: convex-security-audit displayName: Convex Security Audit description: Deep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations version: 1.0.0 author: Convex tags: [convex, security, audit, authorization, rate-limiting, protection] --- # Convex Security Audit Comprehensive security review patterns for Convex applications including authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations. ## Documentation Sources Before implementing, do not assume; fetch the latest documentation: - Primary: https://docs.convex.dev/auth/functions-auth - Production Security: https://docs.convex.dev/production - For broader context: https://docs.convex.dev/llms.txt ## Instructions ### Security Audit Areas 1. Authorization Logic - Who can do what 2. Data Access Boundaries - What data users can see 3. Action Isolation - Protecting external API calls 4. Rate Limiting - Preventing abuse 5. Sensitive Operations - Protecting critical functions ### Authorization Logic Audit #### Role-Based Access Control (RBAC) ``typescript // 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; } // Permission-based check 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; } ` ### Data Access Boundaries Audit ``typescript // 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"; // Audit: Users can only see their own data 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 []; // SECURITY: Filter by userId return await ctx.db .query("userData") .withIndex("byuser", (q) => q.eq("userId", user.id)) .collect(); }, }); // Audit: Verify ownership before returning sensitive data 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); // SECURITY: Verify ownership if (!item || item.ownerId !== user.id) { return null; // Don't reveal if item exists } return item; }, }); // Audit: Shared resources with access list 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(ct