npx claudepluginhub cwinvestments/memstack --plugin memstackThis skill uses the workspace's default tool permissions.
*Audit Next.js API routes for authentication, authorization, validation, and common vulnerabilities.*
Audits web applications and REST APIs for OWASP Top 10 vulnerabilities including broken access control, authentication failures, data protection, and configuration issues. Use when reviewing code, auth/authz, APIs, or before deployment.
Identifies OWASP API Security Top 10 (2023) vulnerabilities like BOLA in REST, GraphQL, gRPC APIs during audits, with code examples and detection patterns for Express, Flask, Spring Boot, Go.
Audits web applications and REST APIs for OWASP Top 10 vulnerabilities including broken access control, authentication failures, and data protection. Use when reviewing code, auth/authz, APIs, or before deployment.
Share bugs, ideas, or general feedback.
Audit Next.js API routes for authentication, authorization, validation, and common vulnerabilities.
When this skill activates, output:
π‘οΈ API Audit β Checking API Route Security...
Then execute the protocol below.
| Context | Status |
|---|---|
| User asks to audit/check API routes | ACTIVE β full audit |
| User mentions API security | ACTIVE β full audit |
| User asks about endpoint protection | ACTIVE β full audit |
| User is writing a new API route | DORMANT β let them finish first |
| Non-Next.js project | DORMANT β not applicable (adapt if Express/Fastify detected) |
Find all API route files in the project:
App Router (Next.js 13+):
find . -path "*/app/api/*/route.ts" -o -path "*/app/api/*/route.js"
Each file exports named functions: GET, POST, PUT, PATCH, DELETE.
Pages Router (legacy):
find . -path "*/pages/api/*.ts" -o -path "*/pages/api/*.js"
Each file exports a default handler.
Middleware:
find . -name "middleware.ts" -o -name "middleware.js" | head -5
Check if global auth middleware exists (reduces per-route auth requirements).
Server Actions (Next.js 14+):
grep -r "'use server'" --include="*.ts" --include="*.tsx" -l
Server actions are also attack surface β treat as API routes. For each server action function, verify it performs authentication before accessing data. Server actions are callable from any client component and receive no automatic auth β they are functionally identical to unauthenticated POST endpoints unless the function explicitly calls getSession(), getAuthContext(), or equivalent.
Compile a list of all routes with their HTTP methods and file paths.
For each route, determine if it verifies the caller is authenticated:
Search for auth patterns in each route file:
getAuthContext / getSession / getServerSession / auth() β framework authgetToken / verifyToken / jwt.verify β manual JWTcookies().get with session validation β cookie-based authheaders().get('authorization') with validation β bearer tokencreateRouteHandlerClient / createServerComponentClient β Supabase authClassify each route:
| Status | Meaning |
|---|---|
| β Authenticated | Auth check found before data access |
| π΄ No Auth | No authentication pattern detected |
| βΉοΈ Public | Route is intentionally public (webhooks, health checks, public data) |
| β οΈ Middleware-only | Auth handled by middleware β verify matcher covers this route |
Flag as CRITICAL if a route performs database writes or returns user-specific data with no auth.
Known public route patterns (classify as βΉοΈ INFO, not CRITICAL):
/api/health, /api/status β health checks/api/webhooks/* β external webhooks (need signature verification instead)/api/auth/* β auth flow endpoints (login, callback, register)/api/public/* β explicitly named public routes/api/cron/* β cron jobs (need secret verification instead)For authenticated routes, verify they check what the user can access:
Search for authorization patterns:
verifyOrgAccess / checkOrgMembership / requireRole β org-level authzuser.id against resource user_id / owner_id β ownership checkuser.role === 'admin' or similarFlag as WARNING if:
organization_id from request body instead of deriving from sessionPattern to enforce:
// BAD β trusts client-provided org ID
const { orgId } = await req.json();
const data = await db.from('documents').select().eq('org_id', orgId);
// GOOD β derives org from authenticated session
const { orgId } = await getAuthContext(req);
const data = await db.from('documents').select().eq('org_id', orgId);
For routes that accept request body or query params:
Search for validation patterns:
z.object / z.string() / .parse( / .safeParse( β ZodJoi.object / .validate( β Joiyup.object / .validate( β Yupbody. or req.json() followed by manual type checks β weak validationFlag as WARNING if:
req.json() or request.body without schema validationFlag as CRITICAL if:
For public-facing routes:
Search for rate limiting patterns:
rateLimit / rateLimiter / limiter imports@upstash/ratelimit β serverless rate limitingX-RateLimit header settingmiddleware.ts)Flag as WARNING if:
For POST/PUT/PATCH routes that accept request bodies:
Search for size enforcement patterns:
Content-Length header checks before parsing bodybodyParser config with sizeLimit optionexport const config = { api: { bodyParser: { sizeLimit: '...' } } } β Next.js Pages Routerrequest.text() / request.json() is called without upstream size limitsFlag as WARNING if:
await request.json() on an unbounded body β a malicious client can send gigabytes of JSON, causing memory exhaustion (DoS)Note: Next.js App Router does NOT enforce a default body size limit on route handlers. Unlike the Pages Router (which defaults to 1MB via bodyParser), App Router passes the raw request through. Projects must enforce limits explicitly.
Correct pattern:
// Check Content-Length before parsing
const contentLength = parseInt(request.headers.get('content-length') || '0');
if (contentLength > 1_000_000) { // 1MB
return NextResponse.json({ error: 'Request too large' }, { status: 413 });
}
const body = await request.json();
For routes that create payments, charges, transfers, or financial transactions:
Search for idempotency patterns:
idempotencyKey / idempotency_key / Idempotency-Key headerstripe.paymentIntents.create({}, { idempotencyKey: ... }) β built-in supportidempotency_key field in request bodies β built-in supportIdentify payment mutation routes: Search for routes that call:
stripe.paymentIntents.create, stripe.charges.create, stripe.invoices.paystripe.checkout.sessions.create, stripe.subscriptions.createsquareClient.payments.create, squareClient.orders.createinvoices, payments, orders, or transactions tablesFlag as WARNING if:
Correct pattern:
// Stripe β pass idempotency key from client or generate deterministically
const session = await stripe.checkout.sessions.create(
{ ... },
{ idempotencyKey: `order-${orderId}-${timestamp}` }
);
Search for dangerous query patterns:
"SELECT * FROM " + table β CRITICAL.rpc() calls with unsanitized user input β WARNINGprisma.$queryRawUnsafe or sql.unsafe β CRITICALSafe patterns (do not flag):
.from().select().eq() chain β safe by design$1, $2 placeholdersSearch for sensitive data in responses:
return NextResponse.json(user) β may include password hashselect('*') results without column filtering β WARNINGFields that should never appear in API responses:
password, password_hash, hashed_password, secret, token, refresh_token, api_key, private_key, ssn, credit_card
Search each route file for these field names, then check if they appear in return/response paths.
Search for CORS patterns:
Access-Control-Allow-Origin: * β overly permissive (WARNING)Access-Control-Allow-Credentials: true with wildcard origin β CRITICALnext.config.js headers configuration for CORSCheck next.config.js or next.config.mjs:
grep -A5 "Access-Control\|headers\(\)" next.config.*
Search for error patterns in each route:
catch (e) { return NextResponse.json(e) } β leaks stack traces (WARNING)catch (e) { return NextResponse.json({ error: e.message }) } β leaks internal errors (WARNING)console.error with full error objects in production β log exposure (INFO)Correct pattern:
catch (error) {
console.error('Route /api/items failed:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
For webhook routes (/api/webhooks/*):
stripe.webhooks.constructEvent(body, sig, secret) β CRITICAL if missingX-Hub-Signature-256 headerreq.text() not req.json()) for signature verificationFor file upload routes:
For DELETE routes:
π‘οΈ API Security Audit Report
Project: <project-name>
Routes found: <count>
Server actions: <count>
Global middleware auth: <yes/no>
## Route Audit
| Route | Method | Auth | Authz | Validation | Risk | Issues |
|-------|--------|------|-------|------------|------|--------|
| /api/users | GET | β
| β
| β
| β
OK | β |
| /api/users | POST | β
| β
| π΄ | β οΈ WARN | No input validation |
| /api/items/[id] | DELETE | π΄ | π΄ | β | π΄ CRIT | No auth, no ownership check |
| /api/webhooks/stripe | POST | βΉοΈ | β | β | β
OK | Signature verified |
| /api/admin/users | GET | β
| β οΈ | β
| β οΈ WARN | No role check for admin route |
## Critical Issues
1. **DELETE /api/items/[id]** β No authentication. Any request can delete any item.
β Fix: Add `getAuthContext(req)` and verify `item.user_id === user.id` before deleting.
2. **POST /api/upload** β No file size limit. Server vulnerable to resource exhaustion.
β Fix: Add size limit config or check `Content-Length` header.
## Warnings
1. **POST /api/users** β Request body parsed without schema validation.
β Fix: Add Zod schema and parse before processing.
2. **GET /api/admin/users** β Authenticated but no admin role verification.
β Fix: Add role check before returning data.
## Info
1. **Supabase RLS active** β Authorization may be handled at database layer for some routes.
Verify RLS policies cover the same access patterns. Run `rls-checker` for full RLS audit.
## Summary
- π΄ Critical: <count>
- β οΈ Warning: <count>
- βΉοΈ Info: <count>
- β
OK: <count>
- Total routes: <count>
## Checklist
- [ ] All non-public routes authenticate callers
- [ ] All data-access routes verify ownership/org membership
- [ ] All POST/PUT/PATCH routes validate input with schema
- [ ] Login/register routes have rate limiting
- [ ] Webhook routes verify signatures
- [ ] No raw SQL with user input
- [ ] API responses exclude sensitive fields
- [ ] Error responses don't leak stack traces
- [ ] CORS configured for specific origins, not wildcard
- [ ] POST/PUT/PATCH routes enforce request body size limits
- [ ] Payment-creating routes use idempotency keys
- [ ] Server actions verify auth before data access
For each CRITICAL and WARNING issue, provide:
@upstash/ratelimit for rate limiting)Offer to apply fixes directly if the user approves.
| Level | Meaning | Action |
|---|---|---|
| π΄ CRITICAL | Active vulnerability exploitable without auth | Fix immediately |
| β οΈ WARNING | Missing defense layer or weak pattern | Fix before production |
| βΉοΈ INFO | Acceptable pattern worth verifying | Review and confirm intentional |
| β OK | Properly secured | No action needed |