Secure Coding Practices: Writing Defensive Code That Resists Attacks

18 november 2024 · CodeMatic Team

Secure Coding Practices

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.