| name | security-review |
| description | Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides a comprehensive OWASP security checklist and patterns. |
Security Review Skill
This skill ensures all code follows security best practices and identifies potential vulnerabilities.
When to Activate
- Implementing authentication or authorization
- Handling user input or file uploads
- Creating new API endpoints
- Working with secrets or credentials
- Implementing payment features
- Storing or transmitting sensitive data
- Integrating third-party APIs
Security Checklist
1. Secrets Management
FAIL: NEVER Do This
const apiKey = "sk-proj-xxxxx"
const dbPassword = "password123"
PASS: ALWAYS Do This
const apiKey = process.env.OPENAI_API_KEY
const dbUrl = process.env.DATABASE_URL
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
Verification Steps
2. Input Validation
Always Validate User Input
import { z } from 'zod'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150)
})
export async function createUser(input: unknown) {
try {
const validated = CreateUserSchema.parse(input)
return await db.users.create(validated)
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.issues }
}
throw error
}
}
File Upload Validation
function validateFileUpload(file: File) {
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)')
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type')
}
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
if (!extension || !allowedExtensions.includes(extension)) {
throw new Error('Invalid file extension')
}
return true
}
Verification Steps
3. SQL Injection Prevention
FAIL: NEVER Concatenate SQL
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
PASS: ALWAYS Use Parameterized Queries
await db.query(
'SELECT * FROM users WHERE email = $1',
[userEmail]
)
const user = await db.users.findUnique({
where: { email: userEmail }
})
Verification Steps
4. Authentication & Authorization
JWT Token Handling
localStorage.setItem('token', token)
res.setHeader('Set-Cookie',
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
Authorization Checks
export async function deleteUser(userId: string, requesterId: string) {
const requester = await db.users.findUnique({
where: { id: requesterId }
})
if (requester.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
await db.users.delete({ where: { id: userId } })
}
Row-Level / Tenant Authorization
Enforce per-row / per-tenant access control in the database or query layer so a
user can never read or mutate another user's (or tenant's) rows — even if an
application-layer check is missed. This can be done with database row-level
security policies, or by always scoping queries to the authenticated
user/tenant id in your data-access layer.
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users view own data"
ON users FOR SELECT
USING (current_user_id() = id);
CREATE POLICY "Users update own data"
ON users FOR UPDATE
USING (current_user_id() = id);
Verification Steps
5. XSS Prevention
Sanitize HTML
import DOMPurify from 'isomorphic-dompurify'
function renderUserContent(html: string) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: []
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
Content Security Policy
Start strict and loosen only with a documented removal plan. Do not default to
'unsafe-inline' or 'unsafe-eval'; they neutralize much of CSP's protection
and should be treated as temporary compatibility debt.
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\s{2,}/g, ' ').trim()
}
]
Verification Steps
6. CSRF Protection
CSRF Tokens
import { csrf } from '@/lib/csrf'
export async function POST(request: Request) {
const token = request.headers.get('X-CSRF-Token')
if (!csrf.verify(token)) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
)
}
}
SameSite Cookies
res.setHeader('Set-Cookie',
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
Verification Steps
7. Rate Limiting
API Rate Limiting
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests'
})
app.use('/api/', limiter)
Expensive Operations
const searchLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
message: 'Too many search requests'
})
app.use('/api/search', searchLimiter)
Verification Steps
8. Sensitive Data Exposure
Logging
console.log('User login:', { email, password })
console.log('Payment:', { cardNumber, cvv })
console.log('User login:', { email, userId })
console.log('Payment:', { last4: card.last4, userId })
Error Messages
catch (error) {
return NextResponse.json(
{ error: error.message, stack: error.stack },
{ status: 500 }
)
}
catch (error) {
console.error('Internal error:', error)
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
)
}
Verification Steps
9. Signature Verification
Never trust a payload just because it arrived — verify its signature before
acting on it. This applies to inbound webhooks, signed API requests, and any
token or message whose authenticity matters. Use your platform's stdlib crypto
and always compare digests with a constant-time function.
HMAC Webhook Signature Verification
import crypto from 'node:crypto'
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Verifying a Signed Payload / Ownership
function verifySignedPayload(
message: string,
signature: string,
publicKeyPem: string
): boolean {
try {
const verifier = crypto.createVerify('SHA256')
verifier.update(message)
verifier.end()
return verifier.verify(publicKeyPem, signature, 'base64')
} catch {
return false
}
}
Verification Steps
10. Dependency Security
Regular Updates
npm audit
npm audit fix
npm update
npm outdated
Lock Files
git add package-lock.json
npm ci
Verification Steps
Security Testing
Automated Security Tests
test('requires authentication', async () => {
const response = await fetch('/api/protected')
expect(response.status).toBe(401)
})
test('requires admin role', async () => {
const response = await fetch('/api/admin', {
headers: { Authorization: `Bearer ${userToken}` }
})
expect(response.status).toBe(403)
})
test('rejects invalid input', async () => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' })
})
expect(response.status).toBe(400)
})
test('enforces rate limits', async () => {
const requests = Array(101).fill(null).map(() =>
fetch('/api/endpoint')
)
const responses = await Promise.all(requests)
const tooManyRequests = responses.filter(r => r.status === 429)
expect(tooManyRequests.length).toBeGreaterThan(0)
})
Pre-Deployment Security Checklist
Before ANY production deployment:
Resources
Remember: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution.