Development
Security and Hardening
Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations.
Security-first development practices for web applications. Treat every external input as hostile.
When to Use
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
Threat Model First
Before hardening, spend five minutes thinking like an attacker:
- Map the trust boundaries. Where does untrusted data cross into your system?
- Name the assets. What's worth stealing or breaking?
- Run STRIDE over each boundary:
| Threat | Ask | Typical mitigation |
|---|---|---|
| Spoofing | Can someone impersonate a user/service? | Authentication, signature verification |
| Tampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries |
| Repudiation | Can an action be denied later? | Audit logging of security events |
| Information disclosure | Can data leak? | Encryption, field allowlists |
| Denial of service | Can it be overwhelmed? | Rate limiting, input size caps |
| Elevation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
The Three-Tier Boundary System
Always Do (No Exceptions)
- Validate all external input at the system boundary
- Parameterize all database queries — never concatenate user input into SQL
- Encode output to prevent XSS
- Use HTTPS for all external communication
- Hash passwords with bcrypt/scrypt/argon2
- Set security headers (CSP, HSTS, X-Frame-Options)
- Use httpOnly, secure, sameSite cookies for sessions
- Run
npm auditbefore every release
Ask First (Requires Human Approval)
- Adding new authentication flows
- Storing new categories of sensitive data
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
Never Do
- Never commit secrets to version control
- Never log sensitive data (passwords, tokens, full credit card numbers)
- Never trust client-side validation as a security boundary
- Never disable security headers for convenience
- Never use
eval()orinnerHTMLwith user-provided data
OWASP Top 10 Prevention Patterns
Injection (SQL, NoSQL, OS Command)
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
Broken Authentication
// Password hashing
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
// Session management
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000,
},
}));
Cross-Site Scripting (XSS)
// BAD: Rendering user input as HTML
element.innerHTML = userInput;
// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;
// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
Broken Access Control
// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// Check that the authenticated user owns this resource
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Not authorized' }
});
}
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
Input Validation Patterns
Schema Validation at Boundaries
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200).trim(),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
dueDate: z.string().datetime().optional(),
});
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: result.error.flatten(),
},
});
}
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
Rate Limiting
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
}));
Secrets Management
.env files:
├── .env.example → Committed (template with placeholder values)
├── .env → NOT committed (contains real secrets)
└── .env.local → NOT committed (local overrides)
.gitignore must include:
.env
.env.local
.env.*.local
*.pem
*.key
Always check before committing:
git diff --cached | grep -i "password\|secret\|api_key\|token"
Security Review Checklist
- Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- Session tokens are httpOnly, secure, sameSite
- Login has rate limiting
- Every endpoint checks user permissions
- Users can only access their own resources
- All user input validated at the boundary
- SQL queries are parameterized
- HTML output is encoded/escaped
- No secrets in code or version control
- Sensitive fields excluded from API responses
- Security headers configured (CSP, HSTS, etc.)
- CORS restricted to known origins
- Dependencies audited for vulnerabilities
- Error messages don't expose internals
Verification
After implementing security-relevant code:
-
npm auditshows no critical or high vulnerabilities - No secrets in source code or git history
- All user input validated at system boundaries
- Authentication and authorization checked on every protected endpoint
- Security headers present in response
- Error responses don't expose internal details
- Rate limiting active on auth endpoints