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
❌ NEVER Do This
typescript1const apiKey = "sk-proj-xxxxx" // Hardcoded secret 2const dbPassword = "password123" // In source code
✅ ALWAYS Do This
typescript1const apiKey = process.env.OPENAI_API_KEY 2const dbUrl = process.env.DATABASE_URL 3 4// Verify secrets exist 5if (!apiKey) { 6 throw new Error('OPENAI_API_KEY not configured') 7}
Verification Steps
- No hardcoded API keys, tokens, or passwords
- All secrets in environment variables
-
.env.localin .gitignore - No secrets in git history
- Production secrets in hosting platform (Vercel, Railway)
2. Input Validation
Always Validate User Input
typescript1import { z } from 'zod' 2 3// Define validation schema 4const CreateUserSchema = z.object({ 5 email: z.string().email(), 6 name: z.string().min(1).max(100), 7 age: z.number().int().min(0).max(150) 8}) 9 10// Validate before processing 11export async function createUser(input: unknown) { 12 try { 13 const validated = CreateUserSchema.parse(input) 14 return await db.users.create(validated) 15 } catch (error) { 16 if (error instanceof z.ZodError) { 17 return { success: false, errors: error.errors } 18 } 19 throw error 20 } 21}
File Upload Validation
typescript1function validateFileUpload(file: File) { 2 // Size check (5MB max) 3 const maxSize = 5 * 1024 * 1024 4 if (file.size > maxSize) { 5 throw new Error('File too large (max 5MB)') 6 } 7 8 // Type check 9 const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] 10 if (!allowedTypes.includes(file.type)) { 11 throw new Error('Invalid file type') 12 } 13 14 // Extension check 15 const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] 16 const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] 17 if (!extension || !allowedExtensions.includes(extension)) { 18 throw new Error('Invalid file extension') 19 } 20 21 return true 22}
Verification Steps
- All user inputs validated with schemas
- File uploads restricted (size, type, extension)
- No direct use of user input in queries
- Whitelist validation (not blacklist)
- Error messages don't leak sensitive info
3. SQL Injection Prevention
❌ NEVER Concatenate SQL
typescript1// DANGEROUS - SQL Injection vulnerability 2const query = `SELECT * FROM users WHERE email = '${userEmail}'` 3await db.query(query)
✅ ALWAYS Use Parameterized Queries
typescript1// Safe - parameterized query 2const { data } = await supabase 3 .from('users') 4 .select('*') 5 .eq('email', userEmail) 6 7// Or with raw SQL 8await db.query( 9 'SELECT * FROM users WHERE email = $1', 10 [userEmail] 11)
Verification Steps
- All database queries use parameterized queries
- No string concatenation in SQL
- ORM/query builder used correctly
- Supabase queries properly sanitized
4. Authentication & Authorization
JWT Token Handling
typescript1// ❌ WRONG: localStorage (vulnerable to XSS) 2localStorage.setItem('token', token) 3 4// ✅ CORRECT: httpOnly cookies 5res.setHeader('Set-Cookie', 6 `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
Authorization Checks
typescript1export async function deleteUser(userId: string, requesterId: string) { 2 // ALWAYS verify authorization first 3 const requester = await db.users.findUnique({ 4 where: { id: requesterId } 5 }) 6 7 if (requester.role !== 'admin') { 8 return NextResponse.json( 9 { error: 'Unauthorized' }, 10 { status: 403 } 11 ) 12 } 13 14 // Proceed with deletion 15 await db.users.delete({ where: { id: userId } }) 16}
Row Level Security (Supabase)
sql1-- Enable RLS on all tables 2ALTER TABLE users ENABLE ROW LEVEL SECURITY; 3 4-- Users can only view their own data 5CREATE POLICY "Users view own data" 6 ON users FOR SELECT 7 USING (auth.uid() = id); 8 9-- Users can only update their own data 10CREATE POLICY "Users update own data" 11 ON users FOR UPDATE 12 USING (auth.uid() = id);
Verification Steps
- Tokens stored in httpOnly cookies (not localStorage)
- Authorization checks before sensitive operations
- Row Level Security enabled in Supabase
- Role-based access control implemented
- Session management secure
5. XSS Prevention
Sanitize HTML
typescript1import DOMPurify from 'isomorphic-dompurify' 2 3// ALWAYS sanitize user-provided HTML 4function renderUserContent(html: string) { 5 const clean = DOMPurify.sanitize(html, { 6 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], 7 ALLOWED_ATTR: [] 8 }) 9 return <div dangerouslySetInnerHTML={{ __html: clean }} /> 10}
Content Security Policy
typescript1// next.config.js 2const securityHeaders = [ 3 { 4 key: 'Content-Security-Policy', 5 value: ` 6 default-src 'self'; 7 script-src 'self' 'unsafe-eval' 'unsafe-inline'; 8 style-src 'self' 'unsafe-inline'; 9 img-src 'self' data: https:; 10 font-src 'self'; 11 connect-src 'self' https://api.example.com; 12 `.replace(/\s{2,}/g, ' ').trim() 13 } 14]
Verification Steps
- User-provided HTML sanitized
- CSP headers configured
- No unvalidated dynamic content rendering
- React's built-in XSS protection used
6. CSRF Protection
CSRF Tokens
typescript1import { csrf } from '@/lib/csrf' 2 3export async function POST(request: Request) { 4 const token = request.headers.get('X-CSRF-Token') 5 6 if (!csrf.verify(token)) { 7 return NextResponse.json( 8 { error: 'Invalid CSRF token' }, 9 { status: 403 } 10 ) 11 } 12 13 // Process request 14}
SameSite Cookies
typescript1res.setHeader('Set-Cookie', 2 `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
Verification Steps
- CSRF tokens on state-changing operations
- SameSite=Strict on all cookies
- Double-submit cookie pattern implemented
7. Rate Limiting
API Rate Limiting
typescript1import rateLimit from 'express-rate-limit' 2 3const limiter = rateLimit({ 4 windowMs: 15 * 60 * 1000, // 15 minutes 5 max: 100, // 100 requests per window 6 message: 'Too many requests' 7}) 8 9// Apply to routes 10app.use('/api/', limiter)
Expensive Operations
typescript1// Aggressive rate limiting for searches 2const searchLimiter = rateLimit({ 3 windowMs: 60 * 1000, // 1 minute 4 max: 10, // 10 requests per minute 5 message: 'Too many search requests' 6}) 7 8app.use('/api/search', searchLimiter)
Verification Steps
- Rate limiting on all API endpoints
- Stricter limits on expensive operations
- IP-based rate limiting
- User-based rate limiting (authenticated)
8. Sensitive Data Exposure
Logging
typescript1// ❌ WRONG: Logging sensitive data 2console.log('User login:', { email, password }) 3console.log('Payment:', { cardNumber, cvv }) 4 5// ✅ CORRECT: Redact sensitive data 6console.log('User login:', { email, userId }) 7console.log('Payment:', { last4: card.last4, userId })
Error Messages
typescript1// ❌ WRONG: Exposing internal details 2catch (error) { 3 return NextResponse.json( 4 { error: error.message, stack: error.stack }, 5 { status: 500 } 6 ) 7} 8 9// ✅ CORRECT: Generic error messages 10catch (error) { 11 console.error('Internal error:', error) 12 return NextResponse.json( 13 { error: 'An error occurred. Please try again.' }, 14 { status: 500 } 15 ) 16}
Verification Steps
- No passwords, tokens, or secrets in logs
- Error messages generic for users
- Detailed errors only in server logs
- No stack traces exposed to users
9. Blockchain Security (Solana)
Wallet Verification
typescript1import { verify } from '@solana/web3.js' 2 3async function verifyWalletOwnership( 4 publicKey: string, 5 signature: string, 6 message: string 7) { 8 try { 9 const isValid = verify( 10 Buffer.from(message), 11 Buffer.from(signature, 'base64'), 12 Buffer.from(publicKey, 'base64') 13 ) 14 return isValid 15 } catch (error) { 16 return false 17 } 18}
Transaction Verification
typescript1async function verifyTransaction(transaction: Transaction) { 2 // Verify recipient 3 if (transaction.to !== expectedRecipient) { 4 throw new Error('Invalid recipient') 5 } 6 7 // Verify amount 8 if (transaction.amount > maxAmount) { 9 throw new Error('Amount exceeds limit') 10 } 11 12 // Verify user has sufficient balance 13 const balance = await getBalance(transaction.from) 14 if (balance < transaction.amount) { 15 throw new Error('Insufficient balance') 16 } 17 18 return true 19}
Verification Steps
- Wallet signatures verified
- Transaction details validated
- Balance checks before transactions
- No blind transaction signing
10. Dependency Security
Regular Updates
bash1# Check for vulnerabilities 2npm audit 3 4# Fix automatically fixable issues 5npm audit fix 6 7# Update dependencies 8npm update 9 10# Check for outdated packages 11npm outdated
Lock Files
bash1# ALWAYS commit lock files 2git add package-lock.json 3 4# Use in CI/CD for reproducible builds 5npm ci # Instead of npm install
Verification Steps
- Dependencies up to date
- No known vulnerabilities (npm audit clean)
- Lock files committed
- Dependabot enabled on GitHub
- Regular security updates
Security Testing
Automated Security Tests
typescript1// Test authentication 2test('requires authentication', async () => { 3 const response = await fetch('/api/protected') 4 expect(response.status).toBe(401) 5}) 6 7// Test authorization 8test('requires admin role', async () => { 9 const response = await fetch('/api/admin', { 10 headers: { Authorization: `Bearer ${userToken}` } 11 }) 12 expect(response.status).toBe(403) 13}) 14 15// Test input validation 16test('rejects invalid input', async () => { 17 const response = await fetch('/api/users', { 18 method: 'POST', 19 body: JSON.stringify({ email: 'not-an-email' }) 20 }) 21 expect(response.status).toBe(400) 22}) 23 24// Test rate limiting 25test('enforces rate limits', async () => { 26 const requests = Array(101).fill(null).map(() => 27 fetch('/api/endpoint') 28 ) 29 30 const responses = await Promise.all(requests) 31 const tooManyRequests = responses.filter(r => r.status === 429) 32 33 expect(tooManyRequests.length).toBeGreaterThan(0) 34})
Pre-Deployment Security Checklist
Before ANY production deployment:
- Secrets: No hardcoded secrets, all in env vars
- Input Validation: All user inputs validated
- SQL Injection: All queries parameterized
- XSS: User content sanitized
- CSRF: Protection enabled
- Authentication: Proper token handling
- Authorization: Role checks in place
- Rate Limiting: Enabled on all endpoints
- HTTPS: Enforced in production
- Security Headers: CSP, X-Frame-Options configured
- Error Handling: No sensitive data in errors
- Logging: No sensitive data logged
- Dependencies: Up to date, no vulnerabilities
- Row Level Security: Enabled in Supabase
- CORS: Properly configured
- File Uploads: Validated (size, type)
- Wallet Signatures: Verified (if blockchain)
Resources
Remember: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution.