Use 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.
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
1const apiKey = "sk-proj-xxxxx"; // Hardcoded secret
2const dbPassword = "password123"; // In source code✅ ALWAYS Do This
1const 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
1import { 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
1function 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
1// DANGEROUS - SQL Injection vulnerability
2const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
3await db.query(query);✅ ALWAYS Use Parameterized Queries
1// 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("SELECT * FROM users WHERE email = $1", [userEmail]);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
1// ❌ WRONG: localStorage (vulnerable to XSS)
2localStorage.setItem("token", token);
3
4// ✅ CORRECT: httpOnly cookies
5res.setHeader(
6 "Set-Cookie",
7 `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`,
8);Authorization Checks
1export 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({ error: "Unauthorized" }, { status: 403 });
9 }
10
11 // Proceed with deletion
12 await db.users.delete({ where: { id: userId } });
13}Row Level Security (Supabase)
1-- 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
1import 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
1// 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 `
13 .replace(/\s{2,}/g, " ")
14 .trim(),
15 },
16];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
1import { 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({ error: "Invalid CSRF token" }, { status: 403 });
8 }
9
10 // Process request
11}SameSite Cookies
1res.setHeader(
2 "Set-Cookie",
3 `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`,
4);Verification Steps
- CSRF tokens on state-changing operations
- SameSite=Strict on all cookies
- Double-submit cookie pattern implemented
7. Rate Limiting
API Rate Limiting
1import 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
1// 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
1// ❌ 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
1// ❌ 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
1import { 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
1async 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
1# 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 outdatedLock Files
1# ALWAYS commit lock files
2git add package-lock.json
3
4# Use in CI/CD for reproducible builds
5npm ci # Instead of npm installVerification 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
1// 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)
27 .fill(null)
28 .map(() => fetch("/api/endpoint"));
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.