home / skills / mhylle / claude-skills-collection / security-review
This skill performs a security review of code changes, providing checklists and remediation guidance to prevent vulnerabilities.
npx playbooks add skill mhylle/claude-skills-collection --skill security-reviewReview the files below or copy the command above to add this skill to your agents.
---
name: security-review
description: Comprehensive security audit for code changes. Use this skill when implementing authentication, authorization, user input handling, API endpoints, secrets/credentials, payment features, or file uploads. Provides security checklists, vulnerability patterns, and remediation guidance. Integrates with implement-phase as a security quality gate.
allowed-tools: Read, Glob, Grep, Bash
---
# Security Review Skill
This skill ensures all code follows security best practices and identifies potential vulnerabilities before they reach production.
## Design Philosophy
Security is not optional. This skill acts as a **security quality gate** that validates code against common vulnerability patterns (OWASP Top 10) and project-specific security requirements. One vulnerability can compromise the entire platform.
## When to Activate
Trigger this skill when code involves:
- **Authentication or authorization** - Login flows, session management, role checks
- **User input handling** - Forms, query parameters, file uploads
- **API endpoints** - New routes, especially public-facing
- **Secrets or credentials** - API keys, database connections, tokens
- **Payment features** - Financial transactions, billing, subscriptions
- **Sensitive data** - PII, health data, financial records
- **Third-party API integration** - External service connections
- **Database queries** - Especially raw SQL or dynamic queries
## Security Checklist
### 1. Secrets Management
#### BAD - Never Do This
```typescript
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
const dbPassword = "password123" // In source code
const config = {
stripe_key: "sk_live_xxxxx" // Committed to repo
}
```
#### GOOD - Always Do This
```typescript
const apiKey = process.env.OPENAI_API_KEY
const dbUrl = process.env.DATABASE_URL
// Verify secrets exist at startup
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
// Use secret management services for production
// AWS Secrets Manager, HashiCorp Vault, etc.
```
#### Verification Checklist
- [ ] No hardcoded API keys, tokens, or passwords in source
- [ ] All secrets loaded from environment variables
- [ ] `.env`, `.env.local`, `.env.production` in .gitignore
- [ ] No secrets in git history (run `git log -p | grep -i "api_key\|secret\|password"`)
- [ ] Production secrets in secure secret management (Vercel, Railway, AWS SM)
- [ ] Different secrets for dev/staging/production environments
---
### 2. Input Validation
#### BAD - Never Trust User Input
```typescript
// DANGEROUS: No validation
async function createUser(req: Request) {
const { email, name, role } = req.body
return db.users.create({ email, name, role }) // role injection!
}
// DANGEROUS: Client-side only validation
if (formData.email.includes('@')) { /* good enough? NO */ }
```
#### GOOD - Validate Everything Server-Side
```typescript
import { z } from 'zod'
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
age: z.number().int().min(0).max(150).optional()
// Note: role is NOT accepted from user input
})
export async function createUser(input: unknown) {
const validated = CreateUserSchema.parse(input)
return await db.users.create({
...validated,
role: 'user' // Role set server-side, never from input
})
}
```
#### File Upload Validation
```typescript
function validateFileUpload(file: File) {
// Size check (5MB max)
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)')
}
// MIME type check (verify actual content, not just header)
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type')
}
// Extension check (defense in depth)
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
if (!extension || !allowedExtensions.includes(extension)) {
throw new Error('Invalid file extension')
}
// Sanitize filename
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_')
return { ...file, name: sanitizedName }
}
```
#### Verification Checklist
- [ ] All user inputs validated with schemas (zod, yup, joi)
- [ ] Validation happens server-side (client validation is UX only)
- [ ] File uploads restricted by size, type, and extension
- [ ] File contents verified (not just extensions)
- [ ] Whitelist validation preferred over blacklist
- [ ] Error messages don't leak sensitive information
- [ ] No direct use of user input in file paths or system commands
---
### 3. SQL Injection Prevention
#### BAD - Never Concatenate SQL
```typescript
// CRITICAL VULNERABILITY - SQL Injection
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
// Also dangerous with template literals
const search = `SELECT * FROM products WHERE name LIKE '%${term}%'`
```
#### GOOD - Always Use Parameterized Queries
```typescript
// Safe - parameterized query with Supabase
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userEmail)
// Safe - parameterized raw SQL
await db.query(
'SELECT * FROM users WHERE email = $1',
[userEmail]
)
// Safe - ORM with proper escaping
const user = await prisma.user.findUnique({
where: { email: userEmail }
})
// Safe - LIKE queries with parameterization
await db.query(
'SELECT * FROM products WHERE name LIKE $1',
[`%${term}%`]
)
```
#### Verification Checklist
- [ ] All database queries use parameterized queries or ORM
- [ ] No string concatenation or interpolation in SQL
- [ ] Query builders used correctly (not bypassed)
- [ ] Stored procedures use parameterized inputs
- [ ] Database user has minimal required permissions
---
### 4. Authentication & Authorization
#### BAD - Insecure Token Storage
```typescript
// WRONG: localStorage vulnerable to XSS
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
// WRONG: No authorization check
async function deleteUser(userId: string) {
await db.users.delete({ where: { id: userId } }) // Anyone can delete!
}
```
#### GOOD - Secure Token Handling
```typescript
// CORRECT: httpOnly cookies (server-side)
res.setHeader('Set-Cookie', [
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/`
])
// CORRECT: Authorization check before action
export async function deleteUser(userId: string, requesterId: string) {
const requester = await db.users.findUnique({
where: { id: requesterId },
select: { id: true, role: true }
})
// Check ownership OR admin role
if (requester.id !== userId && requester.role !== 'admin') {
throw new ForbiddenError('Not authorized to delete this user')
}
await db.users.delete({ where: { id: userId } })
}
```
#### Row Level Security (Supabase/PostgreSQL)
```sql
-- Enable RLS on all tables with user data
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only view their own data
CREATE POLICY "Users view own data"
ON users FOR SELECT
USING (auth.uid() = id);
-- Users can only update their own data
CREATE POLICY "Users update own data"
ON users FOR UPDATE
USING (auth.uid() = id);
-- Admin policy (if needed)
CREATE POLICY "Admins can view all"
ON users FOR SELECT
USING (
EXISTS (
SELECT 1 FROM users WHERE id = auth.uid() AND role = 'admin'
)
);
```
#### Verification Checklist
- [ ] Tokens stored in httpOnly cookies (not localStorage/sessionStorage)
- [ ] Secure and SameSite flags set on cookies
- [ ] Authorization check before every sensitive operation
- [ ] Row Level Security enabled in database (Supabase)
- [ ] Role-based access control implemented correctly
- [ ] Session timeout configured appropriately
- [ ] Password reset tokens single-use and time-limited
- [ ] Failed login attempts tracked and limited
---
### 5. XSS Prevention
#### BAD - Rendering Unsanitized Content
```typescript
// DANGEROUS: Direct HTML injection
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
// DANGEROUS: Eval-like functions
const userCode = getUserInput()
eval(userCode)
new Function(userCode)()
```
#### GOOD - Sanitize All User HTML
```typescript
import DOMPurify from 'isomorphic-dompurify'
function SafeComment({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [] // No attributes allowed
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
// Better: Use markdown instead of HTML
import { marked } from 'marked'
import DOMPurify from 'isomorphic-dompurify'
function MarkdownContent({ markdown }: { markdown: string }) {
const html = marked(markdown)
const clean = DOMPurify.sanitize(html)
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
```
#### Content Security Policy
```typescript
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self'", // Remove 'unsafe-inline' 'unsafe-eval' if possible
"style-src 'self' 'unsafe-inline'", // Needed for most CSS-in-JS
"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('; ')
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
}
]
```
#### Verification Checklist
- [ ] All user-provided HTML sanitized with DOMPurify or similar
- [ ] dangerouslySetInnerHTML only used with sanitized content
- [ ] CSP headers configured and tested
- [ ] No eval(), new Function(), or innerHTML with user data
- [ ] React's built-in XSS protection not bypassed unnecessarily
- [ ] URL parameters validated before use in links (javascript: protocol blocked)
---
### 6. CSRF Protection
#### BAD - No CSRF Protection
```typescript
// VULNERABLE: State-changing GET request
app.get('/api/delete-account', async (req, res) => {
await deleteAccount(req.user.id)
})
// VULNERABLE: No CSRF token verification
app.post('/api/transfer', async (req, res) => {
await transferMoney(req.body.amount, req.body.to)
})
```
#### GOOD - Implement CSRF Protection
```typescript
import { csrf } from '@/lib/csrf'
export async function POST(request: Request) {
// Verify CSRF token from header
const token = request.headers.get('X-CSRF-Token')
if (!csrf.verify(token)) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
)
}
// Process request
}
// Client-side: Include CSRF token in requests
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken, // From meta tag or cookie
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
```
#### SameSite Cookies (Primary Defense)
```typescript
// Modern browsers: SameSite provides CSRF protection
res.setHeader('Set-Cookie', [
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict; Path=/`
])
```
#### Verification Checklist
- [ ] All state-changing operations use POST/PUT/DELETE (not GET)
- [ ] CSRF tokens on forms and AJAX requests
- [ ] SameSite=Strict on all authentication cookies
- [ ] Origin header verified on sensitive endpoints
- [ ] Double-submit cookie pattern for APIs (if needed)
---
### 7. Rate Limiting
#### BAD - No Rate Limiting
```typescript
// VULNERABLE: No limits on expensive operation
app.post('/api/search', async (req, res) => {
const results = await expensiveSearch(req.body.query)
return res.json(results)
})
// VULNERABLE: No limits on auth endpoints
app.post('/api/login', async (req, res) => {
// Brute force attack possible
})
```
#### GOOD - Implement Rate Limiting
```typescript
import rateLimit from 'express-rate-limit'
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later' },
standardHeaders: true,
legacyHeaders: false
})
// Strict rate limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
message: { error: 'Too many login attempts' },
skipSuccessfulRequests: true
})
// Very strict for expensive operations
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
message: { error: 'Too many search requests' }
})
app.use('/api/', apiLimiter)
app.use('/api/auth/', authLimiter)
app.use('/api/search', searchLimiter)
```
#### Verification Checklist
- [ ] Rate limiting on all API endpoints
- [ ] Stricter limits on authentication endpoints
- [ ] Stricter limits on expensive operations (search, AI, etc.)
- [ ] IP-based rate limiting for unauthenticated requests
- [ ] User-based rate limiting for authenticated requests
- [ ] Rate limit headers returned to clients
- [ ] Graceful handling of rate limit exceeded
---
### 8. Sensitive Data Exposure
#### BAD - Leaking Sensitive Data
```typescript
// WRONG: Logging sensitive data
console.log('User login:', { email, password })
console.log('Payment processed:', { cardNumber, cvv, amount })
console.log('Request body:', req.body) // May contain passwords
// WRONG: Exposing internal errors
catch (error) {
return res.json({
error: error.message,
stack: error.stack, // Stack trace exposed!
query: sqlQuery // Query exposed!
})
}
// WRONG: Returning full user object
return res.json({ user }) // Includes password hash, internal IDs, etc.
```
#### GOOD - Protect Sensitive Data
```typescript
// CORRECT: Redact sensitive data in logs
console.log('User login:', { email, userId: user.id })
console.log('Payment processed:', {
last4: card.number.slice(-4),
amount,
userId
})
// CORRECT: Generic error messages to clients
catch (error) {
// Log full error server-side
logger.error('Payment failed', {
error: error.message,
userId,
// Never log card numbers, even partial
})
// Return generic message to client
return res.status(500).json({
error: 'Payment processing failed. Please try again.'
})
}
// CORRECT: Select specific fields
const user = await db.users.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
// Explicitly exclude: passwordHash, internalNotes, etc.
}
})
return res.json({ user })
```
#### Verification Checklist
- [ ] No passwords, tokens, or full card numbers in logs
- [ ] Error messages generic for end users
- [ ] Detailed errors only in server-side logs
- [ ] No stack traces exposed to clients
- [ ] API responses select specific fields (not SELECT *)
- [ ] Sensitive data encrypted at rest
- [ ] Sensitive data encrypted in transit (HTTPS enforced)
- [ ] PII handling compliant with regulations (GDPR, CCPA)
---
### 9. Blockchain Security (Conditional)
*Only applicable when project involves blockchain/crypto functionality.*
#### BAD - Insecure Blockchain Handling
```typescript
// WRONG: Not verifying wallet ownership
async function claimReward(walletAddress: string) {
await sendReward(walletAddress) // Anyone can claim!
}
// WRONG: Blind transaction signing
async function processTransaction(tx: any) {
await wallet.signAndSend(tx) // No validation!
}
```
#### GOOD - Verify Everything
```typescript
import { verify } from '@solana/web3.js'
import nacl from 'tweetnacl'
// Verify wallet ownership with signed message
async function verifyWalletOwnership(
publicKey: string,
signature: string,
message: string
): Promise<boolean> {
try {
const messageBytes = new TextEncoder().encode(message)
const signatureBytes = Buffer.from(signature, 'base64')
const publicKeyBytes = Buffer.from(publicKey, 'base64')
return nacl.sign.detached.verify(
messageBytes,
signatureBytes,
publicKeyBytes
)
} catch {
return false
}
}
// Validate transaction before signing
async function processTransaction(transaction: Transaction) {
// Verify recipient is expected
if (transaction.to !== KNOWN_RECIPIENT) {
throw new Error('Invalid recipient address')
}
// Verify amount is within limits
if (transaction.amount > MAX_TRANSACTION_AMOUNT) {
throw new Error('Amount exceeds limit')
}
// Verify user has sufficient balance
const balance = await getBalance(transaction.from)
if (balance < transaction.amount + GAS_BUFFER) {
throw new Error('Insufficient balance')
}
// Log transaction for audit
await auditLog.record({
type: 'transaction',
from: transaction.from,
to: transaction.to,
amount: transaction.amount,
timestamp: new Date()
})
return await signAndSend(transaction)
}
```
#### Verification Checklist
- [ ] Wallet signatures verified before granting access
- [ ] Transaction details validated before signing
- [ ] Balance checks before transactions
- [ ] No blind transaction signing
- [ ] Transaction limits enforced
- [ ] Audit logging for all transactions
- [ ] Private keys never exposed in logs or errors
- [ ] Smart contract interactions validated
---
### 10. Dependency Security
#### Regular Security Audits
```bash
# Check for known vulnerabilities
npm audit
# Automatically fix what's possible
npm audit fix
# Check for outdated packages
npm outdated
# Update dependencies
npm update
# For major updates, use interactive mode carefully
npx npm-check-updates -i
```
#### Lock File Management
```bash
# ALWAYS commit lock files
git add package-lock.json # or yarn.lock, pnpm-lock.yaml
# In CI/CD, use clean install for reproducible builds
npm ci # Not npm install
# Verify integrity
npm ci --ignore-scripts # Then run scripts separately if needed
```
#### Verification Checklist
- [ ] No known vulnerabilities (`npm audit` clean or exceptions documented)
- [ ] Dependencies regularly updated
- [ ] Lock files committed to repository
- [ ] Dependabot or similar enabled on GitHub
- [ ] Unused dependencies removed
- [ ] Dependencies from trusted sources only
- [ ] License compliance verified (no GPL in proprietary code, etc.)
- [ ] Postinstall scripts reviewed for new dependencies
---
## Integration with implement-phase
When invoked as part of the implement-phase pipeline (Step 3), this skill provides structured output for orchestration.
### Input Context
```
Security Review for Phase [N]
Context:
- Plan: [path to plan file]
- Phase: [phase number and name]
- Changed files: [list of modified/added files]
- Security-relevant changes: [auth, input, api, secrets, payment, uploads]
```
### Output Format
```
STATUS: PASS | PASS_WITH_ISSUES | FAIL
CATEGORIES_CHECKED: [count of applicable categories reviewed]
ISSUES_FOUND:
- [CRITICAL] [Category]: [Description]
- [HIGH] [Category]: [Description]
- [MEDIUM] [Category]: [Description]
- [LOW] [Category]: [Description]
SEVERITY: CRITICAL | HIGH | MEDIUM | LOW | NONE
RECOMMENDATIONS:
- [Non-blocking improvement suggestions]
REPORT: [Path to detailed report if written]
```
### Severity Levels
| Level | Meaning | Action Required |
|-------|---------|-----------------|
| **CRITICAL** | Active vulnerability, exploitable now | Block deployment, fix immediately |
| **HIGH** | Serious vulnerability, likely exploitable | Block deployment, fix before merge |
| **MEDIUM** | Potential vulnerability, context-dependent | Should fix, may proceed with documented exception |
| **LOW** | Best practice violation, minimal risk | Note for improvement |
### Integration Point in Pipeline
```
implement-phase Pipeline:
Step 1: Implementation
Step 2: Functional verification (tests pass)
Step 3: Code review
Step 4: SECURITY REVIEW (this skill) <--
Step 5: Plan synchronization
Step 6: Completion report
On FAIL:
- Block phase completion
- Report blocking issues
- Require fixes before retry
```
---
## Pre-Deployment Security Checklist
Before ANY production deployment, verify:
### Secrets & Configuration
- [ ] No hardcoded secrets in code
- [ ] All secrets in environment variables
- [ ] Production secrets in secure management
- [ ] Different secrets per environment
### Input & Data
- [ ] All user input validated server-side
- [ ] File uploads validated and sanitized
- [ ] All database queries parameterized
- [ ] Sensitive data encrypted at rest
### Authentication & Authorization
- [ ] Tokens in httpOnly cookies
- [ ] Authorization checks on all operations
- [ ] Row Level Security enabled
- [ ] Session management secure
### Web Security
- [ ] XSS protection (CSP, sanitization)
- [ ] CSRF protection enabled
- [ ] HTTPS enforced
- [ ] Security headers configured
### Infrastructure
- [ ] Rate limiting on all endpoints
- [ ] Error messages don't leak info
- [ ] Logging doesn't include secrets
- [ ] Dependencies up to date
### Compliance
- [ ] CORS properly configured
- [ ] Privacy policy accurate
- [ ] Data handling compliant (GDPR/CCPA)
---
## References
For detailed checklists and examples:
- [security-checklist.md](references/security-checklist.md) - Quick reference checklist
- [OWASP Top 10](https://owasp.org/www-project-top-ten/) - Common vulnerabilities
- [OWASP Cheat Sheets](https://cheatsheetseries.owasp.org/) - Detailed guidance
---
**Security is everyone's responsibility. When in doubt, ask for a security review.**
This skill performs a comprehensive security audit of code changes to catch vulnerabilities before they reach production. It acts as a security quality gate during implementation, focusing on authentication, authorization, input handling, secrets, and other high-risk areas. The goal is actionable findings with remediation guidance and verification checklists.
The skill scans code changes and inspects patterns against common vulnerability classes (OWASP Top 10) and project-specific rules. It produces prioritized findings, concrete remediation steps, and a verification checklist for each area (secrets, input validation, SQL, auth, XSS, CSRF, rate limiting, and sensitive data). It integrates with the implementation phase as a gate that must be cleared before merge or deployment.
What does the skill flag as a secret?
It flags hardcoded API keys, tokens, passwords, and any values matching secret patterns in source files or config; it also checks git history for leaked secrets.
How strict is input validation enforcement?
It enforces server-side validation with schema-based checks and whitelist rules; missing validation or acceptance of privileged fields from user input are flagged as high severity.