home / skills / mhylle / claude-skills-collection / security-review

security-review skill

/skills/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-review

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
21.0 KB
---
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.**

Overview

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.

How this skill works

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.

When to use it

  • When adding or changing authentication and authorization logic (login, roles, sessions).
  • When exposing new API endpoints or public routes.
  • When handling user input, forms, file uploads, or query parameters.
  • When introducing secrets, API keys, or credential handling.
  • When implementing payments, sensitive data handling, or third-party integrations.

Best practices

  • Never hardcode secrets; require environment variables and secret management for production.
  • Validate all inputs server-side with strict schemas and whitelist rules (zod, Joi, etc.).
  • Use parameterized queries or ORMs; forbid string concatenation for SQL.
  • Store tokens in httpOnly, Secure, SameSite cookies and enforce authorization checks per action.
  • Sanitize user-supplied HTML, enforce CSP, and avoid eval/new Function patterns.
  • Apply CSRF tokens or SameSite cookies and enforce rate limiting on auth and expensive endpoints.

Example use cases

  • Reviewing a new registration/login flow that introduces session cookies and role handling.
  • Auditing a public API route that accepts user-supplied query parameters or file uploads.
  • Validating a payment integration and surrounding error handling and logging.
  • Checking a migration that changes database queries for potential SQL injection risks.
  • Gatekeeping a merge that adds third-party secret keys or environment variable changes.

FAQ

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.