Security and Hardening
Overview
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
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
The Three-Tier Boundary System
Always Do (No Exceptions)
- •Validate all external input at the system boundary (API routes, form handlers)
- •Parameterize all database queries — never concatenate user input into SQL
- •Encode output to prevent XSS (use framework auto-escaping, don't bypass it)
- •Use HTTPS for all external communication
- •Hash passwords with bcrypt/scrypt/argon2 (never store plaintext)
- •Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- •Use httpOnly, secure, sameSite cookies for sessions
- •Run
npm audit(or equivalent) before every release
Ask First (Requires Human Approval)
- •Adding new authentication flows or changing auth logic
- •Storing new categories of sensitive data (PII, payment info)
- •Adding new external service integrations
- •Changing CORS configuration
- •Adding file upload handlers
- •Modifying rate limiting or throttling
- •Granting elevated permissions or roles
Never Do
- •Never commit secrets to version control (API keys, passwords, tokens)
- •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 - •Never store sessions in client-accessible storage (localStorage for auth tokens)
- •Never expose stack traces or internal error details to users
OWASP Top 10 Prevention
1. Injection (SQL, NoSQL, OS Command)
typescript
// 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 } });
2. Broken Authentication
typescript
// 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, // From environment, not code
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
3. Cross-Site Scripting (XSS)
typescript
// 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);
4. Broken Access Control
typescript
// 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 to modify this task' }
});
}
// Proceed with update
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
5. Security Misconfiguration
typescript
// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());
// Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
// CORS — restrict to known origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));
6. Sensitive Data Exposure
typescript
// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
Input Validation Patterns
Schema Validation at Boundaries
typescript
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(),
});
// Validate at the route handler
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(),
},
});
}
// result.data is now typed and validated
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
File Upload Safety
typescript
// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateUpload(file: UploadedFile) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('File type not allowed');
}
if (file.size > MAX_SIZE) {
throw new ValidationError('File too large (max 5MB)');
}
// Don't trust the file extension — check magic bytes if critical
}
Rate Limiting
typescript
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 10 attempts per 15 minutes
}));
Secrets Management
code
.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:
bash
# Check for accidentally staged secrets git diff --cached | grep -i "password\|secret\|api_key\|token"
Security Review Checklist
markdown
### Authentication - [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12) - [ ] Session tokens are httpOnly, secure, sameSite - [ ] Login has rate limiting - [ ] Password reset tokens expire ### Authorization - [ ] Every endpoint checks user permissions - [ ] Users can only access their own resources - [ ] Admin actions require admin role verification ### Input - [ ] All user input validated at the boundary - [ ] SQL queries are parameterized - [ ] HTML output is encoded/escaped ### Data - [ ] No secrets in code or version control - [ ] Sensitive fields excluded from API responses - [ ] PII encrypted at rest (if applicable) ### Infrastructure - [ ] Security headers configured (CSP, HSTS, etc.) - [ ] CORS restricted to known origins - [ ] Dependencies audited for vulnerabilities - [ ] Error messages don't expose internals
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
Red Flags
- •User input passed directly to database queries, shell commands, or HTML rendering
- •Secrets in source code or commit history
- •API endpoints without authentication or authorization checks
- •Missing CORS configuration or wildcard (
*) origins - •No rate limiting on authentication endpoints
- •Stack traces or internal errors exposed to users
- •Dependencies with known critical vulnerabilities
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 (check with browser DevTools)
- • Error responses don't expose internal details
- • Rate limiting active on auth endpoints