Claude
Skills
Sign in
Back

api-security-review

Included with Lifetime
$97 forever

API security checklist for reviewing endpoints before deployment. Use when creating or modifying API routes to ensure proper authentication, authorization, and input validation.

Backend & APIs

What this skill does


# API Security Review Skill

## Summary
Comprehensive security checklist for API endpoint development. Ensures proper authentication, authorization, input validation, output safety, and security logging are implemented before deployment.

## When to Use
- Before merging any PR with API changes
- When creating new API endpoints
- When modifying authentication/authorization logic
- During security audits
- Code review of API routes

## Quick Checklist

### Pre-Deployment Security Audit
- [ ] **Authentication**: Route requires valid user identity
- [ ] **Authorization**: Ownership/permission checks implemented
- [ ] **Input Validation**: All inputs validated with schema (Zod/Joi/etc.)
- [ ] **Output Safety**: No sensitive data exposed in responses
- [ ] **Logging**: Security events logged appropriately
- [ ] **Rate Limiting**: Protection against abuse configured
- [ ] **Error Handling**: No system information leaked in errors

---

## Authentication

### Requirements
Every API endpoint must verify the user's identity before processing requests.

### Next.js (App Router) with Clerk
```typescript
import { auth } from '@clerk/nextjs';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // 1. Authenticate request
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    );
  }

  // Continue with authenticated request...
}
```

### Express.js with JWT
```typescript
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

function authenticateToken(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, process.env.JWT_SECRET!, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

app.get('/api/protected', authenticateToken, (req, res) => {
  // Request is authenticated
});
```

### FastAPI with OAuth2
```python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = await verify_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user

@app.get("/api/protected")
async def protected_route(current_user: User = Depends(get_current_user)):
    return {"user": current_user.email}
```

### Django REST Framework
```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_view(request):
    # request.user is authenticated
    return Response({'user': request.user.email})
```

---

## Authorization

### Resource Ownership Verification

Authentication proves WHO the user is. Authorization proves the user has permission to access the resource.

### Next.js Example
```typescript
import { auth } from '@clerk/nextjs';
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { eq } from 'drizzle-orm';

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  // 1. Authenticate
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 2. Fetch resource
  const resource = await db.query.resources.findFirst({
    where: eq(resources.id, params.id)
  });

  if (!resource) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  // 3. Authorize - Check ownership
  if (resource.ownerId !== userId) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  // 4. Return authorized data
  return NextResponse.json(resource);
}
```

### Role-Based Access Control (RBAC)
```typescript
enum Role {
  USER = 'user',
  ADMIN = 'admin',
  MODERATOR = 'moderator'
}

function requireRole(allowedRoles: Role[]) {
  return async (request: Request) => {
    const { userId } = await auth();
    if (!userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const user = await db.query.users.findFirst({
      where: eq(users.clerkId, userId)
    });

    if (!user || !allowedRoles.includes(user.role)) {
      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
    }

    return null; // Authorized
  };
}

export async function DELETE(request: Request) {
  const authError = await requireRole([Role.ADMIN, Role.MODERATOR])(request);
  if (authError) return authError;

  // User is authorized as admin or moderator
}
```

### Multi-Tenant Data Isolation
```typescript
// CRITICAL: Prevent cross-tenant data leaks

// ❌ WRONG - No tenant check
const orders = await db.query.orders.findMany({
  where: eq(orders.userId, userId)
});

// ✅ CORRECT - Tenant isolation
const user = await db.query.users.findFirst({
  where: eq(users.clerkId, userId)
});

const orders = await db.query.orders.findMany({
  where: and(
    eq(orders.userId, userId),
    eq(orders.tenantId, user.tenantId) // CRITICAL: tenant boundary
  )
});
```

---

## Input Validation

### Zod Schema Validation (TypeScript)
```typescript
import { z } from 'zod';
import { NextResponse } from 'next/server';

const updateUserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email().optional(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['user', 'admin', 'moderator']).optional(),
});

export async function PATCH(request: Request) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Parse and validate input
  const body = await request.json();
  const result = updateUserSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json({
      error: "Validation failed",
      details: result.error.issues
    }, { status: 400 });
  }

  // Safe to use validated data
  const validatedData = result.data;
  // ... update logic
}
```

### Pydantic Validation (Python)
```python
from pydantic import BaseModel, EmailStr, Field, validator
from fastapi import HTTPException

class UpdateUser(BaseModel):
    id: str = Field(..., regex=r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
    email: EmailStr | None = None
    age: int | None = Field(None, ge=0, le=150)
    role: str | None = Field(None, regex=r'^(user|admin|moderator)$')

    @validator('email')
    def email_must_not_be_disposable(cls, v):
        if v and any(domain in v for domain in ['tempmail.com', '10minutemail.com']):
            raise ValueError('Disposable email addresses not allowed')
        return v

@app.patch("/api/users")
async def update_user(user_data: UpdateUser, current_user: User = Depends(get_current_user)):
    # user_data is validated
    return {"status": "updated"}
```

### SQL Injection Prevention
```typescript
// ❌ NEVER: Raw SQL with string interpolation
const userId = request.params.id;
const query = `SELECT * FROM users WHERE id = '${userId}'`; // VULNERABLE!
db.execute(query);

// ✅ ALWAYS: Use ORM or parameterized queries
import { eq } from 'drizzle-orm';
const user = await db.query.users.findFirst({
  where: eq(users.id, userId)
});

// ✅ OR: Parameterized raw query
const [user] = await db.execute(
  'SELECT * FROM users WHERE id = ?',
  [userId]
);
```

### File Upload Validation
```typescript
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];

export async function POST(request: Request) {
  const formD

Related in Backend & APIs