DOM 污染预防备忘单¶
简介¶
DOM 污染是一种代码复用、仅限 HTML 的注入攻击,攻击者通过注入 HTML 元素(其 id
或 name
属性与安全敏感变量或浏览器 API 的名称匹配),混淆 Web 应用程序并覆盖它们的值,例如用于获取远程内容(如脚本 src)的变量。
在无法进行脚本注入时尤其重要,例如当被 HTML 净化器过滤时,或通过禁止或控制脚本执行来缓解时。在这些情况下,攻击者仍然可以将非脚本 HTML 标记注入网页,并将最初安全的标记转换为可执行代码,从而实现跨站脚本 (XSS)。
本备忘单列出了防止或限制 DOM 污染对您的 Web 应用程序影响的指南、安全编码模式和实践。
背景¶
在深入探讨 DOM 污染之前,让我们回顾一些基本的 Web 背景知识。
当网页加载时,浏览器会创建一个 DOM 树,用于表示页面的结构和内容,并且 JavaScript 代码可以读写此树。
在创建 DOM 树时,浏览器还会为 window
和 document
对象上的(一些)命名 HTML 元素创建一个属性。命名 HTML 元素是指那些具有 id
或 name
属性的元素。例如,标记
<form id=x></a>
将导致浏览器为该表单元素在 window
和 document
上创建名为 x
的属性引用。
var obj1 = document.getElementById('x');
var obj2 = document.x;
var obj3 = document.x;
var obj4 = window.x;
var obj5 = x; // by default, objects belong to the global Window, so x is same as window.x
console.log(
obj1 === obj2 && obj2 === obj3 &&
obj3 === obj4 && obj4 === obj5
); // true
当访问 window
和 document
对象的属性时,命名的 HTML 元素引用优先于对内置 API 以及开发人员在 window
和 document
上定义的其他属性的查找,这也被称为命名属性访问。不了解这种行为的开发人员可能会将 window/document 属性的内容用于敏感操作,例如用于获取远程内容的 URL,而攻击者可以通过注入具有冲突名称的标记来利用它。类似于自定义属性/变量,内置浏览器 API 也可能被 DOM 污染所覆盖。
如果攻击者能够将(非脚本)HTML 标记注入 DOM 树,它可能会因为命名属性访问而改变 Web 应用程序所依赖的变量的值,导致其功能异常、敏感数据泄露或执行攻击者控制的脚本。DOM 污染通过利用这种(遗留)行为,在执行环境(即 window
和 document
对象)与 JavaScript 代码之间造成命名空间冲突。
攻击示例 1¶
let redirectTo = window.redirectTo || '/profile/';
location.assign(redirectTo);
攻击者可以
- 注入标记
<a id=redirectTo href='javascript:alert(1)'
并获得 XSS。 - 注入标记
<a id=redirectTo href='phishing.com'
并获得开放重定向。
攻击示例 2¶
var script = document.createElement('script');
let src = window.config.url || 'script.js';
s.src = src;
document.body.appendChild(s);
攻击者可以注入标记 <a id=config><a id=config name=url href='malicious.js'>
以加载额外的 JavaScript 代码,并实现任意客户端代码执行。
指南摘要¶
为方便快速查阅,以下是接下来将讨论的指南摘要。
指南 | 描述 | |
---|---|---|
# 1 | 使用 HTML 净化器 | 链接 |
# 2 | 使用内容安全策略 | 链接 |
# 3 | 冻结敏感 DOM 对象 | 链接 |
# 4 | 验证所有 DOM 树输入 | 链接 |
# 5 | 使用显式变量声明 | 链接 |
# 6 | 不要将 Document 和 Window 用于全局变量 | 链接 |
# 7 | 验证前不要信任 Document 内置 API | 链接 |
# 8 | 强制类型检查 | 链接 |
# 9 | 使用严格模式 | 链接 |
# 10 | 应用浏览器特性检测 | 链接 |
# 11 | 将变量限制在局部作用域 | 链接 |
# 12 | 在生产环境中使用唯一的变量名 | 链接 |
# 13 | 使用面向对象编程技术,例如封装 | 链接 |
缓解技术¶
#1: HTML 净化¶
强大的 HTML 净化器可以防止或限制 DOM 污染的风险。它们可以通过多种方式实现。例如
- 完全移除
id
和name
等命名属性。尽管有效,但这可能会在合法功能需要命名属性时阻碍可用性。 - 命名空间隔离,例如,通过常量字符串作为命名属性值的前缀,以限制命名冲突的风险。
- 动态检查输入标记的命名属性是否与现有 DOM 树发生冲突,如果是,则移除输入标记的命名属性。
OWASP 推荐使用 DOMPurify 或 Sanitizer API 进行 HTML 净化。
DOMPurify 净化器¶
默认情况下,DOMPurify 会移除所有与内置 API 和属性的污染冲突(使用默认启用的 SANITIZE_DOM
配置选项)。
为了同时保护自定义变量和属性不被污染,您需要启用 SANITIZE_NAMED_PROPS
配置。
var clean = DOMPurify.sanitize(dirty, {SANITIZE_NAMED_PROPS: true});
这将通过为命名属性和 JavaScript 变量添加 user-content-
字符串前缀的方式,隔离它们的命名空间。
Sanitizer API¶
新的浏览器内置 Sanitizer API 在其默认设置下不防止 DOM 污染,但可以配置为移除命名属性。
const sanitizerInstance = new Sanitizer({
blockAttributes: [
{'name': 'id', elements: '*'},
{'name': 'name', elements: '*'}
]
});
containerDOMElement.setHTML(input, {sanitizer: sanitizerInstance});
#2: 内容安全策略¶
内容安全策略 (CSP) 是一组规则,告诉浏览器哪些资源被允许加载到网页上。通过限制 JavaScript 文件的来源(例如,使用 script-src 指令),CSP 可以防止恶意代码被注入页面。
注意:CSP 只能缓解 DOM 污染攻击的某些变体,例如当攻击者试图通过污染脚本源来加载新脚本时,但无法缓解当已存在的代码可以被滥用以执行代码的情况,例如,污染 eval()
等代码评估构造的参数。
#3: 冻结敏感 DOM 对象¶
缓解针对单个对象的 DOM 污染的一种简单方法可以是冻结敏感的 DOM 对象及其属性,例如,通过 Object.freeze() 方法。
注意:冻结对象属性可以防止它们被命名的 DOM 元素覆盖。但是,确定所有需要冻结的对象和对象属性可能并不容易,从而限制了这种方法的实用性。
安全编码指南¶
DOM 污染可以通过防御性编程并遵循一些编码模式和指南来避免。
#4: 验证所有 DOM 树输入¶
在将任何标记插入网页的 DOM 树之前,净化 id
和 name
属性(参见HTML 净化)。
#5: 使用显式变量声明¶
初始化变量时,始终使用 var
、let
或 const
等变量声明符,这可以防止变量被污染。
注意:与 var
不同,使用 let
声明变量不会在 window
上创建属性。因此,window.VARNAME
仍然可能被污染(假设 VARNAME
是变量名)。
#6: 不要将 Document 和 Window 用于全局变量¶
避免使用 document
和 window
等对象来存储全局变量,因为它们很容易被操纵。(例如,参见此处)。
#7: 验证前不要信任 Document 内置 API¶
Document 属性,包括内置属性,总是会被 DOM 污染所覆盖,即使在它们被赋值之后也是如此。
提示:这是由于所谓的命名属性可见性算法,其中命名 HTML 元素引用优先于对 document
上的内置 API 和其他属性的查找。
#8: 强制类型检查¶
在敏感操作中使用 document
和 window
属性之前,始终检查它们的类型,例如,使用 instanceof
运算符。
提示:当一个对象被污染时,它将指向一个 Element
实例,这可能不是预期的类型。
#9: 使用严格模式¶
使用 strict
模式来防止意外的全局变量创建,并在尝试覆盖只读属性时抛出错误。
#10: 应用浏览器特性检测¶
不要依赖于浏览器特定的特性或属性,而应使用特性检测来确定某个特性是否受支持,然后再使用它。这有助于防止在不支持的浏览器中使用这些特性时可能出现的错误和 DOM 污染。
提示:在不支持的浏览器中,不受支持的特性 API 可以充当未定义的变量/属性,使其可被污染。
#11: 将变量限制在局部作用域¶
全局变量更容易被 DOM 污染覆盖。尽可能使用局部变量和对象属性。
#12: 在生产环境中使用唯一的变量名¶
使用唯一的变量名可能有助于防止命名冲突,从而导致意外的覆盖。
#13: 使用面向对象编程技术,例如封装¶
将变量和函数封装在对象或类中可以帮助防止它们被覆盖。通过将它们设为私有,它们无法从对象外部访问,从而使它们更不容易受到 DOM 污染的影响。