Security is a critical aspect of JavaScript development, especially for browser-based applications where code runs in an untrusted environment. A single vulnerability can expose user data, compromise accounts, or damage your organization’s reputation.

Threat Model Overview

Threat Attack Vector Primary Defense
XSS Inject malicious scripts via user input Output encoding, CSP
CSRF Trick browser into unwanted requests SameSite cookies, tokens
Injection Untrusted data executed as code Parameterized queries, validation
Data exposure Secrets in client-side code Server-side storage, env vars
Supply chain Compromised npm packages Audits, lockfiles, pinning

Preventing Cross-Site Scripting (XSS)

XSS occurs when attackers inject scripts that execute in other users’ browsers.

Types of XSS

  • Stored XSS — malicious script saved in database, served to all viewers
  • Reflected XSS — script in URL parameter reflected in page response
  • DOM-based XSS — client-side JavaScript writes untrusted data to DOM unsafely

Output Encoding

Never insert user input into HTML without encoding:

  function escapeHtml(text) {
    const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
    return String(text).replace(/[&<>"']/g, char => map[char]);
}

// BAD
element.innerHTML = userInput;

// GOOD
element.textContent = userInput;

// Or with encoding
element.innerHTML = escapeHtml(userInput);
  

Prefer textContent over innerHTML. Use DOMPurify if HTML rendering is required.

Content Security Policy (CSP)

CSP restricts which scripts, styles, and resources can load:

  <meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  

Server header (preferred):

  Content-Security-Policy: default-src 'self'; script-src 'self'
  

CSP blocks inline scripts and unauthorized external scripts even if XSS injection succeeds.

Preventing Cross-Site Request Forgery (CSRF)

CSRF tricks authenticated users into submitting unwanted requests (e.g., transferring money).

Defenses

  <!-- Anti-CSRF token in forms -->
<form method="post" action="/transfer">
    <input type="hidden" name="csrf_token" value="server-generated-token">
    <input type="number" name="amount">
    <button type="submit">Transfer</button>
</form>
  
  // Include token in fetch requests
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCsrfToken()
    },
    body: JSON.stringify({ amount: 100 })
});
  
  • Set cookies with SameSite=Strict or SameSite=Lax
  • Verify Origin and Referer headers server-side
  • Use custom headers (X-Requested-With) that simple form submissions cannot set

Avoiding Injection Attacks

Never concatenate user input into queries or shell commands:

  // BAD — SQL injection risk (server-side)
const query = `SELECT * FROM users WHERE name = '${userInput}'`;

// GOOD — parameterized query
db.query('SELECT * FROM users WHERE name = ?', [userInput]);
  

Validate and sanitize input at system boundaries:

  function sanitizeUsername(input) {
    const cleaned = input.trim();
    if (!/^[a-zA-Z0-9_]{3,20}$/.test(cleaned)) {
        throw new Error('Invalid username format');
    }
    return cleaned;
}
  

Secure Authentication

Never Store Secrets Client-Side

  // BAD — visible to anyone who opens DevTools
const API_KEY = 'sk-live-abc123secret';

// GOOD — proxy through your server
const response = await fetch('/api/data'); // server adds API key
  

Token Storage

Storage XSS Risk CSRF Risk Recommendation
localStorage High — JS can read None Avoid for auth tokens
HttpOnly cookie Low — JS cannot read Mitigate with SameSite Preferred for sessions
Memory (variable) Lost on refresh None OK for short-lived SPA tokens

Use server-side libraries (bcrypt, argon2) for password hashing — never roll your own crypto.

Dependency Security

  # Audit npm dependencies
npm audit

# Fix auto-fixable issues
npm audit fix

# Check for known vulnerabilities in CI
npx audit-ci --moderate
  

Best practices:

  • Commit package-lock.json for reproducible installs
  • Review new dependencies before adding
  • Use npm ci in CI/CD (not npm install)
  • Enable Dependabot or Snyk for automated alerts

Handling Errors Securely

Don’t expose stack traces or internal details to users:

  // BAD
catch (error) {
    res.status(500).json({ error: error.stack });
}

// GOOD
catch (error) {
    logger.error('Payment failed', { userId, error: error.message });
    res.status(500).json({ error: 'An unexpected error occurred' });
}
  

Additional Hardening

  • HTTPS everywhere — enforce TLS; set Secure flag on cookies
  • Subresource Integrity (SRI) — verify CDN scripts haven’t been tampered with
  • Rate limiting — prevent brute force on login endpoints
  • Input length limits — prevent denial-of-service via oversized payloads
  • Principle of least privilege — RBAC on both client and server

Troubleshooting Security Issues

Symptom Investigation Fix
Script runs from user comment Check innerHTML usage Use textContent or DOMPurify
Session hijacked Check cookie flags Add HttpOnly, Secure, SameSite
npm audit warnings Run npm audit Update or replace vulnerable packages
CORS errors exposing data Review Access-Control headers Restrict allowed origins
Token visible in DevTools Stored in localStorage Move to HttpOnly cookie

Security is not a feature you add at the end — it must be designed into every layer of your JavaScript application from authentication and input handling to dependency management and error responses.