Electron应用的安全性研究及一种管控DOM功能的编程方法
研究背景
为什么要做这个调研
由于 Electron 应用基于 HTML+JS 技术,它继承了网络编程中的安全挑战。
- XSS 和无脚本攻击等问题在 HTML+JS 环境中由来已久,这为本地机器攻击提供了新途径。
- 桌面应用功能复杂,有不同的安全和隐私需求,其安全边界与网站、网页应用等不同,传统基于同源策略的安全边界在桌面应用中常不适用。
调研创新点
- 分析12个开源代码的安全性
- 轻量化半自动地分析70个程序的安全性
【与DOM树的关联】
在应用程序运行时,所有预期的变化都发生在这组 DOM 树中。当这组 DOM 树中的某一个 DOM 树变异成组外的另一个 DOM 树,且后者具有该组中任何 DOM 树都没有的额外功能时,攻击就发生了。安全编程的目标就是抵御这种使 DOM 树获得额外功能的变异。
【新方法】
DOM 树类型检查。(类似于C++/TS等带类型的类型检查)我们引入了一个新概念 —— DOM 树类型,以保障程序的执行安全。
利用对象的样本值来构造一个类型,以捕获程序员关于对象的意图,从而如果DOM对象在实际执行过程中发生了偏离意图的变异,则提出了类型违背。
【新框架】
- TypeBuilder:生成构建DOM文档树类型
- TypeEnforcer:监测DOM树篡改
相关工作
相关概念
- 跨站脚本攻击XSS:攻击者的数据作为脚本执行的所有类型的输入验证bug。
- 无脚本攻击:攻击者利用 HTML 和 DOM 的特性,将有害的非脚本元素作为有效负载。比如,精心构造恶意的 HTML 标签或属性,这些元素本身不是脚本,但能在 DOM 树中引发安全问题。(例如插入
<img src="恶意链接" onerror="恶意代码">
)两种都被叫做负载注入漏洞
其他相关研究
要解决 HTML + JS 中有效负载注入漏洞问题,深入研究不同应用类别的特性是关键所在。
- PhoneGap 框架:针对移动应用,静态代码分析技术。对于每个输入通道,该技术构建一个JavaScript程序切片,并进行污点分析,以确定输入是否可以流入敏感的API
- 针对Eval函数的分析:静态地计算模块中所有可能流向eval或exec调用的字符串,使用运行时检查对调用进行插桩
Electron应用的特点
- 开发者很少使用eval,所以更关注DOM树即可
- 较少受到反射型XSS影响:
- 反射型XSS:当用户访问包含恶意脚本的链接时,服务器将恶意脚本作为响应内容返回给用户浏览器,浏览器会在当前页面执行这些脚本,从而导致用户信息泄露或遭受其他攻击
- Electron App 不像普通网站那样频繁地进行页面导航,它不会因为用户的某些操作而跳转到一个全新的、可能包含恶意脚本的页面
- DOM树较为稳定
- Electron类似于SPA(单页面应用)。初次渲染后,后续仅部分UI进行改动。
- DOM 树的整体结构不会发生频繁的大幅度改变,这种预期的稳定形状为 “DOM 树类型” 的概念提供了基础。
- 从面向输入的分析转为面向结果的分析
- 污点分析(Taint Analysis)在 PhoneGap 移动应用中有效,因为其非传统输入渠道(如条码、短信等)相对明确
- Electron 应用逻辑复杂(数据流复杂)、输入渠道复杂(同时接收非传统(如文件系统、SQL 查询)和传统(如网络请求)输入,难以用统一粒度定义污点规则)。因此不适用于污点分析
- 因此,本文方法从输入分析转向 DOM 树结果分析,通过运行时类型强制(DOM-tree type enforcement)实现以下目标:
- 聚焦最终影响:无论输入来源如何,只关注其最终导致的 DOM 树结构是否符合预期。
- 动态验证:通过实时检查 DOM 树的变异是否违反预定义类型,而非静态推测所有可能路径。
调查发现
针对Electron可能漏洞的一些调查
数据清理的挑战
数据清理就是找到那些有漏洞的脚本、dom等阻止其渲染、请求、执行
- 自定义子语法的分析困难:
- 解析是漏洞清理器(sanitizer)中复杂性的主要来源。若解析存在缺陷,危险的攻击载荷(payload)可能因被错误解析而未被检测到。
- 应用程序需要从多个位置获取用户输入以修改 DOM 元素。在每个位置,程序员都需要为特定的子语法编写解析器,这不仅包括 HTML 子集的子语法,还涉及 CSS、Markdown、shell 命令、文件路径等其他语言。
- 路径爆炸问题:
- 很难预料攻击者的数据可能会流经哪些路径。一些需要清理的数据可能会经过一条没有任何清理逻辑的路径。
- “反清理”(de-sanitization)漏洞:
- 这类漏洞源于跨团队对应用自身清理逻辑的误解。例如,一个应用模块执行的清理操作可能会被另一个模块撤销。
案例分析
Microsoft Teams
- 漏洞类型
- 用户追踪漏洞
- 伪造消息注入漏洞
- 技术细节
- Teams本身的清理器:
- 客户端清理器
- 功能
- img的src若是地址,则要求在白名单内
- 使得连接标签在新窗口打开,并删除ref(来源)
- 客户端移除CSS url属性/attr属性等
- 特殊编码转换
- …
- 功能
- 服务端清理器
- 过滤HTML标签等
- 它不删除CSS中的注释,也不强制CSS属性名的白名单。
- 客户端清理器
- CSS背景图用户追踪消息
- 危害
- 不允许从任意网站加载图像。否则,攻击者可以发送包含可见或不可见图像的消息来跟踪聊天中的其他用户,每当图像被加载或重加载时,用户的IP地址都会被泄露。
- 具体实现
- 服务端:把
test1 ; back... test2:
当作一个字符串,因此不会处理内部的url - 客户端:以
;
作为切割,移除掉无用的属性test1、test2,同时删除注释/**/
,还原了url。造成攻击
- 危害
- 伪造消息注入漏洞
- 背景
- 确保消息的外观不会误导其他用户。例如,如果一个消息的
z-index
设置为大于0的值,并且它的位置属性设置为固定的,它可以占据整个聊天窗口并且不透明地覆盖在所有其他消息之上。
- 确保消息的外观不会误导其他用户。例如,如果一个消息的
- 具体实现
- 传统的增加类名、添加style会被清理者清除
- 通过消息后处理模块(发送消息后页面处理@、附件等的工具组件)的scheme,Teams能够推断消息的项目类型(引用?回复?),并根据不同模式对消息进行差异化改写。怎么改写的呢:该模块将其
value
属性中的字符串直接赋值给class
属性,从而导致漏洞。
- 背景
- Teams本身的清理器:
GraSSHopper
- 通过选定文本进行脚本注入:
- 最终效果
<iframe srcdoc="<script nonce=' TuOfzyEnqua4UQ=='>alert(window.parent. document.body.innerHTML)</script>">
- 可通过零宽字符、颜色干扰使用者,一旦点击就会被攻击
- 主机名注入:
- 当用户被诱使复制下面的” SSH连接字符串”,并将其粘贴到主机名框中,并启动SSH连接时,可以利用第二个漏洞。当用户将鼠标移到选项卡上方时,弹出popup(html)标识当前主机名,但主机名是恶意脚本,因此被攻击。
ssh.org:connection=.<iframe srcdoc="<script nonce='fCRqK3cHTuOfzyEnqua4UQ=='>window. alert(window.parent.document.body.innerHTML) </script>">.nonexistent.com
- 两个注入中nounce的说明
- GraSSHopper 采用了 CSP 策略 [8],禁止不安全的内联内容(如执行
onerror
事件的<img src="x" onerror="...">
),并将script-src
限制在带有随机数(nonce)值的白名单中。这意味着只有携带白名单随机数的脚本才能被应用执行。 - 但是对于传统的web服务程序,可以通过请求方式获取,服务器动态生成,每次用户访问页面时刷新。而对于Electron应用,CSP 及其随机数值被硬编码在客户端程序中,攻击者可通过逆向工程获取这些固定值。
- GraSSHopper 采用了 CSP 策略 [8],禁止不安全的内联内容(如执行
VSCODE
由于文本编辑是主要的UI功能,从用户内容构建HTML字符串的情况极为有限。因此,该应用被设计为只解析一少部分markdown转换为html。但仍然支持
<img>
标签
- 提取URL作为markdown
[Follow Link](https://example.com\()) (ctrl + click)
- 反斜杠会被转义,然后
)
就和最前面的匹配了,所以https: //d1qm7r09oiybbo.cloudfront.net/minion.png)
也会被正常渲染
- 代码注释转译为markdown
- 当鼠标悬停在变量名上,vscode弹出弹出框显示注释信息。
- 可能的情况是:外部图像加载到注释的markdown渲染到hmtl上时,IP地址暴露
其他可能注入的路径
如何解决
【引论】
数据清理往往由字符串转换、字符串替换、正则表达式、字符状态机或HTML / CSS令牌、子语法分析等多个步骤组成。
数据清理挑战的根本原因在于它需要程序员预料到未曾预料到的情况,即枚举所有与程序员意图相反的奇异输入数据。我们认为,一个安全的程序设计方法应该只依赖于程序员正确地表达他/她的意图,而不是对它的否定。
“类型”是一种表达数据和对象意图的机制,可以应用于基于HTML的安全。数据不能成为非预期的类型对象。
- 两个目标
- 使程序员能够表达预期的DOM文档树;
- 在运行时,禁止任何导致非预期DOM文档树的变异。
架构
- TypeBuilder帮助程序员创建DOM文档树类型的开发工具
- TypeEnforcer使用DOM文档树类型来保障应用程序的实际运行,并将任何非预期的变异转化为类型违反。
- DOM Interceptor的共享组件负责拦截所有的DOM文档树变化事件。它由Chromium的Blink渲染引擎中定义的多个钩子函数组成。
在开启类型构建器之后,程序员应该尽可能全面地体验应用程序的各项功能,以此来对其进行测试。在测试运行期间,类型构建器会监控 DOM 树,以构建出 DOM 树类型。直到覆盖所有的例子
结构定义
- 由于一个应用程序的初始 DOM 树是在本地构建的,攻击者在页面加载期间没有机会注入脚本元素。
- 对于一个脚本元素,根据HTML标准[ 12 ],它的脚本文本在页面加载时只执行一次。在此之后,如果更改脚本文本,或者向DOM树中注入新的脚本元素,则完全没有效果。
- 子元素:子元素是去重的
- 文本:不包括文本节点(因为文本内容不会被注入)
- 标识符:每个标签由一个去重的标识符,比如下面的
sidebar
和content
。其子元素就不需要标识符了 - 属性:DOMtree有属性的映射,每个属性有一组值。
- 对于URL:例如下图,属性为
文件路径
或者https://foo.com
这一起源点 - 对于脚本:JavaScript 标记序列,比如
Ident Lparen Num Rparen
,标识函数名(数字)
这类型的js代码。所以switchTo(1);alert(99)
这个脚本不合法,因为中间不是Num
- 对于URL:例如下图,属性为
- Style标签:样式属性值具有一种类型,并且有些属性接受多种类型的值
- 对于每一个样式属性,我们会维护一个字符串值的集合、一个用于 URL 值的来源集合,以及一个针对数值的范围(即一个最小值和一个最大值)
样式属性是通过复杂的 “层叠” 算法计算得出的。这个算法会综合考虑多个因素,包括全局样式(比如外部的
.css
文件或者页面中的<style>
元素定义的样式)、局部样式(元素自身的style
属性)、其他属性(像id
、class
等)以及元素父元素的样式等。
回顾一下第三节 B2 部分中提到的虚假聊天消息漏洞,尽管攻击者无法直接控制任何元素的style
属性,但他仍然可以从全局样式表中引入一个样式属性I (z-index = 9)
。换句话说,一个可靠的防御机制不应该禁止“z-index = 9”
成为合法的全局样式属性,而应该仅在该属性被附加到特定元素上时检测到违规情况。注意:布局相关的属性(除了z-index)外,不受DOM树监控,因为他们不是有效的注入点
DOM树构建
TypeBuilder对DOM树的自动更新
DOM树的一些变种只有在程序员测试某些特征时才能被触发。TypeBuilder通过监视DOM树的变化来工作,并扩展DOM文档树类型来表示以前未见过的变化。
情况1:原本有dog,现在新增cat
类型构建器只会将之前未见过的部分添加到 DOM 树类型中,也就是一个<p>
元素、<img>
元素的onclick
属性,以及其src
属性的一个新值。
情况2:情况1基础上,新增cow
虽然没有了<h1>
和<p>
但是他符合DOMtree,所以DOMTree不作任何变更。
【DOM树更新规则】
DOM树仅将这些子树分解为单个元素和属性,然后将它们合并到 DOM 树类型中。所以即便其他元素不存在也没关系。
类型构建器只会向 DOM 树类型中添加内容,而从不删除内容
类型构建器(TypeBuilder)还会观察到其他类型的文档对象模型(DOM)树变化,包括:(1)元素替换,(2)元素移除,(3)属性修改以及(4)样式重新计算。
程序员微调
- Attribute - value -通配符:
- 使用通配符’ ? ‘和’ * ‘来匹配属性值中的任何字符或字符串
- 场景:随机id、增量id(例如
v-for
,就可以写成< span id = ' item * ' >
)
- 子树扁平化(结构无关子树)
- 场景:在文章阅读器和标记编辑器等应用中,用户的丰富格式内容通常显示在一个内容区域中,该区域是一个专用的子树。
- 扁平化的子树相当于白名单
- 如果div元素被标记为平坦的,那么h1,h2,u和i元素的任何组合和嵌套水平都被认为是合法的。
- 如果div元素被标记为平坦的,那么h1,h2,u和i元素的任何组合和嵌套水平都被认为是合法的。
TypeEnforcer类型监测
TypeEnforcer在检测缺失元素、缺失属性或属性缺失值时,拒绝了DOM文档树的改变,并抛出异常。
核心要点:
- 拦截DOMtree的变化。
- 如果拒绝变更,则应全面收回,不至于造成持续性的影响。
【简单版:如何监听变化】
浏览器渲染内核:
- “Node”(节点)是 DOM 树中所有对象的基类。
- “SetComputedStyle”(设置计算样式)方法是在样式重新计算后更新节点样式的唯一接口。
- “ContainerNode”(容器节点)定义了一个可能有子节点的节点,即一个非叶节点。
- “Element”(元素)从 “ContainerNode” 派生而来,并保存着该元素的属性集。
- “InsertBefore”(在…… 之前插入)、“AppendChild”(追加子节点)和 “ReplaceChild”(替换子节点)方法是所有节点插入事件的关键节点。
- 在每次属性修改事件发生之前,都会调用 “WillModifyAttribute”(即将修改属性)方法。
DOM Interceptor通过监听/拦截这5种hook方法实现。
【但是】
但是,不能完全依靠“变更观察器(MutationObserver)” 应用程序编程接口(API)[14] ,因为在DOM树修改期间也可能会有一些潜在的影响 (持续影响)。
1 | 修改前--->修改期间---->即将修改完毕,调用hooks---->挂载到DOM树,修改完毕 |
会造成潜在影响的Document API包括——
- 文件系统访问:如读取或写入本地文件;
- 网络请求:如发起 HTTP 请求获取数据;
- 事件注册:如绑定事件监听器到元素上;
- 存储操作:如使用
localStorage
或IndexedDB
保存数据; - 脚本执行:如动态加载并执行 JavaScript 代码。
因为我们有类型监测,只要他被挂载到DOM树,这些Document API就是白名单,但一旦这些API在修改期间运行,未进行监测,那就是潜在的威胁!因此,我们重点就是防止在DOM修改期间,这些API被调用了。
【一些概念】
一个元素总是属于一个文档Document,但并不总是连接到一个DOM树。
所以那些未在白名单的元素,都是游离态的
我们只需要防范游离态元素在被挂载到DOM 树前调用可能有潜在影响的Document API就行。
- 断开连接的元素(游离态)
- 一些元素可能不在DOM树中(游离节点),有可能
- 元素初始创建的时候,都是游离态,最后才通过addChild、insertBefrom等方法将其连接到DOM树
- 游离的树:当元素foo的内部HTML属性发生改变时。Blink将新的内部HTML值解析为这样一棵单独的树。解析完成后,将单独的树连接到DOM树中,作为元素foo下的子树。
- 一些元素可能不在DOM树中(游离节点),有可能
- 元素修改及其影响
- 元素可以通过( 1 )插入或移除子元素;( 2 )从父元素中插入或移除;( 3 )修改属性。进行修改
- 这些修改都可能引发一些操作
- 例如,当一个元素的src属性被设置为新的值时,它会立即触发一个网络请求,当请求完成后,它可能会进一步触发其onload或onerror事件处理程序。
【最终解决方案:延迟断开元素的持续影响】
本质:使用DOM树类型为元素的决策提供上下文。
- 通过 DOM 树类型记录元素在树中的具体位置,并基于位置做出决策。
- 允许某个特定位置的
<img>
元素加载foo.com
的图片(因为该位置在训练时被标记为合法);- 禁止其他位置的
<img>
元素加载相同来源(因为该位置未被训练数据覆盖)。
- 只有连接到树时才能决策/判断/拦截:
只有当元素被插入树中时,才需要根据其位置和上下文(即 DOM 树类型)进行实时验证。
- 🌟🌟🌟🌟针对游离态元素:延迟影响直到连接:
对于那些潜在影响,重点就是预防其在被插入DOM树之前(游离态)发生影响 (因为插入后,就已经验证了type正确。) ,解决方式是延迟影响至连接完毕(连接完毕也就确认了type正确)。
- 情景1:隐式创建的未连接元素(通过 innerHTML 解析)
1
div.innerHTML = '<img src="http://example.com">';
此时 <img>
元素尚未被插入 DOM 树,但可能已经触发了网络请求(持续性影响/潜在影响)。
解决方案:延迟所有持续性影响(如网络请求),直到元素被插入 DOM 树。这样,即使元素被提前创建,其影响也会在安全验证后(通过 TypeEnforcer)才被执行。许多持续性影响(如网络请求)本身就是异步的,延迟执行不会改变其最终结果。
- 情景2:显式创建的未连接元素(程序员主动创建)
1 | const img = document.createElement('img'); img.src = 'http://example.com'; // 等待图片加载完成 |
这个做法并不常见。
场景:程序员可能显式创建一个未连接的元素,并在连接前等待其持续性影响完成:例如,等待网络加载完毕后,才绘制图片,以便用户体验好
解决方案:即使程序员显式等待,延迟机制仍然有效。因为持续性影响会被推迟到元素连接时执行,确保所有操作在安全上下文中进行。此外,更好的做法是使用 CSS 的 visibility: hidden
或 display: none
控制元素显示,而非依赖异步等待。
- 情景3:永不连接的未连接元素
程序员可能创建一个元素但永远不将其插入 DOM 树:
1 | const dummy = document.createElement('div'); dummy.style.display = 'none'; // 永不连接 |
场景:这对程序员来说是不合理的场景!因为效果可以简单地通过JavaScript来实现,而不需要使用断开连接的元素。
【进一步拓展研究】
- 子类可重写这些虚方法以实现特定功能。例如:
<img>
重写ParseAttribute
以处理src
属性变化时的网络请求。HTMLElement
重写ParseAttribute
以注册通用事件处理程序(如onclick
)。- 在 121 个
HTMLElement
子类中,53 个重写了至少一个虚方法,这些方法是持续性影响的潜在触发点。
元素类型 | 触发的持续性影响 |
---|---|
<body> , <input> , <iframe> , <portal> |
注册事件处理程序(如onclick ) |
<img> , <video> , <audio> , <source> , <track> |
请求文件 / 网络资源(如加载图片、视频) |
<a> |
触发 DNS 预取(解析链接) |
- 相关影响触发的例子
- 当未连接的子元素被插入到未连接的父元素下时(如
<track>
插入到<video>
),仍可能触发资源请求。 - 移除
<picture>
中的某个<source>
元素可能触发 Blink 重新选择有效源,并发起网络请求。
- 当未连接的子元素被插入到未连接的父元素下时(如
局限性与实现
- 不能操作与DOM树无关的攻击
- 如
eval
函数 - 如顶层视窗的更改,例如L
window.open
- 如
- 论文标题:《A Security Study about Electron Applications and a Programming Methodology to Tame DOM Functionalities》
- 会议:NDSS2023