Security Best Practices
Overview
Comprehensive security guidelines for building secure applications, protecting against common vulnerabilities, and following defense-in-depth principles.
When to Use This Skill
- •Implementing authentication/authorization
- •Handling sensitive data (PII, credentials, tokens)
- •Working with user input
- •Building APIs and endpoints
- •Reviewing code for security issues
- •Implementing security controls
OWASP Top 10 Security Risks
1. Broken Access Control
Risk: Users can access resources they shouldn't
Prevention:
// ✅ Good - Check authorization
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
const requestedUserId = req.params.id;
// Verify user can only access their own profile
if (req.user.id !== requestedUserId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const profile = await getUserProfile(requestedUserId);
res.json(profile);
});
// ❌ Bad - No authorization check
app.get('/api/users/:id/profile', async (req, res) => {
const profile = await getUserProfile(req.params.id);
res.json(profile); // Anyone can view any profile!
});
2. Cryptographic Failures
Risk: Sensitive data exposed due to weak/missing encryption
Prevention:
// ✅ Good - Hash passwords with bcrypt/argon2
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return await bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// ❌ Bad - Storing plain text passwords
user.password = password; // NEVER DO THIS
// ❌ Bad - Weak hashing (MD5, SHA1)
user.password = crypto.createHash('md5').update(password).digest('hex');
Encrypt Sensitive Data at Rest:
const crypto = require('crypto');
// Use environment variables for keys
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // Must be 32 bytes
const IV_LENGTH = 16;
function encrypt(text) {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
function decrypt(text) {
const parts = text.split(':');
const iv = Buffer.from(parts.shift(), 'hex');
const encrypted = Buffer.from(parts.join(':'), 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
3. Injection Attacks
Risk: Malicious code injected into queries/commands
SQL Injection Prevention:
// ✅ Good - Parameterized queries
const user = await db.query(
'SELECT * FROM users WHERE email = $1 AND password = $2',
[email, hashedPassword]
);
// ❌ Bad - String concatenation
const user = await db.query(
`SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`
);
// Attacker can use: ' OR '1'='1
NoSQL Injection Prevention:
// ✅ Good - Validate and sanitize
const email = String(req.body.email).trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
const user = await User.findOne({ email });
// ❌ Bad - Direct object injection
const user = await User.findOne(req.body);
// Attacker can send: { $ne: null } to bypass authentication
Command Injection Prevention:
// ✅ Good - Use libraries, avoid shell execution
const sharp = require('sharp');
await sharp(inputPath).resize(800, 600).toFile(outputPath);
// ❌ Bad - Executing shell commands with user input
const { exec } = require('child_process');
exec(`convert ${userInput} -resize 800x600 output.jpg`);
// Attacker can inject: ; rm -rf /
4. Insecure Design
Risk: Fundamental flaws in application architecture
Prevention:
- •Implement principle of least privilege
- •Use defense in depth (multiple layers of security)
- •Fail securely (secure defaults)
- •Separate duties (different roles/permissions)
- •Follow secure design patterns
5. Security Misconfiguration
Risk: Insecure default configurations, unnecessary features enabled
Prevention:
// ✅ Good - Secure Express configuration
const express = require('express');
const helmet = require('helmet');
const app = express();
// Security headers
app.use(helmet());
// Disable X-Powered-By header
app.disable('x-powered-by');
// CORS configuration
app.use(cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true,
maxAge: 86400
}));
// Rate limiting
const rateLimit = require('express-rate-limit');
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
}));
// ❌ Bad - Insecure configuration
app.use(cors({ origin: '*' })); // Allow all origins
app.set('trust proxy', true); // Without understanding implications
6. Vulnerable and Outdated Components
Risk: Using libraries with known vulnerabilities
Prevention:
# Regular dependency audits npm audit npm audit fix # Use automated tools npm install -g npm-check-updates ncu -u # Check for known vulnerabilities npm install -g snyk snyk test
7. Identification and Authentication Failures
Risk: Weak authentication mechanisms
Prevention:
// ✅ Good - Secure session configuration
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 3600000, // 1 hour
sameSite: 'strict' // CSRF protection
}
}));
// ✅ Good - JWT with proper expiration
const jwt = require('jsonwebtoken');
function generateToken(userId) {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'your-app', audience: 'your-app-users' }
);
}
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users'
});
} catch (error) {
throw new Error('Invalid token');
}
}
// ❌ Bad - Weak session configuration
app.use(session({
secret: 'keyboard cat', // Hardcoded secret
cookie: { secure: false } // Not HTTPS only
}));
Multi-Factor Authentication:
const speakeasy = require('speakeasy');
// Generate secret for user
function generateMFASecret() {
return speakeasy.generateSecret({
name: 'YourApp',
length: 32
});
}
// Verify MFA token
function verifyMFAToken(secret, token) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps tolerance
});
}
8. Software and Data Integrity Failures
Risk: Code/data modified without verification
Prevention:
// ✅ Good - Verify package integrity
// package-lock.json maintains integrity hashes
// ✅ Good - Verify file uploads
const crypto = require('crypto');
function calculateHash(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
function verifyIntegrity(data, expectedHash) {
const actualHash = calculateHash(data);
if (actualHash !== expectedHash) {
throw new Error('Data integrity check failed');
}
}
9. Security Logging and Monitoring Failures
Risk: Attacks go undetected
Prevention:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'security.log', level: 'warn' })
]
});
// Log security events
function logSecurityEvent(event, details) {
logger.warn('SECURITY_EVENT', {
event,
...details,
timestamp: new Date().toISOString(),
ip: details.ip,
userId: details.userId
});
}
// Examples
logSecurityEvent('FAILED_LOGIN', { email, ip: req.ip, attempts: 5 });
logSecurityEvent('UNAUTHORIZED_ACCESS', { userId, resource: '/admin', ip: req.ip });
logSecurityEvent('SUSPICIOUS_ACTIVITY', { userId, action: 'bulk_download', ip: req.ip });
// ⚠️ Never log sensitive data
// ❌ Don't log: passwords, tokens, credit cards, SSN, etc.
10. Server-Side Request Forgery (SSRF)
Risk: Attacker makes server perform unintended requests
Prevention:
// ✅ Good - Whitelist allowed domains
const allowedDomains = ['api.example.com', 'cdn.example.com'];
function isAllowedURL(url) {
try {
const parsed = new URL(url);
return allowedDomains.includes(parsed.hostname);
} catch {
return false;
}
}
app.post('/fetch-content', async (req, res) => {
const { url } = req.body;
if (!isAllowedURL(url)) {
return res.status(400).json({ error: 'Invalid URL' });
}
const response = await fetch(url);
res.json(await response.json());
});
// ❌ Bad - No validation
app.post('/fetch-content', async (req, res) => {
const response = await fetch(req.body.url);
res.json(await response.json());
// Attacker could access internal services: http://localhost:8080/admin
});
Input Validation & Sanitization
Validate All Inputs
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(128).required(),
age: Joi.number().integer().min(13).max(120),
website: Joi.string().uri()
});
app.post('/register', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// Use validated 'value' instead of req.body
await createUser(value);
res.status(201).json({ message: 'User created' });
});
Sanitize HTML Output (XSS Prevention)
const DOMPurify = require('isomorphic-dompurify');
// ✅ Good - Sanitize user content
function sanitizeHTML(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
}
// In React/Vue - use proper escaping
// React automatically escapes by default
<div>{userInput}</div> // Safe
// ❌ Dangerous
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // XSS risk
Secrets Management
Never Commit Secrets
# .gitignore .env .env.local secrets/ *.pem *.key # Use environment variables DATABASE_URL=postgresql://user:pass@localhost:5432/db JWT_SECRET=your-secret-key-here API_KEY=your-api-key-here
Use Secret Management Services
// AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getSecret(secretName) {
const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise();
return JSON.parse(data.SecretString);
}
// HashiCorp Vault
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
async function getVaultSecret(path) {
const result = await vault.read(path);
return result.data;
}
API Security
Rate Limiting
const rateLimit = require('express-rate-limit');
// General rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: 'Too many requests from this IP'
});
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.use('/api/', limiter);
app.use('/api/auth/', authLimiter);
CSRF Protection
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Process form
});
API Key Authentication
async function validateAPIKey(req, res, next) {
const apiKey = req.header('X-API-Key');
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// Hash the API key before lookup
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const client = await findClientByHashedKey(hashedKey);
if (!client || !client.isActive) {
logSecurityEvent('INVALID_API_KEY', { key: apiKey.substring(0, 8), ip: req.ip });
return res.status(401).json({ error: 'Invalid API key' });
}
req.client = client;
next();
}
Security Checklist
Authentication
- • Passwords hashed with bcrypt/argon2 (min 12 rounds)
- • Account lockout after failed attempts
- • Password complexity requirements enforced
- • Secure password reset flow (time-limited tokens)
- • MFA available for sensitive operations
Authorization
- • Role-based access control (RBAC) implemented
- • Principle of least privilege applied
- • Authorization checks on every request
- • No direct object references (use UUIDs)
Data Protection
- • Sensitive data encrypted at rest
- • HTTPS enforced (TLS 1.3+)
- • Secure cookie flags (httpOnly, secure, sameSite)
- • No sensitive data in logs
- • PII/PHI properly handled per regulations
Input/Output
- • All inputs validated and sanitized
- • Output encoded/escaped properly
- • File uploads restricted (type, size)
- • SQL queries parameterized
- • No eval() or dynamic code execution
Infrastructure
- • Security headers configured (Helmet.js)
- • CORS properly configured
- • Rate limiting implemented
- • Dependencies regularly updated
- • Security scanning in CI/CD
Monitoring
- • Security events logged
- • Failed login attempts tracked
- • Anomaly detection configured
- • Incident response plan exists
- • Regular security audits scheduled