悄无声息间,你的 DOM 被劫持了?

文档对象模型(DOM)充当着 HTML 和 JavaScript 之间的接口,搭建起静态内容与动态交互之间的桥梁,对现代 Web 开发而言,DOM 的作用不可或缺。

然而,DOM 也有一个致命的陷阱 —— DOM 劫持。DOM 劫持是指当 HTML 元素与全局 JavaScript 变量或函数产生冲突时,可能会导致 Web 应用程序出现不可预期的行为,甚至产生潜在的安全漏洞。

今天就可大家一起来聊聊 DOM 劫持的问题。

DOM 劫持是怎么发生的?

每个 HTML 元素都可以有一个唯一的 id 或 name 属性,方便在 JavaScript 中引用特定的元素。例如,下面的 HTML 按钮具有一个值为 "myButton" 的 id 属性:

<button id="myButton">Click Me!</button>

我们可以在 JavaScript 代码中使用此 ID 来操作按钮,例如,当点击时改变其文本:

document.getElementById('myButton').onclick = function() {
    this.textContent = 'Clicked!';
};

如果 HTML 元素的 id 或 name 属性与全局 JavaScript 变量或函数冲突会发生什么呢?

当浏览器加载 HTML 页面时,它会自动为 HTML DOM 中的每个 id 和 name 属性创建全局 JavaScript 变量。如果我们有一个名为 “myButton” 的 HTML 元素,浏览器会创建一个全局 JavaScript 变量 myButton,引用该 HTML 元素。

现在,让我们考虑一个场景,其中我们声明了一个名为 myButton 的 JavaScript 函数:

function myButton() {
    // some code
}

但我们还有一个 id 或 name 为 “myButton” 的 HTML 元素。当页面加载时,浏览器的自动进程会引用 HTML 元素并覆盖 JavaScript 函数 myButton

<button id="myButton">Click Me!</button>

console.log(myButton); // This will log the HTML button element, not the function

这个过程叫做 DOM 劫持,它可能会引发不可预测的行为和安全漏洞。如果攻击者能控制这些属性,他们可能有能力向网页注入恶意代码,从而引发包括跨站脚本(XSS)在内的安全问题。

为了说明这一点,让我们考虑以下情景:

Enter your name: <input id="username" type="text">
<button onclick="greet()">Greet</button>

function greet() {
    var username = document.getElementById('username').value;
    alert(`Hello ${username}`);
}

攻击者可以输入类似 <img id='alert' src=x onerror='alert(1)'> 的内容,创建一个 'id' 为 'alert' 的新 HTML 组件。该组件会破坏 JavaScript 中的正常 alert 功能。下次网站尝试使用此功能时,它将无法正常工作,甚至可能运行恶意代码。

我们想象现在有一个带有用户反馈功能的基本 Web 应用程序。用户输入自己的姓名和反馈消息,然后提交。页面显示反馈:

html:

<h2>Feedback Form</h2>
<form>
  <label for="name">Name:</label><br>
  <input type="text" id="name" name="name"><br>
  <label for="feedback">Feedback:</label><br>
  <textarea id="feedback" name="feedback"></textarea><br>
  <input type="submit" value="Submit">
</form>

<div id="feedbackDisplay"></div>

JavaScript:

document.querySelector('form').onsubmit = function(event) {
    event.preventDefault();

    let name = document.getElementById('name').value;
    let feedback = document.getElementById('feedback').value;

    let feedbackElement = document.getElementById('feedbackDisplay');

    feedbackElement.innerHTML = `<p><b>${name}</b>: ${feedback}</p>`;
};

这段代码会获取用户的姓名和反馈,并将其显示在 FeedbackDisplay div 内的段落元素中。

攻击者可以通过在反馈表单中提交一段 HTML 来利用此代码。例如,如果他们在名称字段中输入以下代码并提交表单,则反馈显示区域就会被 Script 替换:

<script id="feedbackDisplay">window.location.href='http://conardli.top';</script>

当表单尝试显示下一条反馈时,就会执行脚本,将用户重定向到恶意网站。这是 DOM 劫持造成严重后果的一个例子 —— 攻击者可以控制用户的浏览器,从而窃取敏感数据或安装恶意软件。

缓解 DOM 劫持的安全编码实践

通过更深入地了解这些漏洞,我们可以继续采取一些最佳实践来减轻 DOM 劫持的风险。

正确定义变量和函数的作用域

DOM 劫持的最常见原因之一是滥用 JavaScript 中的全局作用域。

通过在特定的作用域范围内定义变量和函数,我们可以限制对该范围或任何嵌套范围的覆盖,并最大限度地减少潜在的冲突。

我们来用 JavaScript 的作用域规则并重构前面的示例来展示如何做到这一点:

(function() {
    // All variables and functions are now in this function's scope
    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };
})();

首先我们使用了 DOMPurify 来对上述代码块中的用户输入进行清理。

在此版本的代码中,我们将所有内容都包含在立即调用函数表达式 (IIFE) 中,这会创建一个新作用域。form 和 FeedbackElement 变量以及分配给 onsubmit 事件处理程序的函数不在全局作用域内,因此它们不能被劫持。

使用唯一标识符

确保网页上的每个元素都有唯一的 id 可以降低无意中覆盖重要函数或变量的风险。另外,避免使用通用名称或可能与全局 JavaScript 对象或函数冲突的名称。

避免全局命名空间污染

保持全局命名空间干净是编写安全 JavaScript 的一个重要方面。全局作用域中的变量和函数越多,DOM劫持的风险就越大。使用 JavaScript 的函数作用域或 ES6 的块作用域来保留变量和函数。这是使用后者的示例:

    let form = document.querySelector('form');
    let feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        let name = document.getElementById('name').value;
        let feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        let newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

在这段代码中,我们使用块(由 {} 定义)来创建新作用域。所有变量和函数现在都限制在该块中,并且不在全局作用域内。

正确使用 JavaScript 特性

现代 JavaScript 提供了一些有助于最大限度地缓解 DOM 劫持的风险。特别是 ES6 中引入的 let 和 const 关键字提供了对声明变量的更多控制。

在以前,我们使用 var 关键字声明 JavaScript 变量。var 有一些怪癖,其中之一是就它没有块作用域,只有函数作用域和全局作用域。这意味着用 var 声明的变量可以在声明它的块之外访问和覆盖。

另一方面,let 和 const 都具有块作用域,这意味着它们只能在声明它们的块内访问。这一特性通常使它们成为变量声明的更好选择,因为它限制了覆盖变量的可能性。

我们还可以使用 const 来声明常量 — 分配它们后我们无法更改的值。它们可以防止重要的变量被意外覆盖。

    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

在此代码中,我们将所有 var 的使用替换为 const。我们将所有变量限制在声明它们的块中,并且常量不能被覆盖。

但是 ,使用 let 和 const 并不能完全消除 DOM 劫持的风险,但这种做法仍然是安全编码的一个关键方面。

使用 Devtools 发现潜在的 DOM 劫持风险

例如 Chrome 或 Firefox 中的浏览器开发者工具,也是探测 DOM 劫持漏洞的强大助手。

最简单的方法,我们直接打开 Devtools

然后在控制台输入 window ,这里面包含了网站全局作用域下所有的全局变量和函数。

然后我们检查下是否有任何看起来不合适的变量,尤其是那些与 HTML 元素 id 或 name 同名的变量。

通过 Elements 选项卡,编辑页面的 HTML 来操控 DOM 并测试潜在的漏洞。例如,添加一个 id 与全局变量或函数相匹配的元素,看看是否会被覆写。

最后

大家有什么看法,欢迎来评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值