Secure coding practices and defensive programming patterns for building security-first applications. Use when implementing authentication, handling user input, managing sensitive data, or conducting secure code reviews.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guidance for implementing security-first development patterns with defensive programming techniques and proactive threat mitigation strategies.
Principle: Never trust user input. Validate all data from untrusted sources before processing.
// VULNERABLE: Blocklist approach (incomplete, bypassable)
function validateUsername(username) {
const blocked = ['admin', 'root', 'system'];
return !blocked.includes(username);
}
// SECURE: Allowlist approach (explicit, comprehensive)
function validateUsername(username) {
// Only allow alphanumeric characters, underscores, and hyphens
const pattern = /^[a-zA-Z0-9_-]{3,20}$/;
return pattern.test(username);
}
// SECURE: Multi-layered validation
function validateEmail(email) {
// 1. Type check
if (typeof email !== 'string') return false;
// 2. Length validation
if (email.length > 254) return false;
// 3. Format validation
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) return false;
// 4. Domain validation (optional)
const allowedDomains = ['example.com', 'trusted.org'];
const domain = email.split('@')[1];
if (!allowedDomains.includes(domain)) return false;
return true;
}
// NEVER trust client-side validation alone
const express = require('express');
const { body, validationResult } = require('express-validator');
app.post('/api/register',
// Define validation rules
[
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Invalid email format'),
body('password')
.isLength({ min: 12 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
.withMessage('Password must be 12+ chars with upper, lower, number, special char'),
body('age')
.isInt({ min: 18, max: 120 })
.withMessage('Age must be between 18 and 120'),
],
(req, res) => {
// Check validation results
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed with validated data
createUser(req.body);
}
);
// VULNERABLE: Loose comparison
if (req.body.isAdmin == true) {
grantAdminAccess();
}
// Attack: isAdmin = "true" or isAdmin = 1
// SECURE: Strict type checking
if (req.body.isAdmin === true && typeof req.body.isAdmin === 'boolean') {
grantAdminAccess();
}
// SECURE: Schema validation with libraries
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(120).required(),
isAdmin: Joi.boolean().required()
});
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
Principle: Encode all output based on the context where it will be used.
// VULNERABLE: Direct output without encoding
app.get('/welcome', (req, res) => {
const name = req.query.name;
res.send(`<h1>Welcome ${name}!</h1>`);
// XSS: /welcome?name=<script>alert('XSS')</script>
});
// SECURE: HTML entity encoding
const escapeHtml = (str) => {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, (char) => map[char]);
};
app.get('/welcome', (req, res) => {
const name = escapeHtml(req.query.name);
res.send(`<h1>Welcome ${name}!</h1>`);
});
// BETTER: Use templating engines with auto-escaping
const handlebars = require('handlebars');
const template = handlebars.compile('<h1>Welcome {{name}}!</h1>');
app.get('/welcome', (req, res) => {
res.send(template({ name: req.query.name }));
});
// VULNERABLE: Injecting into JavaScript
res.send(`
<script>
var username = "${req.query.username}";
</script>
`);
// Attack: username="; alert('XSS'); //
// SECURE: JSON encoding for JavaScript context
res.send(`
<script>
var username = ${JSON.stringify(req.query.username)};
</script>
`);
// VULNERABLE: Unencoded URL parameter
const redirectUrl = `/profile?user=${req.query.user}`;
// Attack: user=admin&admin=true
// SECURE: Proper URL encoding
const redirectUrl = `/profile?user=${encodeURIComponent(req.query.user)}`;
// Implement CSP headers to prevent XSS
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-{random}'"], // Use nonces for inline scripts
styleSrc: ["'self'", "https://trusted-cdn.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
upgradeInsecureRequests: []
}
}));
// SECURE: Password hashing with bcrypt
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12; // Adjusts computational cost
async function registerUser(username, password) {
// Validate password strength
if (password.length < 12) {
throw new Error('Password must be at least 12 characters');
}
// Hash password
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// Store username and hash (NEVER store plaintext)
await db.users.create({ username, passwordHash });
}
async function authenticateUser(username, password) {
const user = await db.users.findOne({ username });
if (!user) {
// Use constant-time comparison to prevent timing attacks
await bcrypt.compare(password, '$2b$12$dummy.hash.to.prevent.timing.attack');
throw new Error('Invalid credentials');
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new Error('Invalid credentials');
}
return user;
}
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
// Initialize Redis client
const redisClient = createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // Strong random secret
name: 'sessionId', // Non-default name
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 1800000, // 30 minutes
sameSite: 'strict', // CSRF protection
domain: 'example.com',
path: '/'
},
rolling: true, // Reset expiration on activity
genid: () => {
// Cryptographically secure session ID
return require('crypto').randomBytes(32).toString('hex');
}
}));
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// SECURE: JWT implementation
const JWT_SECRET = process.env.JWT_SECRET; // Strong random secret
const JWT_EXPIRY = '15m'; // Short expiration
const REFRESH_TOKEN_EXPIRY = '7d';
function generateTokens(userId) {
// Access token (short-lived)
const accessToken = jwt.sign(
{ userId, type: 'access' },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY, algorithm: 'HS256' }
);
// Refresh token (long-lived, stored securely)
const refreshToken = jwt.sign(
{ userId, type: 'refresh', jti: crypto.randomBytes(16).toString('hex') },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY, algorithm: 'HS256' }
);
return { accessToken, refreshToken };
}
function verifyAccessToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid token' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
if (decoded.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.user = { userId: decoded.userId };
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Enable TOTP-based MFA
async function enableMFA(userId) {
const secret = speakeasy.generateSecret({
name: `MyApp (${userId})`,
length: 32
});
// Store secret.base32 encrypted in database
await db.users.update(userId, {
mfaSecret: encrypt(secret.base32),
mfaEnabled: false // Activated after verification
});
// Generate QR code for user to scan
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
return { secret: secret.base32, qrCode: qrCodeUrl };
}
function verifyMFAToken(secret, token) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2 // Allow 2 time steps for clock drift
});
}
const crypto = require('crypto');
// SECURE: Symmetric encryption with authenticated encryption
class SecureEncryption {
constructor() {
// 256-bit key from environment
this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
if (this.key.length !== 32) {
throw new Error('Encryption key must be 32 bytes (256 bits)');
}
this.algorithm = 'aes-256-gcm';
}
encrypt(plaintext) {
// Generate random IV (96 bits for GCM)
const iv = crypto.randomBytes(12);
// Create cipher
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
// Encrypt data
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);
// Get authentication tag
const authTag = cipher.getAuthTag();
// Return IV + authTag + ciphertext (all needed for decryption)
return {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
ciphertext: encrypted.toString('hex')
};
}
decrypt(encryptedData) {
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const ciphertext = Buffer.from(encryptedData.ciphertext, 'hex');
// Create decipher
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
decipher.setAuthTag(authTag);
// Decrypt data
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
return decrypted.toString('utf8');
}
}
// Usage
const encryption = new SecureEncryption();
const encrypted = encryption.encrypt('sensitive data');
const decrypted = encryption.decrypt(encrypted);
// NEVER hardcode keys in source code
// BAD: const SECRET_KEY = 'hardcoded-secret-123';
// GOOD: Use environment variables
require('dotenv').config();
const SECRET_KEY = process.env.SECRET_KEY;
// BETTER: Use dedicated key management service
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
async function getSecret(secretName) {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
return response.SecretString;
}
// Key rotation strategy
async function rotateEncryptionKey(oldKey, newKey) {
const records = await db.sensitiveData.findAll();
for (const record of records) {
// Decrypt with old key
const decrypted = decryptWithKey(record.data, oldKey);
// Re-encrypt with new key
const encrypted = encryptWithKey(decrypted, newKey);
// Update record
await db.sensitiveData.update(record.id, { data: encrypted });
}
}
const crypto = require('crypto');
// VULNERABLE: Predictable randomness
Math.random(); // NEVER use for security
// SECURE: Cryptographically secure randomness
const token = crypto.randomBytes(32).toString('hex'); // 256-bit token
const resetToken = crypto.randomBytes(20).toString('hex'); // Password reset
const sessionId = crypto.randomBytes(16).toString('base64url'); // Session IDs
// SECURE: Random integer in range
function getSecureRandomInt(min, max) {
const range = max - min;
const bytesNeeded = Math.ceil(Math.log2(range) / 8);
const maxValid = Math.floor(256 ** bytesNeeded / range) * range;
let randomValue;
do {
randomValue = crypto.randomBytes(bytesNeeded).readUIntBE(0, bytesNeeded);
} while (randomValue >= maxValid);
return min + (randomValue % range);
}
# Regular vulnerability scanning
npm audit
npm audit fix
# Use production dependencies only
npm audit --production
# Advanced scanning with Snyk
npx snyk test
npx snyk monitor
# Check for outdated packages
npm outdated
# Automated dependency updates with security focus
npx npm-check-updates -u
// package.json: Lock dependency versions
{
"dependencies": {
"express": "4.18.2", // Exact version, not "^4.18.2"
"jsonwebtoken": "9.0.0"
}
}
// Use package-lock.json or yarn.lock
// Commit lock files to version control
// Verify package integrity
npm ci # Use in CI/CD instead of npm install
<!-- VULNERABLE: Unverified CDN resource -->
<script src="https://cdn.example.com/library.js"></script>
<!-- SECURE: SRI hash verification -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
// .npmrc: Use private registry for sensitive packages
registry=https://registry.npmjs.org/
@mycompany:registry=https://npm.internal.company.com/
//npm.internal.company.com/:_authToken=${NPM_TOKEN}
// Enable audit for private packages
audit=true
audit-level=moderate
// VULNERABLE: Exposing stack traces to users
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack, // Leaks internal paths, dependencies
query: req.query // Leaks user input
});
});
// SECURE: Generic error messages with internal logging
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: 'combined.log' })
]
});
app.use((err, req, res, next) => {
// Log full error details internally
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userId: req.user?.id,
requestId: req.id,
timestamp: new Date().toISOString()
});
// Return generic message to user
res.status(500).json({
error: 'An internal error occurred',
requestId: req.id // For support inquiries
});
});
// NEVER log sensitive data
logger.info('User login', {
username: user.username,
password: user.password, // NEVER!
creditCard: user.card // NEVER!
});
// SECURE: Sanitize before logging
function sanitizeForLogging(obj) {
const sensitiveFields = ['password', 'creditCard', 'ssn', 'token', 'secret'];
const sanitized = { ...obj };
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
logger.info('User login', sanitizeForLogging({
username: user.username,
password: user.password,
ip: req.ip
}));
// Logs: { username: 'john', password: '[REDACTED]', ip: '192.168.1.1' }
// Log security events
logger.warn('Failed login attempt', {
username: req.body.username,
ip: req.ip,
timestamp: new Date().toISOString()
});
logger.info('Authorization failure', {
userId: req.user.id,
resource: req.path,
action: req.method,
timestamp: new Date().toISOString()
});
// Use correlation IDs to track requests
const { v4: uuidv4 } = require('uuid');
app.use((req, res, next) => {
req.id = uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Log with consistent structure
logger.info('Request received', {
requestId: req.id,
method: req.method,
path: req.path,
userId: req.user?.id,
ip: req.ip,
userAgent: req.headers['user-agent']
});
const helmet = require('helmet');
// Apply secure defaults
app.use(helmet({
// Prevent clickjacking
frameguard: { action: 'deny' },
// Enforce HTTPS
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// Prevent MIME sniffing
noSniff: true,
// XSS protection
xssFilter: true,
// Referrer policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Permissions policy
permissionsPolicy: {
features: {
geolocation: ["'self'"],
camera: ["'none'"],
microphone: ["'none'"]
}
}
}));
// CORS configuration
const cors = require('cors');
app.use(cors({
origin: ['https://trusted-domain.com'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
}));
// Database: Create limited-privilege users
// NEVER use root/admin for application connections
// PostgreSQL example:
/*
CREATE USER app_user WITH PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON products TO app_user;
-- Do NOT grant DELETE, DROP, or admin privileges
*/
// Application: Role-based access control
const roles = {
USER: ['read:profile', 'update:profile'],
MODERATOR: ['read:profile', 'update:profile', 'delete:comments'],
ADMIN: ['read:profile', 'update:profile', 'delete:comments', 'manage:users']
};
function authorize(requiredPermission) {
return (req, res, next) => {
const userRole = req.user.role;
const permissions = roles[userRole] || [];
if (!permissions.includes(requiredPermission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.delete('/api/users/:id', authenticate, authorize('manage:users'), (req, res) => {
deleteUser(req.params.id);
});
// Environment-based configuration
const config = {
development: {
database: {
host: 'localhost',
ssl: false,
debug: true
},
logging: 'debug'
},
production: {
database: {
host: process.env.DB_HOST,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/path/to/ca-cert.pem')
},
debug: false
},
logging: 'error'
}
};
const env = process.env.NODE_ENV || 'development';
const activeConfig = config[env];
// Fail securely
if (env === 'production' && !activeConfig.database.ssl) {
throw new Error('SSL required for production database');
}
Input Validation:
Output Encoding:
Authentication & Authorization:
Cryptography:
Dependencies:
Error Handling & Logging:
Configuration:
High-Risk Patterns to Check:
OWASP Resources:
CWE (Common Weakness Enumeration):
Standards & Guidelines:
Tools: