DOM Clobbering Anti-Pattern
Severity: Medium
Summary
DOM Clobbering overwrites global JavaScript variables via attacker-controlled HTML. Browsers auto-create global variables from id and name attributes. Enables logic bypasses, XSS, and security control evasion. Bypasses HTML sanitizers that allow id and name.
The Anti-Pattern
Application JavaScript relies on global variables that HTML injection overwrites. Code expects legitimate objects but receives DOM element references instead.
BAD Code Example
// VULNERABLE: Using a global variable that can be clobbered.
// Imagine this HTML is injected into the page by an attacker:
// <div id="appConfig"></div>
// The application code expects `appConfig` to be a configuration object.
// However, `window.appConfig` now points to the <div> element above.
if (window.appConfig.isAdmin) {
// A DOM element is "truthy", so this check passes.
// The attacker gains access to the admin panel without being an admin.
showAdminPanel();
}
// Another example:
// Injected HTML: <form id="someForm" action="https://evil-site.com">
// Legitimate button: <button onclick="submitForm()">Submit</button>
function submitForm() {
// The code intends to get a legitimate form, but gets the injected one.
var form = document.getElementById('someForm');
form.submit(); // Submits data to the attacker's site.
}
GOOD Code Example
// SECURE: Avoid global variables and validate DOM elements.
// 1. Use a namespace for your application's objects.
const myApp = {};
myApp.config = {
isAdmin: false
// ... other config
};
// Access the configuration through the namespace.
if (myApp.config.isAdmin) {
showAdminPanel();
}
// 2. Validate the type of element retrieved from the DOM.
function submitForm() {
var form = document.getElementById('someForm');
// Check that the element is actually a form before using it.
if (form instanceof HTMLFormElement) {
form.submit();
} else {
console.error("Error: 'someForm' is not a valid form element.");
}
}
// 3. Freeze critical objects to prevent modification.
Object.freeze(myApp.config);
React Example
BAD:
// VULNERABLE: Accessing window global that can be clobbered
function AdminPanel() {
// Attacker injects: <div id="appConfig"></div>
// window.appConfig now points to the div, not the config object
if (window.appConfig?.isAdmin) {
return <AdminControls />;
}
return <AccessDenied />;
}
GOOD:
// SECURE: Use React Context or module scope
import { useContext } from 'react';
import { ConfigContext } from './ConfigContext';
function AdminPanel() {
const config = useContext(ConfigContext);
if (config.isAdmin) {
return <AdminControls />;
}
return <AccessDenied />;
}
TypeScript Example
GOOD:
// Type safety prevents DOM clobbering
interface AppConfig {
isAdmin: boolean;
apiUrl: string;
}
// Module-scoped, not global
const appConfig: AppConfig = {
isAdmin: false,
apiUrl: '/api'
};
// TypeScript enforces type checking
function checkAdmin(): void {
// Even if window.appConfig exists, TypeScript requires the interface
if (appConfig.isAdmin) { // Type-safe access
showAdminPanel();
}
}
// Validate DOM elements with type guards
function submitForm(formId: string): void {
const elem = document.getElementById(formId);
if (!(elem instanceof HTMLFormElement)) {
throw new TypeError(`Element ${formId} is not a form`);
}
elem.submit();
}
Detection
JavaScript Patterns:
- Global variable access:
window.config,document.userInfo - Implicit globals:
config.isAdmin(no var/let/const declaration) document.getElementById()without type validation- Security checks on globals:
if (window.auth.isLoggedIn)
Search Patterns:
- Grep:
window\.[a-zA-Z]+\.|\bdocument\.[a-zA-Z]+\.|getElementById\(.*\)\.(?!tag|class) - Look for:
if (window.orif (document. - Check: Direct property access without instanceof check
HTML Sanitizer Review:
- DOMPurify: Check
FORBID_ATTRdoesn't excludeid,name - Sanitize-html: Review
allowedAttributesconfig - HTML Purifier: Verify attribute whitelist
Manual Testing:
- Identify global variables: Check
windowobject in console - Inject clobbering HTML:
<div id="globalVarName"></div> - Verify override:
console.log(typeof window.globalVarName) - Test security impact: Try bypassing checks
Prevention
- Avoid using global variables for security-critical operations or configurations. Instead, use a private namespace (e.g.,
const myApp = { config: { ... } };). - Freeze critical objects using
Object.freeze()to make them read-only, preventing them from being overwritten. - Validate the type of any object retrieved from the DOM before using it (e.g.,
if (elem instanceof HTMLFormElement)). - Use a robust HTML sanitizer, but be aware that allowing
idandnameattributes still leaves you vulnerable. DOM Clobbering protection must be implemented in your JavaScript code. - Use prefixes for element IDs that are unlikely to collide with global variables (e.g.,
id="myapp-user-form").
Testing for DOM Clobbering
Manual Testing:
- Identify globals: Open browser console, type
window.and check autocomplete - Test clobbering: Inject
<div id="targetGlobal"></div>via input fields - Verify type change:
typeof window.targetGlobalshould show object → object (Element) - Test bypass: Check if security checks fail (admin access, auth bypass)
Automated Testing:
- Static Analysis: ESLint plugin
eslint-plugin-no-unsanitized - Dynamic Testing: Burp Suite DOM Clobbering scanner
- Code Review: Search for
window.access patterns - Type Checking: TypeScript strict mode catches many cases
Example Test:
// Test that critical objects cannot be clobbered
describe('DOM Clobbering Protection', () => {
it('should prevent config object clobbering', () => {
// Attempt to clobber
const div = document.createElement('div');
div.id = 'appConfig';
document.body.appendChild(div);
// Verify original object intact
expect(typeof myApp.config).toBe('object');
expect(myApp.config.isAdmin).toBe(false);
// Cleanup
document.body.removeChild(div);
});
it('should validate form element types', () => {
// Create fake form
const div = document.createElement('div');
div.id = 'loginForm';
document.body.appendChild(div);
// Should throw or return false
expect(() => submitForm('loginForm')).toThrow(TypeError);
});
});
Browser DevTools Check:
// Run in console to find potential clobbering targets
Object.keys(window).filter(key =>
typeof window[key] === 'object' &&
window[key] !== null &&
!window[key].toString().includes('[native code]')
);
Remediation Steps
- Identify global dependencies - Search for
window.and implicit globals - Audit HTML sanitizer - Check if
idandnameare allowed - Create namespace - Move globals to module scope or namespace object
- Add type validation - Check
instanceofbefore using DOM elements - Freeze critical objects - Use
Object.freeze()on config objects - Update code - Replace
window.foowithmyApp.foo - Test protection - Verify clobbering attempts fail
- Enable TypeScript - Leverage type safety for additional protection
Related Security Patterns & Anti-Patterns
- Cross-Site Scripting (XSS) Anti-Pattern: DOM Clobbering can be a vector to enable XSS.
- Mutation XSS Anti-Pattern: Another sanitizer-bypass technique that abuses the way browsers parse HTML.
- Missing Input Validation Anti-Pattern: Failing to validate the type of a DOM element is a form of missing input validation.