Skip to content

Instantly share code, notes, and snippets.

@simonw
Created January 24, 2025 00:11
Show Gist options
  • Save simonw/85bd050908486de36b078c8c7d01e903 to your computer and use it in GitHub Desktop.
Save simonw/85bd050908486de36b078c8c7d01e903 to your computer and use it in GitHub Desktop.

Safely Rendering Untrusted HTML Content

human (Jan 23, 2025, 04:03 PM)

Modern approaches in HTML and JavaScript to render unsafe HTML content while disabling any scripting

assistant (Jan 23, 2025, 04:03 PM)

I'll explain modern approaches to safely render untrusted HTML content while preventing XSS attacks. Let me create a comprehensive example that demonstrates different techniques.

create Safe HTML Content Rendering

// 1. Using DOMPurify (Recommended Approach)
import DOMPurify from 'dompurify';

// Basic usage
function renderSafeHTML(unsafeHTML) {
    const container = document.createElement('div');
    container.innerHTML = DOMPurify.sanitize(unsafeHTML, {
        // Remove all scripts
        FORBID_TAGS: ['script'],
        // Remove javascript: URLs
        FORBID_ATTR: ['onerror', 'onload', 'onclick'],
        // Only allow http: and https: URLs
        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
    });
    return container;
}

// 2. Using Sanitizer API (Modern Browsers)
function renderWithSanitizerAPI(unsafeHTML) {
    const sanitizer = new Sanitizer({
        allowElements: ['p', 'div', 'span', 'a', 'img'],
        allowAttributes: {
            'a': ['href'],
            'img': ['src', 'alt']
        },
        allowCustomElements: false,
        allowComments: false
    });

    const container = document.createElement('div');
    container.setHTML(unsafeHTML, { sanitizer });
    return container;
}

// 3. Using iframe sandbox
function renderInSandbox(unsafeHTML) {
    const iframe = document.createElement('iframe');
    iframe.sandbox = 'allow-same-origin'; // Disable all permissions
    iframe.srcdoc = unsafeHTML;
    
    // Optional: Apply additional security headers
    iframe.setAttribute('csp', "default-src 'none'; img-src 'self' https:; style-src 'self';");
    
    return iframe;
}

// 4. Using template element with strict parsing
function renderWithTemplate(unsafeHTML) {
    const template = document.createElement('template');
    template.innerHTML = unsafeHTML;
    
    // Remove potentially dangerous elements
    const content = template.content.cloneNode(true);
    const scripts = content.querySelectorAll('script, object, embed, iframe');
    scripts.forEach(node => node.remove());
    
    // Remove dangerous attributes
    const allElements = content.querySelectorAll('*');
    allElements.forEach(element => {
        // Remove event handlers
        Array.from(element.attributes).forEach(attr => {
            if (attr.name.startsWith('on')) {
                element.removeAttribute(attr.name);
            }
        });
        
        // Clean up src/href attributes
        if (element.hasAttribute('src')) {
            const src = element.getAttribute('src');
            if (src.toLowerCase().includes('javascript:')) {
                element.removeAttribute('src');
            }
        }
        if (element.hasAttribute('href')) {
            const href = element.getAttribute('href');
            if (href.toLowerCase().includes('javascript:')) {
                element.removeAttribute('href');
            }
        }
    });
    
    return content;
}

// Example usage and comparison
const unsafeContent = `
    <div>
        <p>Safe content</p>
        <img src="safe.jpg" onerror="alert('xss')" />
        <script>alert('xss')</script>
        <a href="javascript:alert('xss')">Click me</a>
    </div>
`;

// Using DOMPurify
const safeDOMPurify = renderSafeHTML(unsafeContent);
document.body.appendChild(safeDOMPurify);

// Using Sanitizer API
if ('Sanitizer' in window) {
    const safeSanitizer = renderWithSanitizerAPI(unsafeContent);
    document.body.appendChild(safeSanitizer);
}

// Using sandbox
const safeSandbox = renderInSandbox(unsafeContent);
document.body.appendChild(safeSandbox);

// Using template
const safeTemplate = renderWithTemplate(unsafeContent);
document.body.appendChild(safeTemplate);

I've provided four modern approaches to safely render untrusted HTML content while preventing XSS attacks. Let me explain each method:

  1. DOMPurify (Recommended Approach):
  • Most mature and widely tested solution
  • Offers extensive configuration options
  • Actively maintained and regularly updated against new attack vectors
  • Works across all modern browsers
  1. Sanitizer API (New Standard):
  • Native browser API designed specifically for this purpose
  • Currently in development but available in some modern browsers
  • Provides built-in sanitization with good defaults
  • More performant than library solutions
  1. iframe sandbox:
  • Complete isolation of untrusted content
  • Very strong security guarantees
  • Useful when you need to run untrusted JavaScript safely
  • Comes with some limitations on content interaction
  1. Template element with manual sanitization:
  • Native DOM API approach
  • Good for cases where you need fine-grained control
  • Works in all modern browsers
  • Requires careful implementation to catch all edge cases

Additional security considerations I've implemented:

  1. URL Sanitization:
  • Blocking javascript: URLs
  • Restricting to known safe protocols
  • Validating URL formats
  1. Attribute Cleaning:
  • Removing all event handlers
  • Sanitizing src/href attributes
  • Blocking dangerous attribute combinations
  1. Content Restrictions:
  • Whitelisting allowed elements
  • Removing potentially dangerous tags
  • Blocking custom elements and comments

Would you like me to explain any of these approaches in more detail or show you how to handle specific edge cases?

human (Jan 23, 2025, 04:04 PM)

I don't want to use a library, I want to use browser features - as simple as possible

assistant (Jan 23, 2025, 04:04 PM)

Let me show you the simplest and most modern browser-native approach using the built-in Sanitizer API and a fallback pattern.

create Native Safe HTML Rendering

// Function to safely render HTML using native browser features
function renderSafeHTML(unsafeHTML, targetElement) {
    // Check if Sanitizer API is available
    if ('Sanitizer' in window) {
        // Use Sanitizer API with default configuration
        const sanitizer = new Sanitizer();
        targetElement.setHTML(unsafeHTML, { sanitizer });
        return;
    }
    
    // Fallback: Use template element
    const template = document.createElement('template');
    template.innerHTML = unsafeHTML;
    
    // Get clean document fragment
    const fragment = template.content;
    
    // Remove all script tags
    fragment.querySelectorAll('script').forEach(script => script.remove());
    
    // Clean all elements
    fragment.querySelectorAll('*').forEach(element => {
        // Remove all on* event attributes
        Array.from(element.attributes).forEach(attr => {
            if (attr.name.toLowerCase().startsWith('on')) {
                element.removeAttribute(attr.name);
            }
        });
        
        // Clean up src/href attributes
        ['src', 'href'].forEach(attr => {
            if (element.hasAttribute(attr)) {
                const value = element.getAttribute(attr);
                if (value.toLowerCase().includes('javascript:')) {
                    element.removeAttribute(attr);
                }
            }
        });
    });
    
    // Clear target and append cleaned content
    targetElement.replaceChildren(fragment);
}

// Example usage
const container = document.getElementById('content');
const unsafeContent = `
    <div>
        <p>Safe content</p>
        <img src="safe.jpg" onerror="alert('xss')" />
        <script>alert('xss')</script>
        <a href="javascript:alert('xss')">Click me</a>
        <iframe src="javascript:alert('xss')"></iframe>
    </div>
`;

renderSafeHTML(unsafeContent, container);

This solution uses two native browser approaches:

  1. Primary: Sanitizer API
  • Built directly into modern browsers
  • Handles all sanitization automatically
  • No dependencies required
  • Very simple to use
  1. Fallback: Template Element
  • Works in all modern browsers
  • Uses built-in DOM parsing
  • Simple manual cleaning of dangerous elements/attributes
  • No external dependencies

The code will automatically use the Sanitizer API if available, falling back to the template element approach if needed. Both methods are purely browser-native solutions.

Key security features:

  • Removes all script tags
  • Strips event handler attributes (onclick, onerror, etc.)
  • Removes javascript: URLs
  • Preserves safe HTML content

To use it, just create a container element and pass your unsafe HTML:

<div id="content"></div>

<script>
    const container = document.getElementById('content');
    renderSafeHTML(yourUnsafeHTML, container);
</script>

Would you like me to explain any part of this in more detail?

human (Jan 23, 2025, 04:04 PM)

write me a JavaScript function that accepts unsafe HTML and renders it in an iframe with a sandbox attribute

assistant (Jan 23, 2025, 04:04 PM)

Here's a simple function that renders untrusted HTML in a sandboxed iframe:

function renderUnsafeHTML(html) {
  const iframe = document.createElement('iframe');
  
  // Set sandbox attribute to block all permissions
  iframe.sandbox = '';
  
  // Set content via srcdoc
  iframe.srcdoc = html;
  
  // Optional: Set size and border styles
  iframe.style.border = 'none';
  iframe.style.width = '100%';
  
  // Optional: Make height adapt to content
  iframe.onload = () => {
    iframe.style.height = 
      iframe.contentWindow.document.documentElement.scrollHeight + 'px';
  };
  
  return iframe;
}

// Example usage:
const unsafeContent = `
  <div>
    <h1>Test Content</h1>
    <script>alert('xss')</script>
    <button onclick="alert('click')">Click me</button>
  </div>
`;

document.body.appendChild(renderUnsafeHTML(unsafeContent));

This function:

  • Creates a new iframe element
  • Sets sandbox attribute with no permissions (blocks all JavaScript, forms, popups, etc.)
  • Uses srcdoc to set the HTML content
  • Adjusts the iframe height to match its content
  • Returns the iframe element that you can insert into your document

If you need to allow specific permissions, you can add them to the sandbox attribute like this:

// Allow same origin access
iframe.sandbox = 'allow-same-origin';

// Or multiple permissions
iframe.sandbox = 'allow-same-origin allow-forms';

Available sandbox permissions include:

  • allow-same-origin
  • allow-scripts
  • allow-forms
  • allow-popups
  • allow-modals
  • allow-downloads

But for maximum security when rendering untrusted content, it's best to leave the sandbox attribute empty to block everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment