Back to Articles

Chapter 17: Security Best Practices

July 27, 202510 min read
javascriptsecurityxsscsrfinput-validationweb-securitysanitization
Chapter 17: Security Best Practices

Chapter 17: Security Best Practices

XSS Prevention

// ❌ Dangerous - XSS vulnerability
function dangerousRender(userInput) {
    document.innerHTML = `<div>Hello ${userInput}</div>`;
    // If userInput is "<script>alert('XSS')</script>", it will execute!
}

// ✅ Safe - Escape user input
function safeRender(userInput) {
    const escaped = userInput
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#x27;');
    
    document.getElementById('content').innerHTML = `<div>Hello ${escaped}</div>`;
}

// ✅ Even safer - Use textContent
function safestRender(userInput) {
    const div = document.createElement('div');
    div.textContent = `Hello ${userInput}`;
    document.getElementById('content').appendChild(div);
}

// Sanitization library example (like DOMPurify)
function sanitizeHTML(html) {
    // This is a simplified version - use a real library in production
    const allowedTags = ['p', 'br', 'strong', 'em'];
    const allowedAttributes = ['class'];
    
    // Implementation would parse and clean HTML
    return html; // Simplified
}

CSRF Protection

// CSRF token handling
class CSRFProtection {
    constructor() {
        this.token = this.generateToken();
    }
    
    generateToken() {
        return Array.from(crypto.getRandomValues(new Uint8Array(32)))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }
    
    async makeSecureRequest(url, options = {}) {
        const headers = {
            'Content-Type': 'application/json',
            'X-CSRF-Token': this.token,
            ...options.headers
        };
        
        return fetch(url, {
            ...options,
            headers,
            credentials: 'same-origin' // Include cookies
        });
    }
}

const csrf = new CSRFProtection();

Input Validation

// Comprehensive input validation
class Validator {
    static email(email) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
    }
    
    static password(password) {
        // At least 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char
        const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
        return passwordRegex.test(password);
    }
    
    static sanitizeString(str, maxLength = 255) {
        if (typeof str !== 'string') return '';
        return str.trim().slice(0, maxLength);
    }
    
    static isValidJSON(str) {
        try {
            JSON.parse(str);
            return true;
        } catch {
            return false;
        }
    }
    
    static validateUserData(data) {
        const errors = [];
        
        if (!data.email || !this.email(data.email)) {
            errors.push('Invalid email address');
        }
        
        if (!data.password || !this.password(data.password)) {
            errors.push('Password must be at least 8 characters with mixed case, numbers, and symbols');
        }
        
        if (!data.name || data.name.trim().length < 2) {
            errors.push('Name must be at least 2 characters');
        }
        
        return {
            isValid: errors.length === 0,
            errors,
            sanitized: {
                email: this.sanitizeString(data.email),
                name: this.sanitizeString(data.name),
                // Don't sanitize password - hash it instead
            }
        };
    }
}

Advanced Security Patterns

Content Security Policy (CSP) Helper

class CSPHelper {
    static generateNonce() {
        return Array.from(crypto.getRandomValues(new Uint8Array(16)))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }
    
    static createSecureScript(code, nonce) {
        const script = document.createElement('script');
        script.nonce = nonce;
        script.textContent = code;
        return script;
    }
    
    static loadExternalScript(src, nonce) {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = src;
            script.nonce = nonce;
            script.onload = resolve;
            script.onerror = reject;
            
            // Verify source is from allowed domains
            const allowedDomains = ['cdn.example.com', 'trusted-scripts.com'];
            const url = new URL(src);
            
            if (!allowedDomains.includes(url.hostname)) {
                reject(new Error(`Script from unauthorized domain: ${url.hostname}`));
                return;
            }
            
            document.head.appendChild(script);
        });
    }
}

Secure Data Storage

class SecureStorage {
    constructor(key) {
        this.encryptionKey = key;
    }
    
    // Simple encryption (use a proper library in production)
    encrypt(data) {
        const jsonString = JSON.stringify(data);
        // This is a very basic encoding - use proper encryption
        return btoa(jsonString);
    }
    
    decrypt(encryptedData) {
        try {
            const jsonString = atob(encryptedData);
            return JSON.parse(jsonString);
        } catch {
            return null;
        }
    }
    
    setSecureItem(key, value) {
        const encrypted = this.encrypt(value);
        localStorage.setItem(key, encrypted);
    }
    
    getSecureItem(key) {
        const encrypted = localStorage.getItem(key);
        return encrypted ? this.decrypt(encrypted) : null;
    }
    
    removeSecureItem(key) {
        localStorage.removeItem(key);
    }
    
    // Clear sensitive data
    clearSensitiveData() {
        const sensitiveKeys = ['authToken', 'userCredentials', 'sessionData'];
        sensitiveKeys.forEach(key => this.removeSecureItem(key));
    }
}

// Usage
const secureStorage = new SecureStorage('your-encryption-key');
secureStorage.setSecureItem('userToken', { token: 'abc123', expires: Date.now() + 3600000 });

SQL Injection Prevention (for Node.js)

// Safe database query patterns
class DatabaseManager {
    constructor(dbConnection) {
        this.db = dbConnection;
    }
    
    // ❌ Dangerous - SQL injection vulnerability
    async dangerousQuery(userId) {
        const query = `SELECT * FROM users WHERE id = ${userId}`;
        return this.db.query(query); // Never do this!
    }
    
    // ✅ Safe - Parameterized queries
    async safeQuery(userId) {
        const query = 'SELECT * FROM users WHERE id = ?';
        return this.db.query(query, [userId]);
    }
    
    // ✅ Safe search with validation
    async searchUsers(searchTerm, limit = 10) {
        // Validate and sanitize input
        if (typeof searchTerm !== 'string' || searchTerm.length < 2) {
            throw new Error('Invalid search term');
        }
        
        const sanitizedTerm = searchTerm.replace(/[%_]/g, '\\$&'); // Escape SQL wildcards
        const query = 'SELECT id, name, email FROM users WHERE name LIKE ? LIMIT ?';
        
        return this.db.query(query, [`%${sanitizedTerm}%`, parseInt(limit)]);
    }
    
    // Input validation for complex queries
    async updateUser(userId, userData) {
        const allowedFields = ['name', 'email', 'age'];
        const updates = {};
        
        // Only allow whitelisted fields
        for (const [key, value] of Object.entries(userData)) {
            if (allowedFields.includes(key)) {
                updates[key] = value;
            }
        }
        
        if (Object.keys(updates).length === 0) {
            throw new Error('No valid fields to update');
        }
        
        const fields = Object.keys(updates).map(field => `${field} = ?`).join(', ');
        const values = [...Object.values(updates), userId];
        const query = `UPDATE users SET ${fields} WHERE id = ?`;
        
        return this.db.query(query, values);
    }
}

Authentication and Authorization

class AuthManager {
    constructor() {
        this.tokenKey = 'authToken';
        this.refreshTokenKey = 'refreshToken';
    }
    
    // Secure token storage
    setTokens(accessToken, refreshToken) {
        // Store with expiration
        const tokenData = {
            token: accessToken,
            expires: Date.now() + (15 * 60 * 1000) // 15 minutes
        };
        
        localStorage.setItem(this.tokenKey, JSON.stringify(tokenData));
        
        // Refresh token (longer lived, httpOnly cookie preferred)
        localStorage.setItem(this.refreshTokenKey, refreshToken);
    }
    
    getAccessToken() {
        const tokenData = localStorage.getItem(this.tokenKey);
        if (!tokenData) return null;
        
        try {
            const parsed = JSON.parse(tokenData);
            if (Date.now() > parsed.expires) {
                this.clearTokens();
                return null;
            }
            return parsed.token;
        } catch {
            return null;
        }
    }
    
    async refreshAccessToken() {
        const refreshToken = localStorage.getItem(this.refreshTokenKey);
        if (!refreshToken) {
            throw new Error('No refresh token available');
        }
        
        try {
            const response = await fetch('/api/auth/refresh', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ refreshToken })
            });
            
            if (!response.ok) {
                throw new Error('Token refresh failed');
            }
            
            const { accessToken, refreshToken: newRefreshToken } = await response.json();
            this.setTokens(accessToken, newRefreshToken);
            
            return accessToken;
        } catch (error) {
            this.clearTokens();
            throw error;
        }
    }
    
    clearTokens() {
        localStorage.removeItem(this.tokenKey);
        localStorage.removeItem(this.refreshTokenKey);
    }
    
    // Secure API request with automatic token refresh
    async secureRequest(url, options = {}) {
        let token = this.getAccessToken();
        
        if (!token) {
            try {
                token = await this.refreshAccessToken();
            } catch {
                throw new Error('Authentication required');
            }
        }
        
        const response = await fetch(url, {
            ...options,
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
        
        if (response.status === 401) {
            // Token might be expired, try refresh once
            try {
                token = await this.refreshAccessToken();
                return fetch(url, {
                    ...options,
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Content-Type': 'application/json',
                        ...options.headers
                    }
                });
            } catch {
                this.clearTokens();
                throw new Error('Authentication failed');
            }
        }
        
        return response;
    }
}

Rate Limiting and DDoS Protection

class RateLimiter {
    constructor(maxRequests = 100, windowMs = 60000) {
        this.maxRequests = maxRequests;
        this.windowMs = windowMs;
        this.requests = new Map();
    }
    
    checkLimit(identifier) {
        const now = Date.now();
        const windowStart = now - this.windowMs;
        
        // Clean old requests
        if (this.requests.has(identifier)) {
            const userRequests = this.requests.get(identifier)
                .filter(timestamp => timestamp > windowStart);
            this.requests.set(identifier, userRequests);
        }
        
        const userRequests = this.requests.get(identifier) || [];
        
        if (userRequests.length >= this.maxRequests) {
            return {
                allowed: false,
                retryAfter: Math.ceil((userRequests[0] - windowStart) / 1000)
            };
        }
        
        userRequests.push(now);
        this.requests.set(identifier, userRequests);
        
        return {
            allowed: true,
            remaining: this.maxRequests - userRequests.length
        };
    }
    
    // Middleware-style usage
    createMiddleware() {
        return (req, res, next) => {
            const identifier = req.ip || req.connection.remoteAddress;
            const result = this.checkLimit(identifier);
            
            if (!result.allowed) {
                res.status(429).json({
                    error: 'Too many requests',
                    retryAfter: result.retryAfter
                });
                return;
            }
            
            res.setHeader('X-RateLimit-Remaining', result.remaining);
            next();
        };
    }
}

Security Headers and Configuration

class SecurityHeaders {
    static getSecureHeaders() {
        return {
            // Prevent XSS attacks
            'X-XSS-Protection': '1; mode=block',
            
            // Prevent content-type sniffing
            'X-Content-Type-Options': 'nosniff',
            
            // Prevent clickjacking
            'X-Frame-Options': 'DENY',
            
            // HTTPS only
            'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
            
            // Content Security Policy
            'Content-Security-Policy': [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline'",
                "style-src 'self' 'unsafe-inline'",
                "img-src 'self' data: https:",
                "connect-src 'self'",
                "font-src 'self'",
                "object-src 'none'",
                "media-src 'self'",
                "frame-src 'none'"
            ].join('; '),
            
            // Referrer policy
            'Referrer-Policy': 'strict-origin-when-cross-origin'
        };
    }
    
    static applyToResponse(response) {
        const headers = this.getSecureHeaders();
        Object.entries(headers).forEach(([key, value]) => {
            response.setHeader(key, value);
        });
    }
}

Final Security Checklist

// Security audit checklist
const SecurityAudit = {
    checkInputValidation() {
        // Verify all user inputs are validated and sanitized
        return true;
    },
    
    checkAuthentication() {
        // Verify secure authentication implementation
        return true;
    },
    
    checkAuthorization() {
        // Verify proper access controls
        return true;
    },
    
    checkDataEncryption() {
        // Verify sensitive data is encrypted
        return true;
    },
    
    checkSecureHeaders() {
        // Verify security headers are implemented
        return true;
    },
    
    checkDependencies() {
        // Check for vulnerable dependencies
        // npm audit, yarn audit
        return true;
    },
    
    generateReport() {
        const checks = [
            { name: 'Input Validation', passed: this.checkInputValidation() },
            { name: 'Authentication', passed: this.checkAuthentication() },
            { name: 'Authorization', passed: this.checkAuthorization() },
            { name: 'Data Encryption', passed: this.checkDataEncryption() },
            { name: 'Security Headers', passed: this.checkSecureHeaders() },
            { name: 'Dependencies', passed: this.checkDependencies() }
        ];
        
        const passed = checks.filter(check => check.passed).length;
        const total = checks.length;
        
        return {
            score: `${passed}/${total}`,
            percentage: Math.round((passed / total) * 100),
            checks,
            recommendations: this.getRecommendations(checks)
        };
    },
    
    getRecommendations(checks) {
        const failed = checks.filter(check => !check.passed);
        return failed.map(check => `Improve ${check.name}`);
    }
};

Remember: Security is an ongoing process, not a one-time setup. Regularly audit your code, keep dependencies updated, and stay informed about new vulnerabilities and best practices.


Previous Chapter: Chapter 16: Testing and Debugging

Table of Contents: JavaScript Guide

🎉 Congratulations! You've completed the JavaScript Basics & Interview Guide! 🎉