JavaScript Security

45 min read Intermediate

Protect your applications from common vulnerabilities including XSS, CSRF, injection attacks, and learn secure coding practices.

Cross-Site Scripting (XSS)

XSS occurs when attackers inject malicious scripts into web pages viewed by other users.

JavaScript
// VULNERABLE: Using innerHTML with user input
const userInput = "<script>alert('XSS')</script>";
element.innerHTML = userInput; // Executes the script!

// VULNERABLE: Using document.write
document.write(userInput); // Dangerous!

// SAFE: Use textContent instead
element.textContent = userInput; // Displays as text

// SAFE: Escape HTML entities
function escapeHTML(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
}

// Or manual escaping
function escapeHTMLManual(str) {
    return str
        .replace(/&/g, "&")
        .replace(//g, ">")
        .replace(/"/g, """)
        .replace(/'/g, "'");
}

// SAFE: Create elements programmatically
const link = document.createElement("a");
link.href = sanitizeURL(userProvidedURL);
link.textContent = userProvidedText;
container.appendChild(link);

// Sanitize URLs
function sanitizeURL(url) {
    try {
        const parsed = new URL(url);
        // Only allow http and https
        if (!["http:", "https:"].includes(parsed.protocol)) {
            return "";
        }
        return url;
    } catch {
        return "";
    }
}

// DANGEROUS: javascript: URLs
// <a href="javascript:alert('XSS')">Click</a>
// Always validate URL protocols!
Never Trust User Input

Any data from users (form inputs, URL parameters, cookies) should be treated as potentially malicious and properly sanitized before use.

Cross-Site Request Forgery (CSRF)

CSRF tricks users into performing unwanted actions on sites where they're authenticated.

JavaScript
// CSRF Protection: Include token in requests

// 1. Get CSRF token from meta tag or cookie
function getCSRFToken() {
    // From meta tag
    const meta = document.querySelector('meta[name="csrf-token"]');
    if (meta) return meta.content;
    
    // Or from cookie
    const match = document.cookie.match(/csrf_token=([^;]+)/);
    return match ? match[1] : null;
}

// 2. Include token in fetch requests
async function secureRequest(url, options = {}) {
    const token = getCSRFToken();
    
    return fetch(url, {
        ...options,
        headers: {
            ...options.headers,
            "X-CSRF-Token": token,
            "Content-Type": "application/json"
        },
        credentials: "same-origin" // Include cookies
    });
}

// 3. Verify token server-side
// Server checks X-CSRF-Token header matches session token


// SameSite Cookie attribute (set by server)
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

// SameSite options:
// Strict - Cookie never sent cross-site
// Lax - Sent with top-level navigations
// None - Sent always (requires Secure)


// Avoid state-changing GET requests
// BAD: GET /api/delete-account
// GOOD: POST /api/delete-account with CSRF token

Injection Attacks

Prevent code injection through proper input handling.

JavaScript
// NEVER use eval with user input
// DANGEROUS:
eval(userInput); // Can execute arbitrary code!

// DANGEROUS: new Function with user input
new Function(userInput)();

// DANGEROUS: setTimeout/setInterval with strings
setTimeout(userInput, 1000); // Can execute code!
// SAFE: Use function reference
setTimeout(() => safeFunction(), 1000);


// JSON Parsing - use JSON.parse, not eval
// DANGEROUS:
const data = eval("(" + jsonString + ")");

// SAFE:
try {
    const data = JSON.parse(jsonString);
} catch (e) {
    console.error("Invalid JSON");
}


// Template literals - be careful with user input
// DANGEROUS if tag allows code execution:
html`
${userInput}
`; // Depends on html implementation // SAFE: Escape first html`
${escapeHTML(userInput)}
`; // RegExp - escape user input function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const userSearch = "hello (world)"; const regex = new RegExp(escapeRegExp(userSearch), "i"); // SQL Injection (for Node.js with databases) // NEVER concatenate user input into queries // BAD: `SELECT * FROM users WHERE id = ${userId}` // GOOD: Use parameterized queries db.query("SELECT * FROM users WHERE id = ?", [userId]);

Data Security

Protect sensitive data in transit and storage.

JavaScript
// ===== Sensitive Data Storage =====

// NEVER store sensitive data in localStorage
// localStorage is accessible to any script on the page
localStorage.setItem("password", "secret"); // BAD!

// Use httpOnly cookies for auth tokens (set by server)
// These can't be accessed by JavaScript

// For client-side encryption, use Web Crypto API
async function generateKey() {
    return crypto.subtle.generateKey(
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
    );
}

async function encrypt(key, data) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(data);
    
    const ciphertext = await crypto.subtle.encrypt(
        { name: "AES-GCM", iv },
        key,
        encoded
    );
    
    return { iv, ciphertext };
}


// ===== Secure Random Values =====
// NEVER use Math.random() for security
const insecure = Math.random(); // Predictable!

// SAFE: Use crypto API
const secureBytes = crypto.getRandomValues(new Uint8Array(32));

// Generate secure token
function generateToken(length = 32) {
    const bytes = crypto.getRandomValues(new Uint8Array(length));
    return Array.from(bytes)
        .map(b => b.toString(16).padStart(2, "0"))
        .join("");
}


// ===== Password Handling =====
// Never store passwords in plain text
// Always hash on the server with bcrypt/argon2

// Client-side: Clear sensitive data after use
function handleLogin(password) {
    sendToServer(password);
    password = null; // Clear reference
}


// ===== HTTPS =====
// Always use HTTPS in production
if (location.protocol !== "https:" && location.hostname !== "localhost") {
    location.replace("https://" + location.host + location.pathname);
}

Content Security Policy

CSP helps prevent XSS by controlling which resources can be loaded.

HTML
<!-- CSP via meta tag -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' https://cdn.example.com;
               style-src 'self' 'unsafe-inline';
               img-src 'self' data: https:;
               connect-src 'self' https://api.example.com;">

<!-- Or via HTTP header (preferred) -->
<!-- Content-Security-Policy: default-src 'self' -->
JavaScript
// CSP Directives:
// default-src - Fallback for other directives
// script-src  - Valid sources for JavaScript
// style-src   - Valid sources for CSS
// img-src     - Valid sources for images
// connect-src - Valid sources for fetch/XHR/WebSocket
// font-src    - Valid sources for fonts
// frame-src   - Valid sources for iframes

// CSP values:
// 'self'          - Same origin only
// 'none'          - Block all
// 'unsafe-inline' - Allow inline scripts/styles (avoid!)
// 'unsafe-eval'   - Allow eval() (avoid!)
// https:          - Any HTTPS URL
// data:           - Data URIs
// 'nonce-abc123'  - Scripts with matching nonce
// 'sha256-...'    - Scripts matching hash

// Using nonces for inline scripts
// Server generates random nonce per request
// <script nonce="abc123">...</script>
// CSP: script-src 'nonce-abc123'

// Report CSP violations
// Content-Security-Policy-Report-Only: ...
// report-uri /csp-violation-report

Security Best Practices

General guidelines for writing secure JavaScript code.

JavaScript
// 1. Use strict mode
"use strict";

// 2. Validate all inputs
function processUserData(data) {
    if (!data || typeof data !== "object") {
        throw new Error("Invalid data");
    }
    
    if (!isValidEmail(data.email)) {
        throw new Error("Invalid email");
    }
    
    // Process validated data
}

// 3. Use Object.freeze for constants
const CONFIG = Object.freeze({
    API_URL: "https://api.example.com",
    TIMEOUT: 5000
});

// 4. Avoid global variables
(function() {
    const privateVar = "secret";
    // ...
})();

// 5. Use subresource integrity for CDN scripts
// <script src="https://cdn.example.com/lib.js"
//         integrity="sha384-abc123..."
//         crossorigin="anonymous"></script>

// 6. Set secure cookie options (server-side)
// Set-Cookie: session=token; Secure; HttpOnly; SameSite=Strict

// 7. Implement rate limiting
// Prevent brute force attacks on login

// 8. Log security events
function logSecurityEvent(type, details) {
    console.warn(`[Security] ${type}:`, details);
    // Send to monitoring service
}

// 9. Keep dependencies updated
// npm audit
// npm update

// 10. Use security headers (server-side)
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 1; mode=block
// Strict-Transport-Security: max-age=31536000

Authentication Best Practices

Secure authentication protects user accounts from unauthorized access. Follow these patterns to implement robust authentication.

JavaScript - Secure Authentication
// JWT handling
class AuthManager {
    #accessToken = null;
    #refreshToken = null;
    
    setTokens(access, refresh) {
        this.#accessToken = access;
        this.#refreshToken = refresh;
        
        // Store refresh token securely (HttpOnly cookie preferred)
        // Never store in localStorage for sensitive apps
    }
    
    async getValidToken() {
        if (!this.#accessToken) return null;
        
        // Check if token is expired
        const payload = this.decodeToken(this.#accessToken);
        if (payload.exp * 1000 < Date.now()) {
            return await this.refreshAccessToken();
        }
        
        return this.#accessToken;
    }
    
    decodeToken(token) {
        // Decode payload (don't trust without server verification)
        const payload = token.split('.')[1];
        return JSON.parse(atob(payload));
    }
    
    async refreshAccessToken() {
        try {
            const response = await fetch('/api/refresh', {
                method: 'POST',
                credentials: 'include' // Include HttpOnly cookie
            });
            
            if (!response.ok) {
                this.logout();
                return null;
            }
            
            const { accessToken } = await response.json();
            this.#accessToken = accessToken;
            return accessToken;
        } catch {
            this.logout();
            return null;
        }
    }
    
    logout() {
        this.#accessToken = null;
        this.#refreshToken = null;
        // Clear cookies server-side
        fetch('/api/logout', { method: 'POST', credentials: 'include' });
    }
}

// Password strength validation
function validatePassword(password) {
    const checks = {
        length: password.length >= 12,
        uppercase: /[A-Z]/.test(password),
        lowercase: /[a-z]/.test(password),
        number: /[0-9]/.test(password),
        special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
    };
    
    const passed = Object.values(checks).filter(Boolean).length;
    
    return {
        valid: passed >= 4 && checks.length,
        score: passed,
        checks
    };
}

// Brute force protection (client-side delay)
class LoginThrottle {
    constructor() {
        this.attempts = 0;
        this.lockoutUntil = 0;
    }
    
    canAttempt() {
        if (Date.now() < this.lockoutUntil) {
            const remaining = Math.ceil((this.lockoutUntil - Date.now()) / 1000);
            return { allowed: false, waitSeconds: remaining };
        }
        return { allowed: true };
    }
    
    recordFailure() {
        this.attempts++;
        if (this.attempts >= 5) {
            // Exponential backoff: 30s, 60s, 120s...
            const lockoutSeconds = Math.min(30 * Math.pow(2, this.attempts - 5), 3600);
            this.lockoutUntil = Date.now() + lockoutSeconds * 1000;
        }
    }
    
    recordSuccess() {
        this.attempts = 0;
        this.lockoutUntil = 0;
    }
}

Input Validation & Sanitization

Validate and sanitize all user input to prevent injection attacks and data corruption.

JavaScript - Input Validation
// Schema-based validation
class Validator {
    static string(value, { minLength = 0, maxLength = Infinity, pattern } = {}) {
        if (typeof value !== 'string') return { valid: false, error: 'Must be a string' };
        if (value.length < minLength) return { valid: false, error: `Minimum ${minLength} characters` };
        if (value.length > maxLength) return { valid: false, error: `Maximum ${maxLength} characters` };
        if (pattern && !pattern.test(value)) return { valid: false, error: 'Invalid format' };
        return { valid: true };
    }
    
    static email(value) {
        const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!pattern.test(value)) return { valid: false, error: 'Invalid email' };
        return { valid: true };
    }
    
    static number(value, { min = -Infinity, max = Infinity } = {}) {
        const num = Number(value);
        if (isNaN(num)) return { valid: false, error: 'Must be a number' };
        if (num < min) return { valid: false, error: `Minimum value is ${min}` };
        if (num > max) return { valid: false, error: `Maximum value is ${max}` };
        return { valid: true };
    }
    
    static validate(data, schema) {
        const errors = {};
        
        for (const [field, rules] of Object.entries(schema)) {
            const value = data[field];
            const result = rules(value);
            
            if (!result.valid) {
                errors[field] = result.error;
            }
        }
        
        return {
            valid: Object.keys(errors).length === 0,
            errors
        };
    }
}

// Usage
const userSchema = {
    username: v => Validator.string(v, { minLength: 3, maxLength: 20, pattern: /^[a-zA-Z0-9_]+$/ }),
    email: v => Validator.email(v),
    age: v => Validator.number(v, { min: 13, max: 120 })
};

const result = Validator.validate(formData, userSchema);

// HTML sanitization
function sanitizeHTML(str) {
    const temp = document.createElement('div');
    temp.textContent = str;
    return temp.innerHTML;
}

// More robust sanitization with DOMPurify
// import DOMPurify from 'dompurify';
// const clean = DOMPurify.sanitize(dirty);

// URL validation
function isValidURL(string) {
    try {
        const url = new URL(string);
        return ['http:', 'https:'].includes(url.protocol);
    } catch {
        return false;
    }
}

// Safe redirect
function safeRedirect(url, allowedDomains = []) {
    try {
        const parsed = new URL(url, window.location.origin);
        
        // Only allow same-origin or whitelisted domains
        if (parsed.origin === window.location.origin) {
            window.location.href = url;
            return true;
        }
        
        if (allowedDomains.includes(parsed.hostname)) {
            window.location.href = url;
            return true;
        }
        
        console.warn('Blocked redirect to:', url);
        return false;
    } catch {
        console.error('Invalid URL:', url);
        return false;
    }
}

Secure Communication

Ensure all data transmission is encrypted and protected against interception.

JavaScript - Secure Communication
// Force HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
    location.replace(`https:${location.href.substring(location.protocol.length)}`);
}

// Secure fetch wrapper
async function secureFetch(url, options = {}) {
    // Enforce HTTPS
    const parsedURL = new URL(url, window.location.origin);
    if (parsedURL.protocol !== 'https:' && parsedURL.hostname !== 'localhost') {
        throw new Error('HTTPS required');
    }
    
    // Add security headers
    const secureOptions = {
        ...options,
        credentials: 'same-origin', // Or 'include' for cross-origin with cookies
        headers: {
            ...options.headers,
            'Content-Type': 'application/json'
        }
    };
    
    // Add CSRF token if available
    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
    if (csrfToken) {
        secureOptions.headers['X-CSRF-Token'] = csrfToken;
    }
    
    return fetch(url, secureOptions);
}

// Subresource Integrity check
async function loadScriptWithSRI(url, expectedHash) {
    const response = await fetch(url);
    const content = await response.text();
    
    // Calculate hash
    const encoder = new TextEncoder();
    const data = encoder.encode(content);
    const hashBuffer = await crypto.subtle.digest('SHA-384', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashBase64 = btoa(String.fromCharCode(...hashArray));
    
    if (`sha384-${hashBase64}` !== expectedHash) {
        throw new Error('Script integrity check failed');
    }
    
    // Safe to execute
    const script = document.createElement('script');
    script.textContent = content;
    document.head.appendChild(script);
}

// Encrypt sensitive data before storage
async function encryptForStorage(data, key) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(JSON.stringify(data));
    
    const iv = crypto.getRandomValues(new Uint8Array(12));
    
    const encrypted = await crypto.subtle.encrypt(
        { name: 'AES-GCM', iv },
        key,
        dataBuffer
    );
    
    return {
        iv: Array.from(iv),
        data: Array.from(new Uint8Array(encrypted))
    };
}
Security Checklist
  • Never trust user input - always validate and sanitize
  • Use HTTPS for all production traffic
  • Implement CSP to prevent XSS attacks
  • Use HttpOnly cookies for sensitive tokens
  • Keep dependencies updated - run npm audit regularly
  • Limit permissions - principle of least privilege

Summary

XSS Prevention

Escape output, use textContent

CSRF Protection

Tokens, SameSite cookies

Avoid Injection

No eval, parameterized queries

Data Security

HTTPS, crypto API, httpOnly cookies

CSP

Control resource loading

Input Validation

Never trust user input