Skip to main content
โšก Calmops

XSS Prevention and Content Security Policy in JavaScript

XSS Prevention and Content Security Policy in JavaScript

Cross-Site Scripting (XSS) is a critical vulnerability. This article covers XSS types, prevention techniques, and Content Security Policy implementation.

Introduction

XSS prevention provides:

  • Attack protection
  • Data security
  • User safety
  • Application integrity
  • Compliance

Understanding XSS helps you:

  • Identify vulnerabilities
  • Implement defenses
  • Use CSP effectively
  • Secure applications
  • Protect users

XSS Attack Types

Stored XSS

// โŒ Bad: Vulnerable to stored XSS
// User input stored in database
const userComment = '<img src=x onerror="alert(\'XSS\')">';
database.save('comments', userComment);

// Later, when displayed:
document.getElementById('comments').innerHTML = userComment; // Executes!

// โœ… Good: Sanitize before storing
const sanitized = escapeHTML(userComment);
database.save('comments', sanitized);

// Display safely:
document.getElementById('comments').textContent = sanitized;

Reflected XSS

// โŒ Bad: Vulnerable to reflected XSS
// URL: http://example.com?search=<script>alert('XSS')</script>
const searchParam = new URLSearchParams(window.location.search).get('search');
document.getElementById('results').innerHTML = `Search results for: ${searchParam}`;

// โœ… Good: Sanitize URL parameters
const searchParam = new URLSearchParams(window.location.search).get('search');
const sanitized = escapeHTML(searchParam);
document.getElementById('results').textContent = `Search results for: ${sanitized}`;

DOM-based XSS

// โŒ Bad: Vulnerable to DOM-based XSS
function updateProfile(data) {
  document.getElementById('profile').innerHTML = data.bio; // Dangerous!
}

// โœ… Good: Use textContent or sanitize
function updateProfile(data) {
  document.getElementById('profile').textContent = data.bio; // Safe
}

// Or sanitize if HTML is needed:
function updateProfileHTML(data) {
  const sanitized = DOMPurify.sanitize(data.bio);
  document.getElementById('profile').innerHTML = sanitized;
}

XSS Prevention Techniques

Output Encoding

// โœ… Good: Encode output based on context
function encodeHTML(text) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  };
  return text.replace(/[&<>"']/g, (char) => map[char]);
}

function encodeURL(url) {
  return encodeURIComponent(url);
}

function encodeJavaScript(str) {
  return str
    .replace(/\\/g, '\\\\')
    .replace(/"/g, '\\"')
    .replace(/'/g, "\\'")
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r');
}

// Usage
const userInput = '<script>alert("XSS")</script>';
console.log(encodeHTML(userInput));
// &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

Input Validation

// โœ… Good: Validate input
function validateUserInput(input, type = 'text') {
  switch (type) {
    case 'email':
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
    case 'url':
      try {
        new URL(input);
        return true;
      } catch {
        return false;
      }
    case 'text':
      return typeof input === 'string' && input.length > 0;
    case 'number':
      return !isNaN(input) && isFinite(input);
    default:
      return false;
  }
}

// Usage
console.log(validateUserInput('[email protected]', 'email')); // true
console.log(validateUserInput('<script>', 'text')); // true (but will be encoded)

Content Security Policy (CSP)

// โœ… Good: Implement CSP headers (server-side)
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' https://fonts.googleapis.com

// Directives:
// default-src: Default policy for all content
// script-src: Allowed sources for scripts
// style-src: Allowed sources for stylesheets
// img-src: Allowed sources for images
// font-src: Allowed sources for fonts
// connect-src: Allowed sources for fetch/XHR
// frame-src: Allowed sources for iframes
// object-src: Allowed sources for plugins

// Client-side: Check CSP violations
document.addEventListener('securitypolicyviolation', (e) => {
  console.error('CSP violation:', {
    blockedURI: e.blockedURI,
    violatedDirective: e.violatedDirective,
    originalPolicy: e.originalPolicy
  });
});

Strict CSP

// โœ… Good: Strict CSP with nonce
// Server generates nonce for each request
const nonce = generateRandomNonce();

// Header:
// Content-Security-Policy: script-src 'nonce-${nonce}'; default-src 'self'

// HTML:
// <script nonce="${nonce}">
//   // Inline script allowed only with matching nonce
// </script>

// JavaScript:
function generateRandomNonce() {
  return Math.random().toString(36).substring(2, 15) +
         Math.random().toString(36).substring(2, 15);
}

function addScriptWithNonce(code, nonce) {
  const script = document.createElement('script');
  script.nonce = nonce;
  script.textContent = code;
  document.head.appendChild(script);
}

Secure DOM Manipulation

Safe Element Creation

// โœ… Good: Safe element creation
function createSafeElement(tag, content, attributes = {}) {
  const element = document.createElement(tag);
  element.textContent = content; // Safe - no HTML parsing

  for (const [key, value] of Object.entries(attributes)) {
    // Validate attribute
    if (isAllowedAttribute(key)) {
      element.setAttribute(key, value);
    }
  }

  return element;
}

function isAllowedAttribute(attr) {
  const allowedAttrs = ['class', 'id', 'data-*', 'aria-*'];
  return allowedAttrs.some(allowed => {
    if (allowed.endsWith('-*')) {
      return attr.startsWith(allowed.slice(0, -2));
    }
    return attr === allowed;
  });
}

// Usage
const div = createSafeElement('div', '<script>alert("XSS")</script>', {
  class: 'container'
});
// Creates: <div class="container">&lt;script&gt;alert("XSS")&lt;/script&gt;</div>

Template Literals Safely

// โœ… Good: Use template literals with encoding
function renderUserProfile(user) {
  const encodedName = encodeHTML(user.name);
  const encodedBio = encodeHTML(user.bio);

  return `
    <div class="profile">
      <h1>${encodedName}</h1>
      <p>${encodedBio}</p>
    </div>
  `;
}

// โŒ Bad: Direct interpolation
function renderUserProfileUnsafe(user) {
  return `
    <div class="profile">
      <h1>${user.name}</h1>
      <p>${user.bio}</p>
    </div>
  `;
}

// Usage
const user = { name: '<img src=x onerror="alert(\'XSS\')">', bio: 'Developer' };
const html = renderUserProfile(user);
document.getElementById('profile').innerHTML = html; // Safe

Using Security Libraries

DOMPurify

// โœ… Good: Use DOMPurify for HTML sanitization
// Include: <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>

function sanitizeHTML(html) {
  return DOMPurify.sanitize(html);
}

function sanitizeWithConfig(html, config = {}) {
  const defaultConfig = {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
    ALLOWED_ATTR: ['href', 'title'],
    KEEP_CONTENT: true
  };

  return DOMPurify.sanitize(html, { ...defaultConfig, ...config });
}

// Usage
const userHTML = '<p>Hello <script>alert("XSS")</script></p>';
const safe = sanitizeHTML(userHTML);
// <p>Hello </p>

document.getElementById('content').innerHTML = safe;

Trusted Types API

// โœ… Good: Use Trusted Types API (modern browsers)
if (window.trustedTypes) {
  const policy = trustedTypes.createPolicy('default', {
    createHTML: (string) => {
      // Sanitize HTML
      return DOMPurify.sanitize(string);
    },
    createScript: (string) => {
      // Validate script
      if (isAllowedScript(string)) {
        return string;
      }
      throw new Error('Script not allowed');
    },
    createScriptURL: (string) => {
      // Validate script URL
      if (isAllowedURL(string)) {
        return string;
      }
      throw new Error('URL not allowed');
    }
  });

  // Now innerHTML only accepts TrustedHTML
  document.getElementById('content').innerHTML = policy.createHTML(userHTML);
}

function isAllowedScript(script) {
  // Implement validation logic
  return true;
}

function isAllowedURL(url) {
  // Implement validation logic
  return url.startsWith('https://');
}

Practical Security Examples

Secure Comment System

// โœ… Good: Secure comment system
class SecureCommentSystem {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.comments = [];
  }

  addComment(author, text) {
    // Validate input
    if (!author || !text) {
      throw new Error('Author and text are required');
    }

    // Sanitize input
    const sanitizedAuthor = escapeHTML(author.trim());
    const sanitizedText = escapeHTML(text.trim());

    // Store comment
    const comment = {
      id: Date.now(),
      author: sanitizedAuthor,
      text: sanitizedText,
      timestamp: new Date()
    };

    this.comments.push(comment);
    this.render();
  }

  render() {
    this.container.innerHTML = '';

    this.comments.forEach(comment => {
      const div = document.createElement('div');
      div.className = 'comment';

      const author = document.createElement('strong');
      author.textContent = comment.author;

      const text = document.createElement('p');
      text.textContent = comment.text;

      const time = document.createElement('small');
      time.textContent = comment.timestamp.toLocaleString();

      div.appendChild(author);
      div.appendChild(text);
      div.appendChild(time);

      this.container.appendChild(div);
    });
  }

  deleteComment(id) {
    this.comments = this.comments.filter(c => c.id !== id);
    this.render();
  }
}

// Usage
const comments = new SecureCommentSystem('comments-container');
comments.addComment('John', 'Great article!');
comments.addComment('Jane', '<script>alert("XSS")</script>'); // Safely escaped

Secure User Profile Display

// โœ… Good: Secure user profile
class UserProfile {
  constructor(user) {
    this.user = user;
  }

  render() {
    const container = document.createElement('div');
    container.className = 'user-profile';

    // Name
    const name = document.createElement('h1');
    name.textContent = this.user.name;

    // Email
    const email = document.createElement('p');
    email.textContent = `Email: ${this.user.email}`;

    // Bio
    const bio = document.createElement('p');
    bio.textContent = this.user.bio;

    // Avatar (with URL validation)
    const avatar = document.createElement('img');
    const avatarURL = this.validateImageURL(this.user.avatarURL);
    if (avatarURL) {
      avatar.src = avatarURL;
      avatar.alt = this.user.name;
    }

    container.appendChild(avatar);
    container.appendChild(name);
    container.appendChild(email);
    container.appendChild(bio);

    return container;
  }

  validateImageURL(url) {
    try {
      const parsed = new URL(url);
      // Only allow https
      if (parsed.protocol !== 'https:') {
        return null;
      }
      // Only allow specific domains
      const allowedDomains = ['cdn.example.com', 'images.example.com'];
      if (!allowedDomains.includes(parsed.hostname)) {
        return null;
      }
      return url;
    } catch {
      return null;
    }
  }
}

// Usage
const user = {
  name: 'John Doe',
  email: '[email protected]',
  bio: 'Software developer',
  avatarURL: 'https://cdn.example.com/avatars/john.jpg'
};

const profile = new UserProfile(user);
document.getElementById('profile').appendChild(profile.render());

Best Practices

  1. Use textContent for user content:

    // โœ… Good
    element.textContent = userInput;
    
    // โŒ Bad
    element.innerHTML = userInput;
    
  2. Implement CSP headers:

    // โœ… Good
    // Content-Security-Policy: default-src 'self'
    
    // โŒ Bad
    // No CSP headers
    
  3. Validate and sanitize:

    // โœ… Good
    const sanitized = DOMPurify.sanitize(userHTML);
    
    // โŒ Bad
    const html = userHTML;
    

Common Mistakes

  1. Using innerHTML with user input:

    // โŒ Bad
    element.innerHTML = userInput;
    
    // โœ… Good
    element.textContent = userInput;
    
  2. Not implementing CSP:

    // โŒ Bad - no CSP protection
    
    // โœ… Good
    // Content-Security-Policy: default-src 'self'
    
  3. Trusting client-side validation:

    // โŒ Bad - only client validation
    if (validateInput(input)) {
      submitForm();
    }
    
    // โœ… Good - validate server-side too
    

Summary

XSS prevention is critical for security. Key takeaways:

  • Understand XSS types
  • Encode output
  • Validate input
  • Implement CSP
  • Use security libraries
  • Sanitize HTML
  • Use textContent
  • Validate URLs

Next Steps

Comments