Secure coding is about writing code that resists attacks. This guide covers defensive programming techniques, secure patterns, and best practices for JavaScript, TypeScript, and Node.js applications.
Input Validation Principles
Validate Early, Validate Often
// ✅ Validate at API boundary
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
age: z.number().int().min(18).max(120),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/).optional(),
});
async function createUser(req: Request) {
// Validate immediately
const result = userSchema.safeParse(await req.json());
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.errors },
{ status: 400 }
);
}
// Use validated data
const user = await db.users.create({ data: result.data });
return Response.json(user);
}
Output Encoding
Context-Specific Encoding
// HTML context
import DOMPurify from 'isomorphic-dompurify';
function escapeHTML(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// For rich content, use DOMPurify
const cleanHTML = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href'],
});
// JavaScript context
function escapeJS(str: string): string {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/
Error Handling
Secure Error Messages
// ❌ VULNERABLE - Exposes internal details
try {
await db.users.create({ data });
} catch (error) {
res.status(500).json({ error: error.message }); // Exposes DB structure
}
// ✅ SAFE - Generic error messages
try {
await db.users.create({ data });
} catch (error) {
// Log detailed error server-side
logger.error('User creation failed', { error, userId: data.email });
// Return generic message to client
res.status(500).json({
error: 'An error occurred. Please try again later.',
requestId: req.id,
});
}
// Different messages for different error types
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Email already exists' });
}
if (error.code === 'P2025') {
return res.status(404).json({ error: 'Resource not found' });
}
Secure Dependencies
Dependency Management
// Use exact versions for critical packages
{
"dependencies": {
"express": "4.18.2", // Exact version
"jsonwebtoken": "^9.0.0", // Careful with ^
}
}
// Lock file security
// Always commit package-lock.json
// Verify checksums for critical packages
// Regular audits
npm audit
npm audit fix --force // Use carefully
// Use Snyk or Dependabot
// Review dependency updates before merging
Secure File Operations
import path from 'path';
import fs from 'fs/promises';
// ✅ Prevent path traversal
function sanitizeFilename(filename: string): string {
// Remove directory separators
const basename = path.basename(filename);
// Remove dangerous characters
return basename.replace(/[^a-zA-Z0-9._-]/g, '');
}
async function saveFile(file: File, uploadDir: string) {
const safeFilename = sanitizeFilename(file.name);
const filePath = path.join(uploadDir, safeFilename);
// Ensure path is within upload directory
const resolvedPath = path.resolve(filePath);
const resolvedDir = path.resolve(uploadDir);
if (!resolvedPath.startsWith(resolvedDir)) {
throw new Error('Invalid file path');
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
// Validate file size
if (file.size > 5 * 1024 * 1024) { // 5MB
throw new Error('File too large');
}
await fs.writeFile(resolvedPath, await file.arrayBuffer());
}
Secure Random Number Generation
import crypto from 'crypto';
// ✅ Use crypto.randomBytes for secure randomness
function generateSecureToken(): string {
return crypto.randomBytes(32).toString('hex');
}
function generateSecureID(): string {
return crypto.randomBytes(16).toString('base64url');
}
// ❌ NEVER use Math.random() for security
// Math.random() is predictable and not cryptographically secure
Secure Configuration
// ✅ Use environment variables
const config = {
database: {
url: process.env.DATABASE_URL!,
},
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
},
api: {
key: process.env.API_KEY!,
},
};
// Validate required environment variables
function validateConfig() {
const required = ['DATABASE_URL', 'JWT_SECRET', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
}
// Never commit secrets
// Use .env.example for documentation
// Use secret management services in production
Secure Logging
// ❌ VULNERABLE - Logs sensitive data
logger.info('User login', { email, password });
// ✅ SAFE - Sanitize logs
function sanitizeForLogging(data: any): any {
const sensitive = ['password', 'token', 'secret', 'key', 'ssn', 'creditCard'];
const sanitized = { ...data };
for (const key in sanitized) {
if (sensitive.some(s => key.toLowerCase().includes(s))) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
logger.info('User login', sanitizeForLogging({ email, password }));
// Log security events
logger.warn('Failed login attempt', {
email,
ip: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date().toISOString(),
});
Code Review Security Checklist
- ✅ All user inputs validated
- ✅ Output properly encoded
- ✅ No hardcoded secrets
- ✅ Error messages don't leak information
- ✅ SQL queries use parameters
- ✅ Authentication and authorization checks present
- ✅ File operations validate paths
- ✅ Secure random number generation used
- ✅ Dependencies are up to date
- ✅ Sensitive data not logged
Secure Coding Patterns
Fail Secure
// Default to denying access
function checkPermission(user: User, resource: Resource): boolean {
// Fail secure: deny by default
if (!user || !resource) {
return false;
}
// Explicit allow
return user.role === 'admin' || resource.ownerId === user.id;
}
Principle of Least Privilege
Grant only the minimum permissions necessary. Use role-based access control and verify permissions for every operation.
Conclusion
Secure coding requires a defensive mindset. Validate all inputs, encode all outputs, handle errors securely, manage dependencies carefully, and follow secure coding patterns. Regular code reviews focused on security help catch vulnerabilities early. Remember: security is not optional—it's a fundamental requirement.