Use when performing security reviews or hardening an application. Covers OWASP top 10 mitigations, input validation, injection prevention, authentication, secrets management, and dependency scanning.
// VULNERABLE: string concatenation
const result = await db.execute(`SELECT * FROM users WHERE email = '${email}'`);
// SAFE: parameterized queries (Drizzle handles this automatically)
const result = await db.select().from(users).where(eq(users.email, email));
// SAFE: raw SQL with parameters when needed
const result = await db.execute(sql`SELECT * FROM users WHERE email = ${email}`);
// React escapes by default. These are safe:
<p>{userInput}</p>
<div>{comment.body}</div>
// DANGEROUS: never use dangerouslySetInnerHTML with user input
// BAD:
<div dangerouslySetInnerHTML={{ __html: userComment }} />
// If you must render HTML, sanitize with DOMPurify:
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} />
// Set Content-Security-Policy headers
// next.config.js
const securityHeaders = [
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self'" },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
];
// Validate ALL input at the API boundary with Zod
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(13).max(150),
url: z.string().url().optional(),
});
// Validate file uploads
const uploadSchema = z.object({
mimetype: z.enum(['image/jpeg', 'image/png', 'image/webp']),
size: z.number().max(5 * 1024 * 1024), // 5MB limit
});
// Never trust Content-Type headers alone for file validation
// Verify magic bytes or use a library like file-type
// For cookie-based auth, use CSRF tokens
import csrf from 'csurf';
app.use(csrf({ cookie: { httpOnly: true, sameSite: 'strict' } }));
// For token-based auth (Bearer tokens), CSRF is not a concern
// because browsers don't auto-attach Authorization headers
// Always set SameSite on cookies
res.cookie('session', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // or 'lax' for GET navigation
maxAge: 24 * 60 * 60 * 1000,
path: '/',
});
// Password hashing: use bcrypt or argon2, never SHA/MD5
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12); // cost factor >= 12
const isValid = await bcrypt.compare(inputPassword, storedHash);
// JWT best practices
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m', algorithm: 'HS256' } // short-lived access tokens
);
// Always check authorization at the resource level
async function getProject(req: Request, res: Response) {
const project = await db.select().from(projects).where(eq(projects.id, req.params.id));
// ALWAYS verify ownership/permissions, never just authentication
if (project.ownerId !== req.user.id && req.user.role !== 'admin') {
throw new AppError(403, 'FORBIDDEN', 'You do not have access to this project');
}
}
# NEVER commit secrets to version control
# .gitignore must include:
.env
.env.*
*.pem
*.key
credentials.json
// Validate secrets exist at startup
const requiredSecrets = ['JWT_SECRET', 'DATABASE_URL', 'SMTP_PASSWORD'];
for (const secret of requiredSecrets) {
if (!process.env[secret]) {
throw new Error(`Missing required secret: ${secret}`);
}
}
// Use platform-specific secret stores in production:
// - Vercel: Environment Variables
// - AWS: SSM Parameter Store or Secrets Manager
// - Railway: Variables
import cors from 'cors';
// NEVER use origin: '*' with credentials
// BAD:
app.use(cors({ origin: '*', credentials: true }));
// GOOD: explicit allowlist
app.use(cors({
origin: ['https://otto.example.com', 'https://app.otto.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
# Run regularly in CI
npm audit
npm audit --audit-level=high
# Use GitHub Dependabot or Snyk for automated scanning
# .github/dependabot.yml