Cross-Site Scripting (XSS) from User Data in DOM innerHTML Assignment

High Risk Cross-Site Scripting
JavaScriptBrowserXSSDOMinnerHTMLClient-Side Security

What it is

Assigning untrusted user input directly to innerHTML properties creates XSS vulnerabilities, allowing attackers to inject malicious scripts that execute in the user's browser context, potentially stealing sensitive data, hijacking sessions, or performing unauthorized actions.

// Example 1: Direct user input to innerHTML
function displayUserMessage() {
    const message = document.getElementById('messageInput').value;
    
    // Vulnerable: Direct assignment allows script injection
    // User input: <img src=x onerror=alert('XSS')>
    document.getElementById('messageDisplay').innerHTML = message;
}

// Example 2: URL parameter to innerHTML
function displayWelcomeMessage() {
    const urlParams = new URLSearchParams(window.location.search);
    const name = urlParams.get('name');
    
    if (name) {
        // Vulnerable: URL parameter can contain malicious HTML
        // URL: ?name=<script>alert('XSS')</script>
        document.getElementById('welcome').innerHTML = `Welcome, ${name}!`;
    }
}

// Example 3: API response to innerHTML
async function displayNews() {
    try {
        const response = await fetch('/api/news');
        const newsData = await response.json();
        
        let html = '';
        newsData.forEach(article => {
            // Vulnerable: API data might contain malicious content
            html += `
                <div class="article">
                    <h3>${article.title}</h3>
                    <p>${article.summary}</p>
                    <div class="content">${article.content}</div>
                </div>
            `;
        });
        
        document.getElementById('news').innerHTML = html;
    } catch (error) {
        console.error('Error loading news:', error);
    }
}

// Example 4: Local storage to innerHTML
function loadSavedContent() {
    const savedContent = localStorage.getItem('userContent');
    
    if (savedContent) {
        // Vulnerable: Local storage can be manipulated
        document.getElementById('content').innerHTML = savedContent;
    }
}

// Example 5: Form data processing
function processContactForm() {
    const formData = new FormData(document.getElementById('contactForm'));
    const name = formData.get('name');
    const email = formData.get('email');
    const message = formData.get('message');
    
    // Vulnerable: Form data directly in innerHTML
    const confirmationHTML = `
        <div class="confirmation">
            <h3>Thank you, ${name}!</h3>
            <p>We received your message:</p>
            <blockquote>${message}</blockquote>
            <p>We'll respond to ${email} soon.</p>
        </div>
    `;
    
    document.getElementById('result').innerHTML = confirmationHTML;
}
// Example 1: Safe user input display
function displayUserMessage() {
    const message = document.getElementById('messageInput').value;
    
    // Secure: Use textContent for plain text
    document.getElementById('messageDisplay').textContent = message;
    
    // Alternative: Create element safely
    const messageElement = document.createElement('div');
    messageElement.className = 'user-message';
    messageElement.textContent = message;
    
    const container = document.getElementById('messageDisplay');
    container.innerHTML = ''; // Clear existing content
    container.appendChild(messageElement);
}

// Example 2: Safe URL parameter handling
function displayWelcomeMessage() {
    const urlParams = new URLSearchParams(window.location.search);
    const name = urlParams.get('name');
    
    if (name && isValidName(name)) {
        // Secure: Use textContent with DOM manipulation
        const welcomeElement = document.createElement('div');
        welcomeElement.className = 'welcome-message';
        
        const welcomeText = document.createTextNode('Welcome, ');
        const nameSpan = document.createElement('span');
        nameSpan.className = 'username';
        nameSpan.textContent = name; // Safe text assignment
        const exclamation = document.createTextNode('!');
        
        welcomeElement.appendChild(welcomeText);
        welcomeElement.appendChild(nameSpan);
        welcomeElement.appendChild(exclamation);
        
        document.getElementById('welcome').appendChild(welcomeElement);
    }
}

function isValidName(name) {
    return typeof name === 'string' && 
           name.length > 0 && 
           name.length <= 50 && 
           /^[a-zA-Z\s'-]+$/.test(name);
}

// Example 3: Safe API response handling
async function displayNews() {
    try {
        const response = await fetch('/api/news');
        const newsData = await response.json();
        
        const newsContainer = document.getElementById('news');
        newsContainer.innerHTML = ''; // Clear existing content
        
        newsData.forEach(article => {
            // Secure: Create elements with DOM APIs
            const articleElement = document.createElement('div');
            articleElement.className = 'article';
            
            const titleElement = document.createElement('h3');
            titleElement.textContent = article.title; // Safe text
            
            const summaryElement = document.createElement('p');
            summaryElement.textContent = article.summary; // Safe text
            
            const contentElement = document.createElement('div');
            contentElement.className = 'content';
            
            // For rich content, sanitize first
            if (article.content) {
                const sanitizedContent = DOMPurify.sanitize(article.content, {
                    ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'em', 'strong'],
                    ALLOWED_ATTR: []
                });
                contentElement.innerHTML = sanitizedContent;
            }
            
            articleElement.appendChild(titleElement);
            articleElement.appendChild(summaryElement);
            articleElement.appendChild(contentElement);
            
            newsContainer.appendChild(articleElement);
        });
    } catch (error) {
        console.error('Error loading news:', error);
        displayErrorMessage('Failed to load news');
    }
}

// Example 4: Safe local storage handling
function loadSavedContent() {
    const savedContent = localStorage.getItem('userContent');
    
    if (savedContent) {
        try {
            // Parse JSON safely
            const contentData = JSON.parse(savedContent);
            
            if (contentData && typeof contentData === 'object') {
                // Validate and display safely
                displayValidatedContent(contentData);
            }
        } catch (error) {
            console.error('Invalid saved content:', error);
            // Clear corrupted data
            localStorage.removeItem('userContent');
        }
    }
}

function displayValidatedContent(contentData) {
    const container = document.getElementById('content');
    container.innerHTML = '';
    
    if (contentData.title) {
        const titleElement = document.createElement('h2');
        titleElement.textContent = contentData.title;
        container.appendChild(titleElement);
    }
    
    if (contentData.text) {
        const textElement = document.createElement('p');
        textElement.textContent = contentData.text;
        container.appendChild(textElement);
    }
}

// Example 5: Safe form data processing
function processContactForm() {
    const formData = new FormData(document.getElementById('contactForm'));
    const name = formData.get('name');
    const email = formData.get('email');
    const message = formData.get('message');
    
    // Validate inputs
    if (!isValidName(name) || !isValidEmail(email) || !isValidMessage(message)) {
        displayErrorMessage('Please provide valid information');
        return;
    }
    
    // Secure: Create confirmation with DOM APIs
    const confirmationDiv = document.createElement('div');
    confirmationDiv.className = 'confirmation';
    
    const thankYouHeader = document.createElement('h3');
    thankYouHeader.textContent = `Thank you, ${name}!`;
    
    const receivedParagraph = document.createElement('p');
    receivedParagraph.textContent = 'We received your message:';
    
    const messageBlockquote = document.createElement('blockquote');
    messageBlockquote.textContent = message;
    
    const responseParagraph = document.createElement('p');
    responseParagraph.textContent = `We'll respond to ${email} soon.`;
    
    confirmationDiv.appendChild(thankYouHeader);
    confirmationDiv.appendChild(receivedParagraph);
    confirmationDiv.appendChild(messageBlockquote);
    confirmationDiv.appendChild(responseParagraph);
    
    const resultContainer = document.getElementById('result');
    resultContainer.innerHTML = '';
    resultContainer.appendChild(confirmationDiv);
}

function isValidEmail(email) {
    return typeof email === 'string' && 
           /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
           email.length <= 254;
}

function isValidMessage(message) {
    return typeof message === 'string' && 
           message.trim().length > 0 && 
           message.length <= 1000;
}

function displayErrorMessage(message) {
    const errorElement = document.createElement('div');
    errorElement.className = 'error-message';
    errorElement.textContent = message;
    
    const container = document.getElementById('result');
    container.innerHTML = '';
    container.appendChild(errorElement);
}

💡 Why This Fix Works

The vulnerable examples show common innerHTML XSS patterns. The secure versions use textContent, DOM APIs, and sanitization to safely handle user data.

Why it happens

Applications that directly assign user-controlled data to innerHTML without proper sanitization or encoding.

Root causes

Direct innerHTML Assignment with User Input

Applications that directly assign user-controlled data to innerHTML without proper sanitization or encoding.

Preview example – JAVASCRIPT
// User input from form or URL parameter
const userContent = document.getElementById('userInput').value;

// Vulnerable: Direct assignment to innerHTML
document.getElementById('content').innerHTML = userContent;

// Another vulnerable pattern
function displayMessage(message) {
    // Attacker can inject: <img src=x onerror=alert('XSS')>
    document.querySelector('.message').innerHTML = message;
}

Template String Injection

Building HTML strings with user input using template literals or string concatenation before assigning to innerHTML.

Preview example – JAVASCRIPT
function displayUserProfile(userData) {
    const profileHTML = `
        <div class="profile">
            <h2>${userData.name}</h2>
            <p>${userData.bio}</p>
            <img src="${userData.avatar}" alt="Profile">
        </div>
    `;
    
    // Vulnerable: Template injection via innerHTML
    document.getElementById('profile').innerHTML = profileHTML;
}

Dynamic Content Generation

Creating dynamic HTML content from user data without proper escaping before inserting it into the DOM.

Preview example – JAVASCRIPT
function renderComments(comments) {
    let html = '';
    
    comments.forEach(comment => {
        // Vulnerable: Building HTML with unescaped user data
        html += `
            <div class="comment">
                <strong>${comment.author}</strong>
                <p>${comment.text}</p>
                <small>Posted on ${comment.date}</small>
            </div>
        `;
    });
    
    document.getElementById('comments').innerHTML = html;
}

Fixes

1

Use textContent for Text-Only Content

Replace innerHTML with textContent when displaying plain text content to automatically escape HTML entities.

2

Use DOM APIs for Element Creation

Build DOM elements using createElement and other DOM APIs instead of innerHTML to safely handle user data.

3

Implement HTML Sanitization

When HTML content is required, use a trusted sanitization library like DOMPurify to clean user input before inserting it.

4

Use Template Engines with Auto-Escaping

Employ client-side template engines that automatically escape content by default, such as Handlebars or Mustache.

Detect This Vulnerability in Your Code

Sourcery automatically identifies cross-site scripting (xss) from user data in dom innerhtml assignment and many other security issues in your codebase.