Client-Side Security Essentials

Reviewed & published by Brayan K

Client-side security is the practice of protecting browser-based JavaScript apps from attacks like cross-site scripting (XSS) and cross-site request forgery (CSRF) by sanitizing input, handling tokens safely, and layering defenses.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

While this online editor runs real JavaScript, some security examples require a browser environment. Download Node.js to run JavaScript on your computer, use your browser's Developer Console (Press F12) to test code snippets, or create a .html file with <script> tags and open it in your browser.

Master XSS prevention, CSRF protection, input sanitization, secure token handling, and defense-in-depth architecture.

What You'll Learn

Why Client-Side Security Matters

The browser is exposed territory. Anyone can inject HTML, override JavaScript, modify requests, steal tokens, and manipulate the DOM. The only safe approach is to treat all user-controlled input as hostile.

What Attackers Can Do

Your Defense Goals

Cross-Site Scripting (XSS) — The #1 Frontend Threat

XSS occurs when malicious script executes inside your page. Even one tiny mistake can compromise your entire application.

Three Types of XSS

// ❌ DANGEROUS - XSS vulnerabilities
const userInput = '<img src=x onerror="alert(document.cookie)">';

// This allows script execution!
document.getElementById("output").innerHTML = userInput;

// ✅ SAFE - Use textContent instead
document.getElementById("output").textContent = userInput;
// Renders as plain text, not HTML

// ❌ DANGEROUS - Setting href from user input
const link = document.createElement("a");
link.href = "javascript:alert('XSS')"; // Executes JS!

// ✅ SAFE - Validate URLs first
function safeURL(url) {
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  return "about:blank"; // Safe fallback
}

link.href = safeURL(userInput);

// ❌ DANGEROUS - eval() with user input
eval(userInput); // NEVER do this!

// ❌ DANGEROUS - Dynamic script creation
const script = document.createElement("script");
script.textContent = userInput; // Can execute anything

console.log("✅ Always sanitize user input before rendering!");

HTML Escaping — Your First Line of Defense

Escaping converts dangerous characters into safe HTML entities. This is essential when you must display user content.

// HTML escape function - prevents XSS
function escapeHTML(str) {
  const escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
    '/': '&#x2F;',
    '`': '&#x60;',
    '=': '&#x3D;'
  };
  
  return String(str).replace(/[&<>"'`=\/]/g, char => escapeMap[char]);
}

// Test with malicious input
const maliciousInput = '<script>alert("XSS")</script>';
const safeOutput = escapeHTML(maliciousInput);

console.log("Original:", maliciousInput);
console.log("Escaped:", safeOutput);
// Output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;&#x2F;script&gt;

// Safe to insert as HTML now
// element.innerHTML = safeOutput;

// Practical example: user comment
const userComment = `
  <b>Bold attack</b>
  <img src=x onerror=alert(1)>
  Normal text here
`;

console.log("\nUser comment escaped:");
console.log(escapeHTML(userComment));

DOM-Based XSS — Common in SPAs

DOM XSS happens when JavaScript reads user input (from URLs, forms, storage) and inserts it into the page without sanitization. This is extremely common in React, Vue, and Angular apps.

// DOM-based XSS - when URL params go directly to DOM

// ❌ DANGEROUS - Reading URL and inserting into DOM
const urlParams = new URLSearchParams("?name=<img src=x onerror=alert(1)>");
const name = urlParams.get("name");

// This would execute the XSS payload:
// document.body.innerHTML = "Hello, " + name;

// ✅ SAFE - Always sanitize URL parameters
function sanitizeParam(param) {
  if (!param) return "";
  
  // Use textContent approach
  const div = document.createElement("div");
  div.textContent = param;
  return div.innerHTML; // Now escaped
}

const safeName = sanitizeParam(name);
console.log("Safe name:", safeName);

// ✅ SAFE - Use textContent for display
function displayUserName(name) {
  const container = document.createElement("div");
  container.textContent = "Hello, " + name;
  return container.outerHTML;
}

console.log("Safe display:", displayUserName(name));

// ❌ DANGEROUS - document.write with user data
// document.write(location.hash.substring(1));

// ✅ SAFE - Validate hash before use
function safeHash() {
  const hash = location.hash.substring(1);
  // Only allow alphanumeric and hyphens
  if (/^[a-zA-Z0-9-]+$/.test(hash)) {
    return hash;
  }
  return "";
}

console.log("\nAlways validate URL parameters and hash fragments!");

Cross-Site Request Forgery (CSRF)

CSRF forces a victim's browser to make authenticated requests without their knowledge. If your app uses cookies for authentication, you're vulnerable.

CSRF Defense Layers

// CSRF Attack Example and Defense

// ❌ How CSRF works:
// 1. User logs into bank.com (session cookie stored)
// 2. User visits evil.com
// 3. Evil.com has hidden form:
/*
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="amount" value="10000">
  <input type="hidden" name="to" value="attacker">
</form>
<script>document.forms[0].submit()</script>
*/
// 4. Browser sends cookies automatically
// 5. Bank thinks it's legitimate request

// ✅ CSRF Token Protection
class CSRFProtection {
  constructor() {
    this.token = this.generateToken();
  }
  
  generateToken() {
    // Generate cryptographically random token
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
  }
  
  // Add token to all requests
  addToRequest(options = {}) {
    return {
      ...options,
      headers: {
        ...options.headers,
        'X-CSRF-Token': this.token
      }
    };
  }
  
  // Validate token on "server" side
  validateToken(requestToken) {
    return requestToken === this.token;
  }
}

const csrf = new CSRFProtection();
console.log("CSRF Token:", csrf.token.substring(0, 20) + "...");

// Making a protected request
const fetchOptions = csrf.addToRequest({
  method: 'POST',
  body: JSON.stringify({ amount: 100 })
});

console.log("\nProtected request headers:");
console.log(fetchOptions.headers);

// Validation
console.log("\nValid token?", csrf.validateToken(csrf.token));
console.log("Invalid token?", csrf.validateToken("fake-token"));

Input Sanitization — Clean Everything

Any time user input appears in HTML, URLs, DOM, or attributes — it must be sanitized.

// Comprehensive Input Sanitization

// Basic sanitizer for plain text
function sanitizeText(input) {
  if (typeof input !== 'string') return '';
  
  return input
    // Remove null bytes
    .replace(/\0/g, '')
    // Normalize unicode
    .normalize('NFC')
    // Trim whitespace
    .trim()
    // Limit length
    .slice(0, 1000);
}

// HTML sanitizer (basic - use DOMPurify in production)
function sanitizeHTML(html) {
  // Create temporary element
  const temp = document.createElement('div');
  temp.textContent = html;
  return temp.innerHTML;
}

// URL sanitizer
function sanitizeURL(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    
    // Only allow http/https protocols
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return 'about:blank';
    }
    
    // Optionally: only allow same origin
    // if (parsed.origin !== window.location.origin) {
    //   return 'about:blank';
    // }
    
    return parsed.href;
  } catch {
    return 'about:blank';
  }
}

// Test cases
console.log("=== Text Sanitization ===");
console.log(sanitizeText("  Hello World  "));
console.log(sanitizeText("<script>alert(1)</script>"));

console.log("\n=== HTML Sanitization ===");
console.log(sanitizeHTML("<b>Bold</b>"));
console.log(sanitizeHTML('<img src=x onerror="alert(1)">'));

console.log("\n=== URL Sanitization ===");
console.log(sanitizeURL("https://example.com"));
console.log(sanitizeURL("javascript:alert(1)"));
console.log(sanitizeURL("data:text/html,<script>"));

Use DOMPurify for production HTML sanitization. It handles SVG attacks, mutation XSS, and edge cases that simple regex cannot.

Advanced XSS Vectors

Even when developers escape <script> tags, attackers exploit less obvious injection points.

// Advanced XSS Vectors to Watch For

// 1. Event Handler Injection
const userAvatar = 'x" onerror="stealCookies()';
// ❌ DANGEROUS:
// `<img src="${userAvatar}">`
// Results in: <img src="x" onerror="stealCookies()">

// ✅ SAFE: Escape attribute values
function escapeAttr(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

console.log("Safe attribute:", escapeAttr(userAvatar));

// 2. Template Literal Injection
const userName = '${alert(1)}';
// In some template engines, this could execute!
// Always sanitize before template insertion

// 3. SVG-based XSS
const maliciousSVG = '<svg onload="alert(1)"></svg>';
console.log("\nSVG attack (escaped):", escapeAttr(maliciousSVG));

// 4. CSS Injection (in older browsers)
const cssAttack = 'expression(alert(1))';
// Can execute JS via CSS in old IE

// 5. Unicode Bypass Attempts
const unicodeAttack = '\u003cscript\u003e'; // <script> in unicode
console.log("Unicode decoded:", JSON.parse('"' + unicodeAttack + '"'));

// 6. Double Encoding Bypass
const doubleEncoded = '%253Cscript%253E'; // %3C = <
console.log("Double encoded attack:", decodeURIComponent(decodeURIComponent(doubleEncoded)));

// Defense: Always sanitize at the LAST moment before rendering
// Never trust data that has been "already sanitized" from elsewhere

Secure Token Handling

Token theft is the ultimate goal of most XSS attacks. Never store sensitive tokens in localStorage — XSS can steal them.

❌ Dangerous Token Storage

// Secure Token Handling

// ❌ DANGEROUS - Token in localStorage (XSS can steal it)
// localStorage.setItem('token', 'secret-jwt-token');

// ❌ DANGEROUS - Token in URL
// window.location = '/dashboard?token=secret';

// ✅ BETTER - Token in memory (cleared on page refresh)
class SecureTokenStore {
  #accessToken = null;
  #tokenExpiry = null;
  
  setToken(token, expiresIn = 3600) {
    this.#accessToken = token;
    this.#tokenExpiry = Date.now() + (expiresIn * 1000);
  }
  
  getToken() {
    if (!this.#accessToken) return null;
    
    // Check expiry
    if (Date.now() > this.#tokenExpiry) {
      this.clearToken();
      return null;
    }
    
    return this.#accessToken;
  }
  
  clearToken() {
    this.#accessToken = null;
    this.#tokenExpiry = null;
  }
  
  isValid() {
    return this.getToken() !== null;
  }
}

const tokenStore = new SecureTokenStore();
tokenStore.setToken('my-access-token', 3600);

console.log("Token valid?", tokenStore.isValid());
console.log("Token value:", tokenStore.getToken() ? "***hidden***" : "none");

// For API calls, use Authorization header
function secureApiCall(url, options = {}) {
  const token = tokenStore.getToken();
  
  if (!token) {
    throw new Error('No valid token');
  }
  
  return {
    url,
    options: {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      }
    }
  };
}

console.log("\nSecure API call config:");
console.log(secureApiCall('/api/data'));

Content Security Policy (CSP)

CSP is a browser-level defense that restricts what scripts can run. Even if an attacker injects a script, CSP can block it from executing.

// Content Security Policy (CSP) - Browser-Level XSS Protection

// CSP is set via HTTP header by the server:
// Content-Security-Policy: default-src 'self'; script-src 'self'

// Example CSP configurations:

const cspExamples = {
  // Strict - Only same-origin resources
  strict: {
    "default-src": "'self'",
    "script-src": "'self'",
    "style-src": "'self'",
    "img-src": "'self' data:",
    "connect-src": "'self'",
    "frame-ancestors": "'none'",
    "object-src": "'none'"
  },
  
  // Moderate - Allow specific CDNs
  moderate: {
    "default-src": "'self'",
    "script-src": "'self' https://cdn.jsdelivr.net",
    "style-src": "'self' 'unsafe-inline'", // Allow inline styles
    "img-src": "'self' https: data:",
    "connect-src": "'self' https://api.example.com"
  },
  
  // With nonce for inline scripts (most secure for SPAs)
  withNonce: {
    "script-src": "'self' 'nonce-abc123'",
    // Only scripts with matching nonce will run:
    // <script nonce="abc123">safe code</script>
  }
};

// Convert to header string
function toCSPHeader(policy) {
  return Object.entries(policy)
    .map(([key, value]) => `${key} ${value}`)
    .join('; ');
}

console.log("Strict CSP:");
console.log(toCSPHeader(cspExamples.strict));

console.log("\nModerate CSP:");
console.log(toCSPHeader(cspExamples.moderate));

// What CSP blocks:
console.log("\n❌ CSP blocks:");
console.log("- Inline scripts without nonce");
console.log("- Scripts from non-whitelisted domains");
console.log("- eval() and new Function()");
console.log("- Inline event handlers (onclick, etc.)");
console.log("- javascript: URLs");

Defense-in-Depth Architecture

No single mechanism protects a modern app. You need layered security — if one layer fails, others prevent disaster.

// Defense-in-Depth Security Architecture

class SecureApp {
  constructor() {
    this.csrfToken = this.generateCSRFToken();
  }
  
  // Layer 1: Input Validation
  validateInput(input, rules) {
    const errors = [];
    
    if (rules.required && !input) {
      errors.push('Field is required');
    }
    if (rules.maxLength && input.length > rules.maxLength) {
      errors.push(`Max length is ${rules.maxLength}`);
    }
    if (rules.pattern && !rules.pattern.test(input)) {
      errors.push('Invalid format');
    }
    
    return { valid: errors.length === 0, errors };
  }
  
  // Layer 2: Sanitization
  sanitize(input, type = 'text') {
    if (type === 'text') {
      return String(input).replace(/[<>]/g, '');
    }
    if (type === 'html') {
      const div = document.createElement('div');
      div.textContent = input;
      return div.innerHTML;
    }
    if (type === 'url') {
      try {
        const url = new URL(input);
        if (!['http:', 'https:'].includes(url.protocol)) {
          return 'about:blank';
        }
        return url.href;
      } catch {
        return 'about:blank';
      }
    }
    return '';
  }
  
  // Layer 3: CSRF Protection
  generateCSRFToken() {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
  }
  
  // Layer 4: Safe Rendering
  safeRender(text, element) {
    // Always use textContent for user data
    const span = document.createElement('span');
    span.textContent = text;
    return span.outerHTML;
  }
  
  // Layer 5: Secure API Calls
  async secureRequest(url, options = {}) {
    return {
      url,
      ...options,
      headers: {
        ...options.headers,
        'X-CSRF-Token': this.csrfToken,
        'Content-Type': 'application/json'
      },
      credentials: 'same-origin' // Don't send cookies cross-origin
    };
  }
}

// Usage
const app = new SecureApp();

// Validate
const validation = app.validateInput('[email protected]', {
  required: true,
  maxLength: 100,
  pattern: /^[^@]+@[^@]+\.[^@]+$/
});
console.log("Validation:", validation);

// Sanitize
console.log("\nSanitized text:", app.sanitize('<script>alert(1)</script>'));
console.log("Sanitized URL:", app.sanitize('javascript:alert(1)', 'url'));

// Safe render
console.log("\nSafe HTML:", app.safeRender('<b>Bold</b>'));

console.log("\nCSRF Token:", app.csrfToken);

Security Best Practices Checklist

// Security Best Practices Checklist

const securityChecklist = {
  // XSS Prevention
  xss: {
    "Use textContent instead of innerHTML": true,
    "Escape HTML entities": true,
    "Use DOMPurify for HTML sanitization": true,
    "Validate URL protocols": true,
    "Never use eval()": true,
    "Implement CSP headers": true
  },
  
  // CSRF Prevention
  csrf: {
    "Use CSRF tokens": true,
    "SameSite cookie attribute": true,
    "Verify Origin header": true,
    "Use POST for state changes": true
  },
  
  // Token Security
  tokens: {
    "HttpOnly cookies for sessions": true,
    "Secure flag on cookies": true,
    "Short token expiry": true,
    "Don't store tokens in localStorage": true,
    "Use Authorization header": true
  },
  
  // General
  general: {
    "Input validation": true,
    "Output encoding": true,
    "HTTPS everywhere": true,
    "Audit dependencies": true,
    "Minimize third-party scripts": true
  }
};

// Display checklist
function displayChecklist(checklist) {
  for (const [category, items] of Object.entries(checklist)) {
    console.log(`\n=== ${category.toUpperCase()} ===`);
    for (const [item, checked] of Object.entries(items)) {
      console.log(`${checked ? '✅' : '❌'} ${item}`);
    }
  }
}

displayChecklist(securityChecklist);

// Quick security test
function quickSecurityAudit(code) {
  const risks = [];
  
  if (code.includes('innerHTML')) {
    risks.push('⚠️ innerHTML usage detected - ensure sanitization');
  }
  if (code.includes('eval(')) {
    risks.push('🚨 eval() usage - CRITICAL RISK');
  }
  if (code.includes('document.write')) {
    risks.push('⚠️ document.write usage - XSS risk');
  }
  if (code.includes('localStorage') && code.includes('token')) {
    risks.push('⚠️ Token in localStorage - XSS can steal it');
  }
  
  return risks.length > 0 ? risks : ['✅ No obvious risks detected'];
}

console.log("\n=== Code Audit Example ===");
const testCode = 'element.innerHTML = userInput; localStorage.setItem("token", jwt);';
console.log("Risks:", quickSecurityAudit(testCode));
// 🎯 YOUR TURN — replace each ___ using the hint beside it.

function escapeHtml(text) {
  return String(text)
    // 1) & goes FIRST, or you would re-escape the & in &lt; below.
    .replaceAll("&", "___")            // 👉 replace ___ with &amp;
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

const comment = '<img src=x onerror="steal()">';
console.log("Raw:    ", comment);
console.log("Escaped:", escapeHtml(comment));

// 2) An allow-list, not a block-list: anything not named is refused.
const allowed = new ___(["/home", "/about"]);   // 👉 replace ___ with Set
function safeRedirect(target) {
  // 3) Ask the allow-list whether it holds this exact value.
  return allowed.___(target) ? target : "/home";  // 👉 replace ___ with has
}
console.log("Good:", safeRedirect("/about"));
console.log("Bad: ", safeRedirect("https://evil.example/steal"));

// 4) Parse a query string properly instead of splitting on & and = yourself.
const params = new ___("q=hello&page=2");       // 👉 replace ___ with URLSearchParams
console.log("Query:", params.get("q"), "page", params.get("page"));

// 5) Send only the fields the client needs. Never the whole record.
const user = { name: "ada", role: "admin" };
const safeCopy = { name: user.___ };            // 👉 replace ___ with name
console.log("Sent to client:", JSON.stringify(safeCopy));

console.log("Never do this:", typeof eval === "function" ? "eval exists, do not feed it input" : "no eval");

// ✅ Expected output:
// Raw:     <img src=x onerror="steal()">
// Escaped: &lt;img src=x onerror=&quot;steal()&quot;&gt;
// Good: /about
// Bad:  /home
// Query: hello page 2
// Sent to client: {"name":"ada"}
// Never do this: eval exists, do not feed it input

🛡️ Security Mastery Summary

Practice quiz

What does XSS stand for?

  • Extended Style Sheets
  • XML Server Security
  • Cross-Site Scripting
  • Cross-Server Sync

Answer: Cross-Site Scripting. XSS is Cross-Site Scripting, where malicious script executes inside your page.

Which is the safe way to display untrusted user input?

  • element.textContent = input
  • element.innerHTML = input
  • eval(input)
  • document.write(input)

Answer: element.textContent = input. textContent renders input as plain text, not executable HTML.

What does HTML escaping do?

  • Removes all text
  • Encrypts the page
  • Deletes scripts from disk
  • Converts dangerous characters like < and > into safe HTML entities

Answer: Converts dangerous characters like < and > into safe HTML entities. Escaping turns characters like < > & " into entities so they can't form active markup.

What is CSRF?

  • A faster fetch API
  • Forcing a victim's browser to make authenticated requests without their knowledge
  • A CSS framework
  • A caching strategy

Answer: Forcing a victim's browser to make authenticated requests without their knowledge. Cross-Site Request Forgery tricks the browser into sending authenticated requests.

Which is a recommended CSRF defense in the lesson?

  • CSRF tokens and SameSite cookies
  • Store tokens in the URL
  • Use eval for validation
  • Disable HTTPS

Answer: CSRF tokens and SameSite cookies. CSRF tokens, SameSite cookies, and Origin validation defend against CSRF.

Why should you NOT store sensitive tokens in localStorage?

  • It is too slow
  • It has no space
  • XSS can read it
  • It is deprecated

Answer: XSS can read it. localStorage is readable by JavaScript, so an XSS attack can steal the token.

When validating a URL from user input, which protocols should be allowed?

  • javascript: and data:
  • http: and https: only
  • any protocol
  • file: only

Answer: http: and https: only. Only allow http/https; reject javascript: and data: which can execute code.

What is Content Security Policy (CSP)?

  • A linter rule
  • A password manager
  • A bundler plugin
  • A browser-level defense restricting which scripts can run

Answer: A browser-level defense restricting which scripts can run. CSP is set via HTTP header and restricts script sources, blocking injected scripts.

Which dangerous function should you never call with user input?

  • JSON.parse()
  • eval()
  • Array.map()
  • String.trim()

Answer: eval(). eval() executes arbitrary code and must never run untrusted input.

What does defense-in-depth mean for a secure app?

  • One strong firewall is enough
  • Only validate on the server
  • Layer multiple security mechanisms so others hold if one fails
  • Encrypt the CSS

Answer: Layer multiple security mechanisms so others hold if one fails. Layered security (validation, sanitization, CSP, tokens) means one failure doesn't doom the app.

Continue this course