mit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npx detect-secrets-hook --baseline .secrets.baseline
#### 2. Input Validation and Injection Prevention
Input validation must occur at every trust boundary. Schema validation libraries provide runtime checks and type inference, reducing the risk of injection attacks.
**Architecture Decision:** Validate all external inputs immediately upon entry. Use parameterized queries for database interactions to eliminate SQL injection vectors.
**Implementation:**
```typescript
// src/schemas/user.schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
body: z.object({
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user'),
metadata: z.record(z.string(), z.unknown()).optional(),
}),
});
// src/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
export const validate = (schema: ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (err) {
res.status(400).json({ error: 'Validation failed' });
}
};
};
// src/routes/user.routes.ts
import { Router } from 'express';
import { validate } from '../middleware/validate';
import { CreateUserSchema } from '../schemas/user.schema';
import { db } from '../db';
const router = Router();
router.post(
'/users',
validate(CreateUserSchema),
async (req, res) => {
const { email, role } = req.body;
// Parameterized query prevents SQL injection
const result = await db.query(
'INSERT INTO users (email, role) VALUES ($1, $2) RETURNING id',
[email, role]
);
res.status(201).json(result.rows[0]);
}
);
3. Access Control and Resource Ownership
Broken access control often stems from missing ownership checks. Every request to a protected resource must verify that the authenticated user has permission to access or modify that specific resource.
Architecture Decision: Implement authorization as middleware that runs after authentication. Use UUIDs for public-facing identifiers to prevent enumeration attacks, while retaining sequential IDs for internal database performance.
Implementation:
// src/middleware/authorize.ts
import { Request, Response, NextFunction } from 'express';
import { db } from '../db';
export const requireOwnership = async (req: Request, res: Response, next: NextFunction) => {
const resourceId = req.params.id;
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Verify ownership in a single query
const resource = await db.query(
'SELECT id FROM resources WHERE id = $1 AND owner_id = $2',
[resourceId, userId]
);
if (resource.rows.length === 0) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// src/utils/identifiers.ts
import { v7 as uuidv7 } from 'uuid';
export const generatePublicId = () => uuidv7();
// Use UUIDv7 for sortable, time-based public IDs
4. Rate Limiting and Security Headers
Rate limiting protects against abuse and brute-force attacks. Security headers mitigate client-side attacks like XSS and clickjacking.
Architecture Decision: Apply rate limiting at the application layer for granular control, or use CDN-level limiting for high-traffic endpoints. Configure security headers via middleware to ensure consistent application.
Implementation:
// src/middleware/security.ts
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { Express } from 'express';
export const configureSecurity = (app: Express) => {
// Security Headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Minimize unsafe-inline in production
},
},
hsts: { maxAge: 31536000, includeSubDomains: true },
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts' },
});
app.post('/auth/login', authLimiter);
};
5. Cryptography and Error Handling
Weak cryptographic algorithms and verbose error messages expose sensitive data and facilitate attacks.
Architecture Decision: Use memory-hard hashing algorithms for passwords and authenticated encryption for data. Implement a global error handler to sanitize responses in production.
Implementation:
// src/utils/crypto.ts
import { hash, verify } from '@node-rs/argon2';
export const hashPassword = async (password: string) => {
return hash(password, {
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});
};
export const verifyPassword = async (hash: string, password: string) => {
return verify(hash, password);
};
// src/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
// Log detailed error internally
console.error(`[ERROR] ${new Date().toISOString()} - ${err.message}`, err.stack);
// Return generic message to client
const isProduction = process.env.NODE_ENV === 'production';
const message = isProduction ? 'Internal Server Error' : err.message;
res.status(err.status || 500).json({
error: message,
...(isProduction ? {} : { stack: err.stack }),
});
};
6. Dependency Management
Outdated dependencies introduce known vulnerabilities. Automated scanning must be integrated into the CI pipeline.
Implementation:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --audit-level=high
# Fails CI on high/critical vulnerabilities
Pitfall Guide
| Pitfall Name | Explanation | Fix |
|---|
Committed .env Files | Developers often commit .env files for convenience, exposing secrets in git history. | Add .env to .gitignore. Use detect-secrets in pre-commit hooks. Rotate any exposed secrets immediately. |
| Client-Side Validation Only | Relying solely on frontend validation allows attackers to bypass checks via direct API calls. | Validate all inputs at the server boundary using schema libraries like Zod. Treat client data as untrusted. |
| IDOR on Secondary Keys | Checking ownership only on the primary id field while ignoring other identifiers like email or slug. | Implement ownership checks for all resource identifiers. Use a unified authorization middleware that validates access for every query parameter. |
| Global Rate Limits | Applying a single rate limit across all endpoints allows attackers to exhaust limits on low-value routes. | Configure granular rate limits per route or route group. Apply stricter limits to authentication and sensitive operations. |
CSP unsafe-inline | Using unsafe-inline in Content-Security-Policy negates XSS protection by allowing inline scripts. | Use nonces or hashes for inline scripts. Refactor code to external scripts. Start with a restrictive CSP and relax only as necessary. |
Ignoring npm audit Warnings | Treating audit warnings as noise rather than actionable items leads to accumulation of vulnerabilities. | Configure CI to fail on high/critical vulnerabilities. Use npm audit fix regularly. Pin dependency versions where possible. |
| MD5 for Integrity Checks | Using MD5 for checksums or non-password hashing is risky due to collision vulnerabilities. | Use SHA-256 or SHA-3 for integrity checks. Reserve MD5 only for legacy compatibility where security is not a concern. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Traffic Public API | CDN Rate Limiting | Offloads computation from application servers; scales automatically. | Low (CDN tier upgrade) |
| Internal Microservice | Application Rate Limiting | Provides granular control based on service context and user roles. | None |
| Public Resource URLs | UUIDs (v7) | Prevents enumeration attacks; v7 offers sortability for indexing. | None |
| Internal Database Keys | Sequential IDs | Optimizes storage and index performance; reduces join overhead. | None |
| Password Storage | Argon2 | Memory-hard algorithm resists GPU/ASIC attacks better than bcrypt. | Low (CPU overhead) |
| Legacy System Migration | bcrypt | Widely supported; easier integration with older libraries. | Low |
| Data Encryption at Rest | AES-256-GCM | Authenticated encryption ensures confidentiality and integrity. | None |
| Error Reporting | Structured Logging | Enables correlation with traces while keeping user responses clean. | Low (Logging infra) |
Configuration Template
// src/security-config.ts
import { Express } from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { errorHandler } from './middleware/error-handler';
import { validate } from './middleware/validate';
import { requireOwnership } from './middleware/authorize';
export const applySecurityStack = (app: Express) => {
// 1. Headers
app.use(helmet());
// 2. Rate Limiting
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
});
app.use('/api', globalLimiter);
// 3. Validation & Auth Middleware Registration
// Example route setup
app.post('/api/resources',
validate(ResourceSchema),
async (req, res) => { /* ... */ }
);
app.get('/api/resources/:id',
requireOwnership,
async (req, res) => { /* ... */ }
);
// 4. Error Handling (Must be last)
app.use(errorHandler);
};
Quick Start Guide
-
Initialize Security Dependencies:
npm install zod helmet express-rate-limit @node-rs/argon2 uuid
npm install -D git-secrets detect-secrets husky lint-staged
-
Configure Pre-Commit Hooks:
npx husky init
# Add detect-secrets and lint-staged to .husky/pre-commit
-
Add Middleware to Entry Point:
Import and apply the security stack in your main application file.
import { applySecurityStack } from './security-config';
const app = express();
applySecurityStack(app);
-
Run Initial Audit:
Execute a dependency scan and fix vulnerabilities.
npm audit --audit-level=high
npm audit fix
-
Verify Headers:
Start the server and test headers using curl or an online scanner.
curl -I http://localhost:3000
This strategy addresses the most prevalent vulnerabilities identified in current codebases while providing actionable, production-ready implementations. By integrating these controls into the development workflow, teams can significantly reduce risk exposure and improve overall security posture.