$npx -y skills add mworldorg/markdown-memory --skill ecc-security-reviewUse this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
| 1 | <!-- Vendored from ECC (https://github.com/affaan-m/everything-claude-code), MIT © 2026 Affaan Mustafa. See LICENSE & NOTICE.md in this folder. Skill renamed security-review → ecc-security-review to avoid clash with the built-in /security-review command. Content otherwise verbatim. --> |
| 2 | |
| 3 | # Security Review Skill |
| 4 | |
| 5 | This skill ensures all code follows security best practices and identifies potential vulnerabilities. |
| 6 | |
| 7 | ## When to Activate |
| 8 | |
| 9 | - Implementing authentication or authorization |
| 10 | - Handling user input or file uploads |
| 11 | - Creating new API endpoints |
| 12 | - Working with secrets or credentials |
| 13 | - Implementing payment features |
| 14 | - Storing or transmitting sensitive data |
| 15 | - Integrating third-party APIs |
| 16 | |
| 17 | ## Security Checklist |
| 18 | |
| 19 | ### 1. Secrets Management |
| 20 | |
| 21 | #### FAIL: NEVER Do This |
| 22 | ```typescript |
| 23 | const apiKey = "sk-proj-xxxxx" // Hardcoded secret |
| 24 | const dbPassword = "password123" // In source code |
| 25 | ``` |
| 26 | |
| 27 | #### PASS: ALWAYS Do This |
| 28 | ```typescript |
| 29 | const apiKey = process.env.OPENAI_API_KEY |
| 30 | const dbUrl = process.env.DATABASE_URL |
| 31 | |
| 32 | // Verify secrets exist |
| 33 | if (!apiKey) { |
| 34 | throw new Error('OPENAI_API_KEY not configured') |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | #### Verification Steps |
| 39 | - [ ] No hardcoded API keys, tokens, or passwords |
| 40 | - [ ] All secrets in environment variables |
| 41 | - [ ] `.env.local` in .gitignore |
| 42 | - [ ] No secrets in git history |
| 43 | - [ ] Production secrets in hosting platform (Vercel, Railway) |
| 44 | |
| 45 | ### 2. Input Validation |
| 46 | |
| 47 | #### Always Validate User Input |
| 48 | ```typescript |
| 49 | import { z } from 'zod' |
| 50 | |
| 51 | // Define validation schema |
| 52 | const CreateUserSchema = z.object({ |
| 53 | email: z.string().email(), |
| 54 | name: z.string().min(1).max(100), |
| 55 | age: z.number().int().min(0).max(150) |
| 56 | }) |
| 57 | |
| 58 | // Validate before processing |
| 59 | export async function createUser(input: unknown) { |
| 60 | try { |
| 61 | const validated = CreateUserSchema.parse(input) |
| 62 | return await db.users.create(validated) |
| 63 | } catch (error) { |
| 64 | if (error instanceof z.ZodError) { |
| 65 | return { success: false, errors: error.errors } |
| 66 | } |
| 67 | throw error |
| 68 | } |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | #### File Upload Validation |
| 73 | ```typescript |
| 74 | function validateFileUpload(file: File) { |
| 75 | // Size check (5MB max) |
| 76 | const maxSize = 5 * 1024 * 1024 |
| 77 | if (file.size > maxSize) { |
| 78 | throw new Error('File too large (max 5MB)') |
| 79 | } |
| 80 | |
| 81 | // Type check |
| 82 | const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] |
| 83 | if (!allowedTypes.includes(file.type)) { |
| 84 | throw new Error('Invalid file type') |
| 85 | } |
| 86 | |
| 87 | // Extension check |
| 88 | const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] |
| 89 | const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] |
| 90 | if (!extension || !allowedExtensions.includes(extension)) { |
| 91 | throw new Error('Invalid file extension') |
| 92 | } |
| 93 | |
| 94 | return true |
| 95 | } |
| 96 | ``` |
| 97 | |
| 98 | #### Verification Steps |
| 99 | - [ ] All user inputs validated with schemas |
| 100 | - [ ] File uploads restricted (size, type, extension) |
| 101 | - [ ] No direct use of user input in queries |
| 102 | - [ ] Whitelist validation (not blacklist) |
| 103 | - [ ] Error messages don't leak sensitive info |
| 104 | |
| 105 | ### 3. SQL Injection Prevention |
| 106 | |
| 107 | #### FAIL: NEVER Concatenate SQL |
| 108 | ```typescript |
| 109 | // DANGEROUS - SQL Injection vulnerability |
| 110 | const query = `SELECT * FROM users WHERE email = '${userEmail}'` |
| 111 | await db.query(query) |
| 112 | ``` |
| 113 | |
| 114 | #### PASS: ALWAYS Use Parameterized Queries |
| 115 | ```typescript |
| 116 | // Safe - parameterized query |
| 117 | const { data } = await supabase |
| 118 | .from('users') |
| 119 | .select('*') |
| 120 | .eq('email', userEmail) |
| 121 | |
| 122 | // Or with raw SQL |
| 123 | await db.query( |
| 124 | 'SELECT * FROM users WHERE email = $1', |
| 125 | [userEmail] |
| 126 | ) |
| 127 | ``` |
| 128 | |
| 129 | #### Verification Steps |
| 130 | - [ ] All database queries use parameterized queries |
| 131 | - [ ] No string concatenation in SQL |
| 132 | - [ ] ORM/query builder used correctly |
| 133 | - [ ] Supabase queries properly sanitized |
| 134 | |
| 135 | ### 4. Authentication & Authorization |
| 136 | |
| 137 | #### JWT Token Handling |
| 138 | ```typescript |
| 139 | // FAIL: WRONG: localStorage (vulnerable to XSS) |
| 140 | localStorage.setItem('token', token) |
| 141 | |
| 142 | // PASS: CORRECT: httpOnly cookies |
| 143 | res.setHeader('Set-Cookie', |
| 144 | `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`) |
| 145 | ``` |
| 146 | |
| 147 | #### Authorization Checks |
| 148 | ```typescript |
| 149 | export async function deleteUser(userId: string, requesterId: string) { |
| 150 | // ALWAYS verify authorization first |
| 151 | const requester = await db.users.findUnique({ |
| 152 | where: { id: requesterId } |
| 153 | }) |
| 154 | |
| 155 | if (requester.role !== 'admin') { |
| 156 | return NextResponse.json( |
| 157 | { error: 'Unauthorized' }, |
| 158 | { status: 403 } |
| 159 | ) |
| 160 | } |
| 161 | |
| 162 | // Proceed with deletion |
| 163 | await db.users.delete({ where: { id: userId } }) |
| 164 | } |
| 165 | ``` |
| 166 | |
| 167 | #### Row Level Security (Supabase) |
| 168 | ```sql |
| 169 | -- Enable RLS on all tables |
| 170 | ALTER TABLE users ENABLE ROW LEVEL SECURITY; |
| 171 | |
| 172 | -- Users can only view their own data |
| 173 | CREATE POLICY "Users view own data" |
| 174 | ON users FOR SELECT |
| 175 | USING (auth.uid() = id); |
| 176 | |
| 177 | -- Users can only update their own data |
| 178 | CREATE POLICY "Users update own data" |
| 179 | ON users FOR UPDATE |
| 180 | USING (auth.uid() = id); |
| 181 | ``` |
| 182 | |
| 183 | #### Verification Steps |
| 184 | - [ ] Toke |