Security & Vulnerability Prevention
Overview
Security isn't optional. This skill covers common vulnerabilities, prevention patterns, and security-first thinking for modern web applications.
Why it matters:
- •Vulnerabilities lead to data breaches ($4.45M average cost)
- •Users trust you with their data (GDPR, CCPA compliance needed)
- •Security debt grows with inaction
- •Prevention is 100x cheaper than remediation
Core Concepts
1. OWASP Top 10 (Know Them)
typescript
// 1. Broken Access Control
// WRONG: Trusting client to enforce permissions
const deleteUser = (id: string) => {
return fetch(`/api/users/${id}`, { method: 'DELETE' });
};
// RIGHT: Verify permissions on server
async function deleteUser(userId: string, currentUser: User) {
if (userId === currentUser.id || currentUser.role === 'admin') {
// Only allow deleting self or admin deleting anyone
await db.users.delete(userId);
} else {
throw new Error('Forbidden');
}
}
// 2. Cryptographic Failures
// WRONG: Storing passwords in plain text
const user = { name: 'Alice', password: 'MyPassword123' };
// RIGHT: Hash and salt passwords
import bcrypt from 'bcrypt';
const hashedPassword = await bcrypt.hash('MyPassword123', 10);
const user = { name: 'Alice', password: hashedPassword };
// 3. Injection (SQL, Command, etc.)
// WRONG: String concatenation in queries
const query = `SELECT * FROM users WHERE email = '${email}'`;
// RIGHT: Use parameterized queries
const user = await db.users.findUnique({ where: { email } }); // Prisma handles it
// 4. Insecure Design
// RIGHT: Design security in from the start
function createUser(data: RegisterRequest) {
// Validate email format
// Enforce strong passwords
// Rate limit registrations
// Log security events
}
// 5. Security Misconfiguration
// WRONG: Exposing debug info in production
if (process.env.NODE_ENV === 'production') {
console.error(error); // Don't log errors to client
}
// RIGHT: Log securely
function handleError(error: Error) {
// Log full details to server logs
console.error('[ERROR]', error);
// Return sanitized error to client
return {
error: 'An error occurred. Please try again.',
};
}
// 6. Vulnerable Components
// WRONG: Using outdated packages
package.json: { "lodash": "3.x" } // Outdated!
// RIGHT: Keep dependencies updated
// Regularly run: npm audit, npm update
// 7. Authentication Failures
// WRONG: Reusing user input as ID
const user = await db.users.findFirst({ where: { id: req.body.userId } });
// RIGHT: Use authenticated context
const user = await db.users.findFirst({ where: { id: req.user.id } });
// 8. Software Data Integrity
// RIGHT: Verify downloaded packages haven't been tampered with
// npm uses checksums and HTTPS for integrity
// 9. Logging & Monitoring Failures
// RIGHT: Log security events
function loginUser(email: string) {
// Log ALL login attempts (success and failure)
const user = await authenticate(email);
logger.info('User logged in', { userId: user.id, timestamp: Date.now() });
}
// 10. Server-Side Template Injection
// WRONG: Injecting unsanitized user input into templates
ejs.render(`<h1>${userInput}</h1>`);
// RIGHT: Use safe templating
<h1>{userInput}</h1> // React auto-escapes
2. XSS (Cross-Site Scripting) Prevention
typescript
// ❌ WRONG: Rendering user input as HTML
function Comment({ text }: { text: string }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
// User can inject: <script>alert('hacked')</script>
}
// ✅ CORRECT: React auto-escapes text
function Comment({ text }: { text: string }) {
return <div>{text}</div>; // Auto-escaped
// User input rendered as: <script>...</script>
}
// If HTML is truly needed, sanitize it
import DOMPurify from 'dompurify';
function RichComment({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// Content Security Policy (strongest defense)
// In HTTP headers or meta tag:
// <meta http-equiv="Content-Security-Policy"
// content="script-src 'self'; object-src 'none';">
// Blocks all inline scripts unless whitelisted
3. CSRF (Cross-Site Request Forgery) Prevention
typescript
// ❌ WRONG: Accept state-changing requests without verification
app.post('/api/transfer', (req, res) => {
// Any website can POST here if user is authenticated
transferMoney(req.body);
});
// ✅ CORRECT: Require CSRF token
// 1. Server generates token on page load
const csrfToken = crypto.randomBytes(32).toString('hex');
// Send to client in response
// 2. Client includes token in forms
<form method="POST" action="/api/transfer">
<input type="hidden" name="csrf" value={csrfToken} />
{/* Form fields */}
</form>
// 3. Server validates token before processing
app.post('/api/transfer', (req, res) => {
if (req.body.csrf !== req.session.csrfToken) {
throw new Error('CSRF token invalid');
}
transferMoney(req.body);
});
// Or use SameSite cookies (modern approach)
// Set-Cookie: session=abc123; SameSite=Strict
// Prevents cross-site cookie inclusion entirely
4. Secret Management
typescript
// ❌ WRONG: Committing secrets to git
api_key = "sk-abc123def456" // In .env or code
// ✅ CORRECT: Use environment variables (not in git)
// .env.local (never commit this!)
DATABASE_URL=postgresql://...
API_KEY=sk-abc123def456
STRIPE_SECRET=sk_test_...
// Access in code
const apiKey = process.env.API_KEY; // From env at runtime
// ✅ EVEN BETTER: Use secret management service
// AWS Secrets Manager, HashiCorp Vault, etc.
const apiKey = await secretsManager.get('api-key');
// For NEXT_PUBLIC_ variables (safe to expose)
// Only non-sensitive data
const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL;
// This gets embedded in client-side code
Deep Patterns
Pattern 1: Security Headers
typescript
// Set comprehensive security headers
function setSecurityHeaders(res: Response) {
// Prevent MIME sniffing
res.setHeader("X-Content-Type-Options", "nosniff");
// Prevent clickjacking
res.setHeader("X-Frame-Options", "DENY");
// Prevent XSS (even if filter bypassed)
res.setHeader("X-XSS-Protection", "1; mode=block");
// Content Security Policy (strongest)
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'",
);
// HSTS (force HTTPS)
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
// Referrer policy (privacy)
res.setHeader("Referrer-Policy", "no-referrer");
}
Pattern 2: Input Validation & Sanitization
typescript
// Always validate & sanitize user input
function validateUserInput(input: unknown): string {
if (typeof input !== "string") {
throw new Error("Must be a string");
}
// Length check
if (input.length > 1000) {
throw new Error("Too long");
}
// Pattern check
if (!/^[a-zA-Z0-9\s]*$/.test(input)) {
throw new Error("Invalid characters");
}
// Trim whitespace
return input.trim();
}
// Use schema validation library
import { z } from "zod";
const UserSchema = z.object({
email: z.string().email(),
password: z.string().min(12).regex(/[A-Z]/).regex(/[0-9]/),
name: z.string().min(2).max(100),
});
function createUser(data: unknown) {
const validated = UserSchema.parse(data); // Throws if invalid
return saveUser(validated);
}
Pattern 3: Rate Limiting
typescript
// Prevent brute force attacks
class RateLimiter {
private attempts = new Map<string, number[]>();
isAllowed(userId: string, maxAttempts = 5, windowMs = 60000): boolean {
const now = Date.now();
const times = this.attempts.get(userId) || [];
// Remove old attempts outside window
const recentAttempts = times.filter((t) => now - t < windowMs);
if (recentAttempts.length >= maxAttempts) {
return false; // Too many attempts
}
// Record this attempt
recentAttempts.push(now);
this.attempts.set(userId, recentAttempts);
return true;
}
}
// Usage
const limiter = new RateLimiter();
app.post("/api/login", (req, res) => {
if (!limiter.isAllowed(req.body.email)) {
return res.status(429).send("Too many login attempts");
}
authenticateUser(req.body);
});
Pattern 4: Secure Cookie Settings
typescript
// Set httpOnly, Secure, SameSite flags
function setAuthCookie(res: Response, token: string) {
res.setHeader(
"Set-Cookie",
[
`auth=${token}`,
"HttpOnly", // Not accessible to JavaScript
"Secure", // Only over HTTPS
"SameSite=Strict", // Not sent cross-site
"Max-Age=3600", // Expires in 1 hour
"Path=/",
].join("; "),
);
}
Anti-Patterns to Avoid
typescript
// ❌ Wrong: Storing passwords in plain text
const user = { name: "Alice", password: "secret" };
// ✅ Correct: Hash passwords with salt
const hashedPassword = await bcrypt.hash("secret", 10);
// ❌ Wrong: Exposing error details to users
if (emailExists) {
throw new Error("This email is already registered");
// Attacker knows this account exists!
}
// ✅ Correct: Generic error messages for security
function registerUser(email: string) {
// Don't reveal whether email exists
return sendEmail(email, "If this email is registered..."); // Same message always
}
// ❌ Wrong: Trusting client-side validation only
if (amount > 0) {
transfer(); // Client can be bypassed!
}
// ✅ Correct: Validate on server
app.post("/transfer", (req, res) => {
const amount = parseInt(req.body.amount); // Re-parse
if (amount <= 0) throw new Error("Invalid amount");
transfer(amount);
});
Security Checklist
Before deployment:
- • All secrets stored in environment variables (never in code)
- • HTTPS enabled (Secure cookie flag set)
- • Input validation on all user inputs
- • CSRF tokens required for state-changing operations
- • Security headers set (CSP, HSTS, etc.)
- • Dependency vulnerabilities checked (
npm audit) - • Rate limiting on login/forms
- • Error messages don't expose sensitive info
- • Database queries parameterized (Prisma/ORM)
- • User permissions verified on server
Tools & Services
- •npm audit - Check dependencies for vulnerabilities
- •OWASP ZAP - Automated security scanner
- •Snyk - Continuous vulnerability monitoring
- •Auth0/Auth2go - Secure authentication services
- •Sentry - Error tracking without exposing data
- •HashiCorp Vault - Secret management
Common Vulnerabilities in Web Apps
| Vulnerability | Impact | Prevention |
|---|---|---|
| SQL Injection | Attacker reads entire database | Parameterized queries (Prisma) |
| XSS | Attacker steals user sessions | Sanitize input, auto-escape (React) |
| CSRF | Attacker performs actions as user | CSRF tokens, SameSite cookies |
| Weak Auth | Attacker guesses passwords | Strong passwords, rate limiting, MFA |
| Broken Access | Attacker accesses others' data | Server-side permission checks |
Key Questions
- •What secrets does the app have? (API keys, DB creds, OAuth tokens)
- •Who can access this data? (Auth model correct?)
- •What if user input is malicious? (Sanitized?)
- •Is this over HTTPS? (Always!)
- •Are dependencies up-to-date? (Security patches current?)