| name | security-review |
| description | Security review checklist and implementation patterns for web applications — secrets management, input validation, authentication, authorization, SQL injection, XSS, CSRF, rate limiting, security headers, and sensitive data handling. Always activate when the user is implementing authentication or authorization, handling user input or file uploads, creating API endpoints, working with secrets or credentials, implementing payment features, storing or transmitting sensitive data, or integrating third-party APIs. Also activate proactively when spotting hardcoded secrets, missing auth checks, or unsafe data handling in user code. |
Security Review
Security patterns and checklists for production web applications. One missed check can compromise everything — apply this skill completely, not selectively.
Workflow
When this skill activates:
- Identify the threat surface — what is being built or reviewed? Authentication, API endpoint, file upload, data storage, payment flow?
- Apply the relevant sections — navigate to the sections that apply. Don't skip sections that seem unlikely to matter.
- Flag issues proactively — if spotted in user code, call them out immediately with the corrected pattern.
- Run the pre-deployment checklist before any production release.
- For advanced topics — file upload magic bytes validation, CSRF implementation, timing attacks, password hashing, JWT verification, SSRF prevention, LLM prompt injection, and Python patterns — see
references/advanced.md.
1. Secrets Management
const apiKey = "sk-proj-xxxxx"
const dbPassword = "password123"
import { z } from 'zod'
const env = z.object({
OPENAI_API_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
}).parse(process.env)
Checklist:
2. Input Validation
Validate every input at the boundary — before it touches your database, filesystem, or business logic. Use allow-lists, not block-lists.
import { z } from 'zod'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100).trim(),
age: z.number().int().min(0).max(150),
})
export async function POST(request: NextRequest) {
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
}
const result = CreateUserSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Validation failed', issues: result.error.issues },
{ status: 422 },
)
}
return NextResponse.json({ data: await createUser(result.data) })
}
Checklist:
3. SQL Injection Prevention
Parameterized queries are non-negotiable. String interpolation in SQL is always wrong, regardless of what the input is.
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
await db.query('SELECT * FROM users WHERE email = $1', [userEmail])
const user = await db.users.findUnique({ where: { email: userEmail } })
const { data } = await supabase.from('users').select('*').eq('email', userEmail)
Checklist:
4. Authentication & Authorization
Token Storage
localStorage.setItem('token', jwt)
res.setHeader('Set-Cookie', [
`session=${jwt}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/`,
])
Authorization Checks
Always verify the user exists and has the required permission before proceeding:
export async function deleteUser(userId: string, requesterId: string) {
const requester = await db.users.findUnique({ where: { id: requesterId } })
if (!requester) {
return NextResponse.json({ error: 'Requester not found' }, { status: 401 })
}
if (requester.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const target = await db.users.findUnique({ where: { id: userId } })
if (!target) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
await db.users.delete({ where: { id: userId } })
return NextResponse.json({ success: true })
}
Row Level Security (Supabase)
Enable RLS on every table — default-deny, then grant what's needed:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_select_own"
ON users FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "users_update_own"
ON users FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
CREATE POLICY "admins_all"
ON users FOR ALL
USING (EXISTS (
SELECT 1 FROM users WHERE id = auth.uid() AND role = 'admin'
));
Checklist:
5. XSS Prevention
React escapes variables in JSX by default — but dangerouslySetInnerHTML bypasses this entirely.
import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: userInput }} />
function SafeUserContent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
<p>{userInput}</p>
Content Security Policy
A properly configured CSP is a critical XSS defense layer. 'unsafe-inline' and 'unsafe-eval' in script-src completely defeat the protection:
const cspHeader = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; ')
Checklist:
6. Rate Limiting
Next.js App Router (Upstash)
express-rate-limit is Express middleware and does not work in Next.js App Router. Use @upstash/ratelimit with Redis:
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const rateLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '15 m'),
analytics: true,
})
export const searchLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'),
})
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, limit, remaining, reset } = await rateLimiter.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': new Date(reset).toISOString(),
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
},
},
)
}
}
Checklist:
7. Security Headers
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
},
]
export default {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
},
}
Checklist:
8. CORS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const ALLOWED_ORIGINS = [
'https://app.example.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean) as string[]
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin') ?? ''
const isAllowed = ALLOWED_ORIGINS.includes(origin)
const response = NextResponse.next()
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token')
}
if (request.method === 'OPTIONS') {
return new NextResponse(null, { status: 204, headers: response.headers })
}
return response
}
Checklist:
9. Sensitive Data Exposure
catch (error) {
console.log('Login attempt:', { email, password })
return NextResponse.json({ error: error.message, stack: error.stack }, { status: 500 })
}
catch (error) {
logger.error({ err: error, userId, action: 'login' }, 'Login failed')
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 },
)
}
logger.info({
userId: user.id,
email: user.email,
payment: { last4: card.last4, brand: card.brand },
})
Checklist:
10. Dependency Security
npm audit
npm audit fix
npm outdated
npm ci
Checklist:
Security Testing
test('protected route returns 401 without auth', async () => {
const response = await fetch('/api/protected')
expect(response.status).toBe(401)
})
test('admin route returns 403 for non-admin user', async () => {
const response = await fetch('/api/admin', {
headers: { Authorization: `Bearer ${regularUserToken}` },
})
expect(response.status).toBe(403)
})
test('rejects invalid input with 422', async () => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'not-an-email' }),
})
expect(response.status).toBe(422)
})
test('rejects malformed JSON with 400', async () => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: 'not json {{{',
})
expect(response.status).toBe(400)
})
test('enforces rate limit after threshold', async () => {
const requests = Array.from({ length: 20 }, () =>
fetch('/api/search', { headers: { 'X-Forwarded-For': '1.2.3.4' } }),
)
const responses = await Promise.all(requests)
expect(responses.some(r => r.status === 429)).toBe(true)
})
test('error response does not leak stack trace', async () => {
const response = await fetch('/api/trigger-error')
const body = await response.json()
expect(body).not.toHaveProperty('stack')
expect(body.error).not.toMatch(/at Object|node_modules/)
})
Pre-Deployment Security Checklist
Before any production release:
Resources
For advanced topics — file upload magic bytes, CSRF implementation, timing attacks, JWT verification, password hashing, SSRF, LLM prompt injection, and Python patterns — see references/advanced.md.