Security
Secure Coding
Best practices for writing secure code and preventing common vulnerabilities
Secure coding is the practice of writing software that is resistant to security vulnerabilities. This guide covers common vulnerability patterns and how to prevent them in your code.
Secure Coding Principles
Input Validation
Never trust user input. All data from external sources must be validated:
// BAD: Direct use of user input
const userId = req.params.id;
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
// GOOD: Parameterized query with validation
const userId = req.params.id;
if (!isValidUUID(userId)) {
throw new ValidationError('Invalid user ID format');
}
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);Validation Rules
| Input Type | Validation |
|---|---|
| IDs | Format check (UUID, numeric), existence check |
| Strings | Length limits, character allowlists, encoding |
| Numbers | Range validation, type coercion |
| Dates | Format validation, range checks |
| Files | Type verification, size limits, content scanning |
| URLs | Protocol allowlist, domain validation |
Injection Prevention
SQL Injection
// BAD: String concatenation
const query = `SELECT * FROM users WHERE email = '${email}'`;
// GOOD: Parameterized queries
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);
// GOOD: Using ORM
const user = await prisma.user.findUnique({
where: { email: email }
});NoSQL Injection
// BAD: Direct object use
const user = await User.findOne({ username: req.body.username });
// Attacker sends: { "username": { "$gt": "" } }
// GOOD: Type validation
const username = String(req.body.username);
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
throw new ValidationError('Invalid username');
}
const user = await User.findOne({ username });Command Injection
// BAD: Shell command with user input
const output = execSync(`convert ${filename} output.png`);
// GOOD: Avoid shell, use arrays
const output = execFileSync('convert', [filename, 'output.png']);
// GOOD: Allowlist validation
const allowedFormats = ['jpg', 'png', 'gif'];
const ext = path.extname(filename).slice(1);
if (!allowedFormats.includes(ext)) {
throw new ValidationError('Invalid file format');
}Authentication Security
Password Handling
import bcrypt from 'bcrypt';
// GOOD: Proper password hashing
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Password requirements
function validatePassword(password: string): boolean {
const minLength = 12;
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*]/.test(password);
return (
password.length >= minLength &&
hasUppercase &&
hasLowercase &&
hasNumber &&
hasSpecial
);
}Session Management
// GOOD: Secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
name: '__Host-session', // Secure prefix
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
}
}));
// Regenerate session on authentication
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
if (user) {
req.session.regenerate((err) => {
req.session.userId = user.id;
res.redirect('/dashboard');
});
}
});JWT Security
import jwt from 'jsonwebtoken';
// GOOD: Secure JWT configuration
const JWT_CONFIG = {
algorithm: 'RS256', // Asymmetric algorithm
expiresIn: '15m', // Short expiry
issuer: 'your-app',
audience: 'your-api',
};
function createToken(payload: object): string {
return jwt.sign(payload, privateKey, JWT_CONFIG);
}
function verifyToken(token: string): object {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Explicitly specify allowed algorithms
issuer: 'your-app',
audience: 'your-api',
});
}Authorization
Role-Based Access Control (RBAC)
// Define permissions
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'admin'],
editor: ['read', 'write'],
viewer: ['read'],
} as const;
// Authorization middleware
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role;
const userPermissions = PERMISSIONS[userRole] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage
app.delete('/users/:id', requirePermission('delete'), deleteUser);Object-Level Authorization
// GOOD: Check ownership before access
async function getDocument(req: Request, res: Response) {
const document = await Document.findById(req.params.id);
if (!document) {
return res.status(404).json({ error: 'Not found' });
}
// Verify user has access to this specific document
if (document.ownerId !== req.user.id &&
!document.sharedWith.includes(req.user.id)) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(document);
}Cross-Site Scripting (XSS) Prevention
Output Encoding
import { escape } from 'html-escaper';
// BAD: Direct interpolation
const html = `<div>${userInput}</div>`;
// GOOD: HTML encoding
const html = `<div>${escape(userInput)}</div>`;
// For React/JSX - automatic escaping
function UserProfile({ name }: { name: string }) {
// React automatically escapes this
return <div>{name}</div>;
}
// DANGEROUS: Explicitly render HTML (avoid if possible)
function RenderHTML({ content }: { content: string }) {
// Sanitize first if you must render HTML
const sanitized = DOMPurify.sanitize(content);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}Content Security Policy
// GOOD: Strict CSP headers
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '));
next();
});Cross-Site Request Forgery (CSRF) Prevention
import csrf from 'csurf';
// GOOD: CSRF protection for state-changing requests
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// CSRF token validated automatically
processSubmission(req.body);
});For APIs, use the SameSite cookie attribute and custom headers:
// API CSRF protection with custom header
app.use('/api', (req, res, next) => {
const csrfHeader = req.headers['x-csrf-token'];
const csrfCookie = req.cookies['csrf-token'];
if (!csrfHeader || csrfHeader !== csrfCookie) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
});Cryptography
Encryption
import crypto from 'crypto';
// GOOD: AES-256-GCM encryption
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
function encrypt(plaintext: string, key: Buffer): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
// Return IV + Tag + Ciphertext
return iv.toString('hex') + tag.toString('hex') + encrypted;
}
function decrypt(ciphertext: string, key: Buffer): string {
const iv = Buffer.from(ciphertext.slice(0, IV_LENGTH * 2), 'hex');
const tag = Buffer.from(ciphertext.slice(IV_LENGTH * 2, (IV_LENGTH + TAG_LENGTH) * 2), 'hex');
const encrypted = ciphertext.slice((IV_LENGTH + TAG_LENGTH) * 2);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Key Derivation
import crypto from 'crypto';
// GOOD: PBKDF2 for password-based key derivation
function deriveKey(password: string, salt: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, 100000, 32, 'sha256', (err, key) => {
if (err) reject(err);
else resolve(key);
});
});
}Secure File Handling
import path from 'path';
import crypto from 'crypto';
// GOOD: Secure file upload handling
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
async function handleUpload(file: Express.Multer.File): Promise<string> {
// Validate file type
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('Invalid file type');
}
// Validate file size
if (file.size > MAX_SIZE) {
throw new ValidationError('File too large');
}
// Generate secure filename (prevent path traversal)
const ext = path.extname(file.originalname).toLowerCase();
const secureFilename = crypto.randomUUID() + ext;
// Store outside web root
const storagePath = path.join('/secure/uploads', secureFilename);
// Move file
await fs.rename(file.path, storagePath);
return secureFilename;
}Error Handling
// GOOD: Secure error handling
class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message);
}
}
// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// Log full error for debugging
logger.error('Error occurred', {
error: err.message,
stack: err.stack,
requestId: req.id,
path: req.path,
});
// Send sanitized error to client
if (err instanceof AppError && err.isOperational) {
res.status(err.statusCode).json({
error: err.message,
});
} else {
// Don't leak internal details
res.status(500).json({
error: 'An unexpected error occurred',
});
}
});Logging Security
// GOOD: Secure logging
const sensitiveFields = ['password', 'token', 'ssn', 'creditCard'];
function sanitizeForLogging(obj: object): object {
const sanitized = { ...obj };
for (const key of Object.keys(sanitized)) {
if (sensitiveFields.some(f => key.toLowerCase().includes(f))) {
sanitized[key] = '[REDACTED]';
} else if (typeof sanitized[key] === 'object') {
sanitized[key] = sanitizeForLogging(sanitized[key]);
}
}
return sanitized;
}
// Usage
logger.info('User login attempt', sanitizeForLogging({
email: user.email,
password: password, // Will be redacted
ipAddress: req.ip,
}));Security Headers
import helmet from 'helmet';
// GOOD: Comprehensive security headers
app.use(helmet());
// Or configure individually
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true }));
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.xssFilter());
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));Secure Dependencies
{
"scripts": {
"audit": "npm audit --audit-level=high",
"audit:fix": "npm audit fix",
"outdated": "npm outdated"
}
}Automate dependency scanning:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm audit --audit-level=highBest Practices Summary
Do
- Validate all input at trust boundaries
- Use parameterized queries for all database operations
- Implement proper authentication and authorization
- Encode output appropriately for the context
- Use established cryptographic libraries
- Keep dependencies updated and scanned
- Log security events (without sensitive data)
- Use HTTPS and secure headers everywhere
Don't
- Trust any user input
- Roll your own cryptography
- Store secrets in code
- Log sensitive information
- Use deprecated algorithms (MD5, SHA1 for security)
- Expose stack traces to users
- Disable security controls for convenience
- Ignore security warnings from tools
Related Resources
- Security Overview
- Threat Modeling
- Vulnerability Scanning
- OWASP Cheat Sheet Series
- Microsoft Secure Coding Guidelines
Compliance
This section fulfills ISO 13485 requirements for design outputs (7.3.4) and control of production (7.5.1), and ISO 27001 requirements for secure coding (A.8.28), application security requirements (A.8.26), and cryptography (A.8.24).
How is this guide?
Last updated on