Search results will appear here...
DOM-based vulnerabilities occur when client-side JavaScript dynamically manipulates the Document Object Model (DOM) in an unsafe way, allowing attackers to execute code in the user's browser context.
DOM-based vulnerabilities are a type of client-side security issue where the vulnerability exists in the client-side code (JavaScript) rather than in the server-side code. These vulnerabilities occur when JavaScript code takes user-controllable data (like URL parameters, form inputs, or messages from other windows) and uses it in unsafe ways.
| Characteristic | Reflected/Stored XSS | DOM-based XSS |
|---|---|---|
| Attack Vector | Server-side processing of user input | Client-side processing of user input |
| Server Involvement | Server includes malicious content in response | Server may never see the malicious payload |
| Detection | Visible in HTTP responses | Only visible in client-side execution |
| Mitigation | Server-side input validation and output encoding | Client-side input validation and safe DOM manipulation |
This demo shows how unsafe handling of URL parameters can lead to DOM-based XSS vulnerabilities.
Search results will appear here...
When the vulnerable search function is used:
location.searchinnerHTMLThe attack doesn't involve the server at all - it happens entirely in the browser's DOM.
The protected search function implements these safeguards:
encodeURIComponent() when adding parameters to URLstextContent instead of innerHTML to insert text safely
// Safe DOM manipulation
function displaySafeSearchResults(term) {
const resultElement = document.getElementById('searchResults');
const sanitizedTerm = document.createElement('span');
sanitizedTerm.textContent = term; // Safely handles all characters
resultElement.innerHTML = ''; // Clear previous content
resultElement.appendChild(document.createTextNode('You searched for: '));
resultElement.appendChild(sanitizedTerm);
}
This demo shows how improper handling of URL redirections can lead to dangerous DOM-based open redirect vulnerabilities.
Imagine you've just logged in and should be redirected to the page you were trying to access:
This would redirect to:
When the vulnerable redirect function is used:
location.href or used in window.open() without validationjavascript: protocol, it will execute arbitrary JavaScriptOpen redirects are commonly used in phishing attacks to make malicious URLs appear legitimate or to execute JavaScript.
// Safe redirect function
function safeRedirect(url) {
// Only allow relative URLs or specific domains
if (url.startsWith('/') || url.startsWith('https://trusted-domain.com')) {
// Additional validation for relative URLs
if (url.startsWith('/')) {
// Make sure there's no "/../" path traversal
if (!/\/\.\.\//.test(url)) {
location.href = url;
return true;
}
} else {
location.href = url;
return true;
}
}
// Block the redirect and show an error
console.error("Redirect blocked: Invalid destination");
return false;
}
This demo shows how improper validation of postMessage data can lead to DOM-based vulnerabilities.
This simulates communication between a parent page and an iframe:
When the vulnerable postMessage handler is used:
window.postMessage()postMessage vulnerabilities are particularly dangerous in complex web applications that use iframes or third-party integrations.
// Safe message event listener
window.addEventListener('message', function(event) {
// 1. Validate the origin
if (!/^https:\/\/(trusted-domain\.com|app\.trusted-domain\.com)$/.test(event.origin)) {
console.error('Message from untrusted origin rejected:', event.origin);
return;
}
// 2. Validate message structure
try {
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (!data || typeof data !== 'object' || !data.type) {
console.error('Invalid message format');
return;
}
// 3. Process based on validated message type
switch (data.type) {
case 'TEXT_MESSAGE':
if (typeof data.text !== 'string') return;
displayTextMessage(data.text); // Uses textContent, not innerHTML
break;
case 'UI_UPDATE':
if (!data.elementId || typeof data.elementId !== 'string') return;
if (!data.property || typeof data.property !== 'string') return;
if (data.value === undefined) return;
// Only allow updates to specific elements and properties
const allowedUpdates = {
'status-display': ['textContent', 'className'],
'progress-bar': ['style.width', 'aria-valuenow']
};
if (!allowedUpdates[data.elementId] ||
!allowedUpdates[data.elementId].includes(data.property)) {
console.error('Unauthorized element or property update');
return;
}
updateElement(data.elementId, data.property, data.value);
break;
default:
console.error('Unknown message type:', data.type);
}
} catch (e) {
console.error('Error processing message:', e);
}
});
Instead of vulnerable methods, use safer alternatives:
textContent instead of innerHTML when adding text contentcreateElement() and appendChild() instead of document.write()eval(), setTimeout()/setInterval() with string arguments, and new Function()
// Unsafe way
element.innerHTML = userControlledValue;
// Safe way
element.textContent = userControlledValue;
// When HTML is needed:
const sanitizedHTML = DOMPurify.sanitize(userControlledValue);
element.innerHTML = sanitizedHTML;
When you need to set HTML content, use a sanitization library like DOMPurify:
// Include the library
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>
// Use it to sanitize HTML before insertion
const userHTML = getParameterFromUrl('content');
const clean = DOMPurify.sanitize(userHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target']
});
element.innerHTML = clean;
Always validate user input on the client side (in addition to server-side validation):
// Validate URL parameters before use
function getValidatedParameter(param, pattern) {
const value = new URLSearchParams(window.location.search).get(param);
if (!value) return null;
// Check against a whitelist pattern
if (pattern && !pattern.test(value)) {
console.error(`Invalid value for parameter ${param}`);
return null;
}
return value;
}
// Example usage
const userId = getValidatedParameter('userId', /^[0-9]{1,10}$/);
if (userId) {
loadUserProfile(userId);
}
Use proper URL encoding/decoding functions:
// When adding parameters to URLs
const params = new URLSearchParams();
params.append('search', userInput); // Automatically handles encoding
// Creating a URL with parameters
const url = new URL('/search', window.location.origin);
url.searchParams.append('term', userInput);
history.pushState({}, '', url);
// When extracting URL parameters
const urlParams = new URLSearchParams(window.location.search);
const term = urlParams.get('term'); // Automatically handles decoding
Use CSP to restrict script execution and prevent many DOM-based attacks:
// Add this header to your server responses
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
Always validate the origin and message structure:
// Sending messages
targetWindow.postMessage({
type: 'USER_ACTION',
action: 'save',
data: { id: 123, name: 'Example' }
}, 'https://trusted-target.com');
// Receiving messages
window.addEventListener('message', function(event) {
// Always verify the origin
if (event.origin !== 'https://trusted-source.com') {
console.error('Message from untrusted origin:', event.origin);
return;
}
// Validate the message structure
try {
const data = typeof event.data === 'object' ? event.data : JSON.parse(event.data);
if (!data.type || typeof data.type !== 'string') {
throw new Error('Invalid message format');
}
// Process message based on type
switch (data.type) {
case 'USER_ACTION':
handleUserAction(data);
break;
// Other cases
default:
console.error('Unknown message type');
}
} catch (e) {
console.error('Invalid message:', e);
}
});
Follow security best practices for your framework:
dangerouslySetInnerHTML when possiblebypassSecurityTrustHtmlv-html directives