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.search
innerHTML
The 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 possiblebypassSecurityTrustHtml
v-html
directives