.claude/skills/api-security-best-practices/SKILL.md
Secure API design patterns for Next.js App Router with Supabase. Authentication, authorization, input validation, rate limiting, and OWASP API Top 10. Adapted for MGM-Web multi-tenant architecture.
npx skillsauth add vitoropereira/claude-starter-kit api-security-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide for building secure APIs in Next.js App Router with Supabase Auth. Covers authentication, authorization, input validation, and protection against OWASP API Top 10.
src/app/api/// src/app/api/example/route.ts
import { NextResponse } from "next/server";
import { getOrgContextFromCookies } from "@/lib/auth/get-org-context";
import { createClient } from "@/lib/supabase/server";
export async function GET(req: Request) {
// Step 1: Authenticate
const org = await getOrgContextFromCookies();
if (!org) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Step 2: Authorize (for admin-only actions)
if (!org.canManageUsers) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Step 3: Query with tenant isolation
const supabase = await createClient();
const { data } = await supabase
.from("groups")
.select("*")
.eq("group_owner", org.organizationRootUserId); // CRITICAL
return NextResponse.json({ data });
}
| Scenario | Helper |
|----------|--------|
| Dashboard API routes (cookie auth) | getOrgContextFromCookies() |
| Routes with Bearer token | getOrgContext(req) |
| User without org context (invite flow) | supabase.auth.getUser() |
| Server-side admin operations | createAdminClient() |
interface OrgContext {
userId: number; // User's internal ID
organizationRootUserId: number; // Org owner — use for group_owner queries
levelOrder: number; // 1=Owner, 2=Admin, 3=Member
canManageUsers: boolean; // Owner + Admin
canCreateGroups: boolean; // Owner + Admin
canViewAllGroups: boolean; // Owner + Admin
}
// ❌ BAD: No auth check at all
export async function GET(req: Request) {
const supabase = await createClient();
const { data } = await supabase.from("groups").select("*");
return NextResponse.json({ data }); // Exposes ALL groups!
}
// ❌ BAD: Auth but no tenant isolation
export async function GET(req: Request) {
const org = await getOrgContextFromCookies();
if (!org) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { data } = await supabase.from("groups").select("*");
// Missing .eq("group_owner", org.organizationRootUserId)!
return NextResponse.json({ data });
}
// ❌ BAD: Auth but no permission check for admin action
export async function DELETE(req: Request) {
const org = await getOrgContextFromCookies();
if (!org) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// Missing: if (!org.canManageUsers) return 403
await supabase.from("groups").delete().eq("id", groupId);
}
import { z } from "zod";
const AddGroupSchema = z.object({
inviteCode: z.string()
.min(1, "Invite code required")
.max(100, "Invite code too long")
.regex(/^[a-zA-Z0-9]+$/, "Invalid characters"),
});
export async function POST(req: Request) {
const org = await getOrgContextFromCookies();
if (!org) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Validate input
const body = await req.json();
const parsed = AddGroupSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid input", details: parsed.error.flatten() },
{ status: 400 }
);
}
// Use validated data
const { inviteCode } = parsed.data;
// ... proceed with safe data
}
| Data Type | Zod Pattern |
|-----------|-------------|
| IDs (numeric) | z.number().int().positive() |
| IDs (string from params) | z.string().regex(/^\d+$/) |
| Email | z.string().email() |
| Pagination cursor | z.string().optional() |
| Search query | z.string().max(200).optional() |
| Enum values | z.enum(["active", "canceled", "trialing"]) |
// ❌ BAD: Using raw request body
const { groupId, userId } = await req.json();
await supabase.from("groups").update({ name }).eq("id", groupId);
// ✅ GOOD: Validate + use org context for ownership
const parsed = UpdateGroupSchema.safeParse(await req.json());
if (!parsed.success) return NextResponse.json({ error: "Invalid" }, { status: 400 });
await supabase
.from("groups")
.update({ name: parsed.data.name })
.eq("id", parsed.data.groupId)
.eq("group_owner", org.organizationRootUserId); // Tenant isolation
// ❌ BAD: Exposes database structure
catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
// "duplicate key value violates unique constraint "users_email_key""
}
// ✅ GOOD: Generic error + server-side logging
catch (error) {
console.error("[API] Group creation error:", error);
return NextResponse.json(
{ error: "Failed to create group" },
{ status: 500 }
);
}
| Status | When | Response |
|--------|------|----------|
| 400 | Invalid input | { error: "Invalid input", details: zodErrors } |
| 401 | Not authenticated | { error: "Unauthorized" } |
| 403 | Not authorized (wrong role) | { error: "Forbidden" } |
| 404 | Resource not found (or not owned) | { error: "Not found" } |
| 500 | Server error | { error: "Internal server error" } (never expose details) |
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const rateLimit = new Map<string, { count: number; resetAt: number }>();
function isRateLimited(ip: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetAt) {
rateLimit.set(ip, { count: 1, resetAt: now + windowMs });
return false;
}
entry.count++;
return entry.count > limit;
}
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/auth")) {
const ip = request.headers.get("x-forwarded-for") || "unknown";
if (isRateLimited(ip, 10, 15 * 60 * 1000)) { // 10 req / 15 min
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429, headers: { "Retry-After": "900" } }
);
}
}
}
For production, use Vercel's built-in DDoS protection and configure:
vercel.json// next.config.ts
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-XSS-Protection", value: "1; mode=block" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
| # | Vulnerability | MGM-Web Risk | Mitigation |
|---|--------------|-------------|------------|
| 1 | Broken Object Level Authorization | Missing group_owner filter | Always filter by org.organizationRootUserId |
| 2 | Broken Authentication | Supabase session bypass | Use getOrgContextFromCookies() consistently |
| 3 | Broken Object Property Level Authorization | Returning sensitive fields | Use .select() to pick only needed columns |
| 4 | Unrestricted Resource Consumption | Large group lists, analytics queries | Pagination with nextCursor, limit params |
| 5 | Broken Function Level Authorization | Member accessing admin functions | Check org.canManageUsers / org.canCreateGroups |
| 6 | Unrestricted Access to Sensitive Business Flows | Group add abuse | Rate limit /api/groups/add |
| 7 | SSRF | AI agent fetching external URLs | Validate URLs in AI pipeline |
| 8 | Security Misconfiguration | Missing CORS, headers | Security headers in next.config.ts |
| 9 | Improper Inventory Management | Undocumented API routes | Keep route structure documented in CLAUDE.md |
| 10 | Unsafe Consumption of APIs | MGM Backend API | Validate responses from mgmApi calls |
getOrgContextFromCookies() or appropriate auth helpercanManageUsers, canCreateGroups)org.organizationRootUserId (tenant isolation).select() (no password hashes, internal IDs)supabase_auth_id or auth_id exposed to clientidor-testing — IDOR vulnerability testing with MGM-specific examplessecurity-best-practices — Next.js + React security specsecurity-threat-model — Repository-grounded threat modelingbroken-authentication — Auth bypass testingtop-web-vulnerabilities — OWASP-aligned vulnerability referencetesting
Draft cold emails, warm intro blurbs, follow-ups, update emails, and investor communications for fundraising. Use when the user wants outreach to angels, VCs, strategic investors, or accelerators and needs concise, personalized, investor-facing messaging.
testing
Create and update pitch decks, one-pagers, investor memos, accelerator applications, financial models, and fundraising materials. Use when the user needs investor-facing documents, projections, use-of-funds tables, milestone plans, or materials that must stay internally consistent across multiple fundraising assets.
tools
iMessage/SMS CLI for listing chats, history, and sending messages via Messages.app.
development
This skill should be used when the user asks to "test for insecure direct object references," "find IDOR vulnerabilities," "exploit broken access control," "enumerate user IDs or object references," or "bypass authorization to access other users' data." Adapted for MGM-Web multi-tenant architecture.