name security-patterns description Security best practices for authentication, input validation, OWASP patterns, and secure coding. Use when handling user input, auth, secrets, or sensitive data. user-invocable false
Security Patterns
Comprehensive security patterns and best practices for secure application development.
When to Use
Implementing authentication or authorization
Handling user input or file uploads
Working with secrets or environment variables
Creating API endpoints
Storing or transmitting sensitive data
Integrating third-party services
OWASP Top 10 Patterns
1. Broken Access Control
❌ WRONG: Missing Authorization
export async function DELETE (request : Request ) {
const { userId } = await request.json ()
await db.users .delete ({ where : { id : userId } })
return NextResponse .json ({ success : true })
}
✅ CORRECT: Proper Authorization
export async function DELETE (request : Request ) {
const session = await getSession (request)
const { userId } = await request.json ()
if (session.userId !== userId && session.role !== 'admin' ) {
return NextResponse .json (
{ error : 'Unauthorized' },
{ status : 403 }
)
}
await db.users .delete ({ where : { id : userId } })
return NextResponse .json ({ success : true })
}
2. Cryptographic Failures
❌ WRONG: Hardcoded Secrets
const JWT_SECRET = "my-super-secret-key"
const API_KEY = "sk-proj-xxxxxxxxxxxxx"
const DATABASE_URL = "postgresql://user:password@localhost/db"
✅ CORRECT: Environment Variables
JWT_SECRET =use-a-strong-randomly-generated-secret
OPENAI_API_KEY =sk-proj-xxxxxxxxxxxxx
DATABASE_URL =postgresql :
const jwtSecret = process.env .JWT_SECRET
if (!jwtSecret) {
throw new Error ('JWT_SECRET environment variable not set' )
}
const apiKey = process.env .OPENAI_API_KEY
if (!apiKey) {
throw new Error ('OPENAI_API_KEY not configured' )
}
Verification Steps:
3. Injection Attacks
SQL Injection
❌ WRONG: String Concatenation
const email = request.body .email
const query = `SELECT * FROM users WHERE email = '${email} '`
await db.query (query)
✅ CORRECT: Parameterized Queries
const { data, error } = await supabase
.from ('users' )
.select ('*' )
.eq ('email' , email)
await db.query (
'SELECT * FROM users WHERE email = $1' ,
[email]
)
Command Injection
❌ WRONG: Unsanitized Shell Commands
import { exec } from 'child_process'
const filename = request.body .filename
exec (`cat ${filename} ` , callback)
✅ CORRECT: Avoid Shell Commands
import { readFile } from 'fs/promises'
import path from 'path'
const filename = request.body .filename
const safePath = path.join ('/safe/directory' , path.basename (filename))
const content = await readFile (safePath, 'utf8' )
4. Insecure Design
❌ WRONG: Weak Password Requirements
function validatePassword (password : string ) {
return password.length >= 6
}
✅ CORRECT: Strong Password Policy
import { z } from 'zod'
const PasswordSchema = z.string ()
.min (12 , 'Password must be at least 12 characters' )
.regex (/[A-Z]/ , 'Must contain uppercase letter' )
.regex (/[a-z]/ , 'Must contain lowercase letter' )
.regex (/[0-9]/ , 'Must contain number' )
.regex (/[^A-Za-z0-9]/ , 'Must contain special character' )
function validatePassword (password : string ) {
try {
PasswordSchema .parse (password)
return { valid : true }
} catch (error) {
return { valid : false , errors : error.errors }
}
}
Input Validation Patterns
Schema-Based Validation
import { z } from 'zod'
const CreateUserSchema = z.object ({
email : z.string ().email (),
name : z.string ().min (1 ).max (100 ),
age : z.number ().int ().min (0 ).max (150 ).optional (),
role : z.enum (['user' , 'admin' , 'moderator' ]),
metadata : z.record (z.string ()).optional ()
})
export async function POST (request : Request ) {
try {
const body = await request.json ()
const validated = CreateUserSchema .parse (body)
const user = await db.users .create (validated)
return NextResponse .json ({ success : true , user })
} catch (error) {
if (error instanceof z.ZodError ) {
return NextResponse .json (
{ error : 'Validation failed' , details : error.errors },
{ status : 400 }
)
}
throw error
}
}
File Upload Validation
const MAX_FILE_SIZE = 5 * 1024 * 1024
const ALLOWED_TYPES = ['image/jpeg' , 'image/png' , 'image/gif' , 'image/webp' ]
const ALLOWED_EXTENSIONS = ['.jpg' , '.jpeg' , '.png' , '.gif' , '.webp' ]
function validateFileUpload (file : File ): { valid : boolean ; error ?: string } {
if (file.size > MAX_FILE_SIZE ) {
return { valid : false , error : 'File too large (max 5MB)' }
}
if (!ALLOWED_TYPES .includes (file.type )) {
return { valid : false , error : 'Invalid file type' }
}
const extension = file.name .toLowerCase ().match (/\.[^.]+$/ )?.[0 ]
if (!extension || !ALLOWED_EXTENSIONS .includes (extension)) {
return { valid : false , error : 'Invalid file extension' }
}
return { valid : true }
}
Sanitize HTML Input
import DOMPurify from 'isomorphic-dompurify'
function sanitizeUserHTML (html : string ): string {
return DOMPurify .sanitize (html, {
ALLOWED_TAGS : ['b' , 'i' , 'em' , 'strong' , 'p' , 'br' , 'ul' , 'ol' , 'li' ],
ALLOWED_ATTR : [],
ALLOW_DATA_ATTR : false
})
}
function UserContent ({ html }: { html: string } ) {
const clean = sanitizeUserHTML (html)
return <div dangerouslySetInnerHTML ={{ __html: clean }} />
}
Authentication Patterns
JWT Token Handling
❌ WRONG: localStorage (XSS vulnerable)
localStorage .setItem ('token' , token)
✅ CORRECT: httpOnly Cookies
export async function POST (request : Request ) {
const { email, password } = await request.json ()
const user = await authenticateUser (email, password)
if (!user) {
return NextResponse .json ({ error : 'Invalid credentials' }, { status : 401 })
}
const token = await generateJWT (user)
const response = NextResponse .json ({ success : true })
response.cookies .set ('token' , token, {
httpOnly : true ,
secure : true ,
sameSite : 'strict' ,
maxAge : 60 * 60 * 24
})
return response
}
Password Hashing
❌ WRONG: Plain Text or Weak Hashing
import crypto from 'crypto'
const user = { email, password : password }
const hash = crypto.createHash ('md5' ).update (password).digest ('hex' )
✅ CORRECT: bcrypt or Argon2
import bcrypt from 'bcryptjs'
async function hashPassword (password : string ): Promise <string > {
const saltRounds = 12
return await bcrypt.hash (password, saltRounds)
}
async function verifyPassword (password : string , hash : string ): Promise <boolean > {
return await bcrypt.compare (password, hash)
}
const hashedPassword = await hashPassword (plainPassword)
await db.users .create ({ email, password : hashedPassword })
Multi-Factor Authentication
import speakeasy from 'speakeasy'
import QRCode from 'qrcode'
async function setupMFA (userId : string , email : string ) {
const secret = speakeasy.generateSecret ({
name : `MyApp (${email} )` ,
length : 32
})
await db.users .update ({
where : { id : userId },
data : { mfaSecret : secret.base32 }
})
const qrCode = await QRCode .toDataURL (secret.otpauth_url !)
return { secret : secret.base32 , qrCode }
}
function verifyMFAToken (token : string , secret : string ): boolean {
return speakeasy.totp .verify ({
secret,
encoding : 'base32' ,
token,
window : 2
})
}
Authorization Patterns
Role-Based Access Control (RBAC)
type Role = 'user' | 'moderator' | 'admin'
const PERMISSIONS = {
user : ['read:own' , 'write:own' ],
moderator : ['read:all' , 'write:all' , 'delete:flagged' ],
admin : ['read:all' , 'write:all' , 'delete:all' , 'manage:users' ]
} as const
function hasPermission (role : Role , permission : string ): boolean {
return PERMISSIONS [role].includes (permission)
}
async function requirePermission (permission : string ) {
return async (request : Request ) => {
const session = await getSession (request)
if (!session || !hasPermission (session.role , permission)) {
return NextResponse .json ({ error : 'Forbidden' }, { status : 403 })
}
return null
}
}
export async function DELETE (request : Request ) {
const authError = await requirePermission ('delete:all' )(request)
if (authError) return authError
}
Row Level Security (Supabase)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
CREATE POLICY "Admins full access"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'admin'
)
);
Rate Limiting
import rateLimit from 'express-rate-limit'
const apiLimiter = rateLimit ({
windowMs : 15 * 60 * 1000 ,
max : 100 ,
message : 'Too many requests, please try again later' ,
standardHeaders : true ,
legacyHeaders : false
})
const searchLimiter = rateLimit ({
windowMs : 60 * 1000 ,
max : 10 ,
message : 'Too many search requests'
})
const authLimiter = rateLimit ({
windowMs : 15 * 60 * 1000 ,
max : 5 ,
skipSuccessfulRequests : true
})
app.use ('/api/' , apiLimiter)
app.use ('/api/search' , searchLimiter)
app.use ('/api/auth/login' , authLimiter)
Security Headers
const securityHeaders = [
{
key : 'X-DNS-Prefetch-Control' ,
value : 'on'
},
{
key : 'Strict-Transport-Security' ,
value : 'max-age=63072000; includeSubDomains; preload'
},
{
key : 'X-Frame-Options' ,
value : 'SAMEORIGIN'
},
{
key : 'X-Content-Type-Options' ,
value : 'nosniff'
},
{
key : 'X-XSS-Protection' ,
value : '1; mode=block'
},
{
key : 'Referrer-Policy' ,
value : 'origin-when-cross-origin'
},
{
key : 'Content-Security-Policy' ,
value : `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
` .replace (/\s{2,}/g , ' ' ).trim ()
}
]
module .exports = {
async headers ( ) {
return [
{
source : '/:path*' ,
headers : securityHeaders
}
]
}
}
Logging Best Practices
❌ WRONG: Logging Sensitive Data
console .log ('Login attempt:' , { email, password })
console .log ('Payment processed:' , { cardNumber, cvv, amount })
console .log ('Error:' , error)
✅ CORRECT: Redact Sensitive Data
function sanitizeForLogging (data : any ): any {
const sensitive = ['password' , 'token' , 'secret' , 'apiKey' , 'cvv' , 'ssn' ]
const sanitized = { ...data }
for (const key of sensitive) {
if (key in sanitized) {
sanitized[key] = '[REDACTED]'
}
}
return sanitized
}
console .log ('Login attempt:' , sanitizeForLogging ({ email, password }))
console .log ('Payment processed:' , { userId, amount, last4 : card.last4 })
console .error ('Error:' , { message : error.message , userId, endpoint })
Pre-Deployment Security Checklist
Secrets : No hardcoded secrets, all in environment variables
Input Validation : All user inputs validated with schemas
SQL Injection : All queries use parameterized queries or ORM
XSS Prevention : User HTML sanitized, CSP headers configured
CSRF Protection : SameSite cookies, CSRF tokens on mutations
Authentication : Tokens in httpOnly cookies, passwords hashed with bcrypt
Authorization : Role checks on all protected routes
Rate Limiting : Enabled on all public endpoints
HTTPS : Enforced in production (HSTS headers)
Security Headers : All headers configured (CSP, X-Frame-Options, etc.)
Error Handling : Generic error messages, no stack traces to users
Logging : No sensitive data logged (passwords, tokens, PII)
Dependencies : Up to date, no known vulnerabilities (npm audit)
File Uploads : Validated (size, type, extension)
CORS : Properly configured, not * in production
Resources