如果你手头还维护着以Adobe Flex 3构建的老旧RIA系统,这条漏洞预警建议认真看完。Adobe Flex 3 DOM型XSS漏洞并不是指Flash Player本体的某个神奇洞,而是当SWF模块与浏览器DOM之间通过ExternalInterface、FlashVars等方式交换数据时,由于缺少必要的输入输出校验,把不可信内容直接拼进了DOM,最终形成客户端脚本注入。它可能造成会话劫持、页面伪造、敏感信息被窃取等不少麻烦。这篇文章我会从原理讲起,结合一个可复现的最小示例,逐步拆解漏洞成因、危害评估方法,最后给出能在生产环境直接落地的修复和排查方案。适合正在维护Flex遗留项目、或者刚接手这块安全评估工作的朋友参考,刚好把这类不太常见的XSS变种一次讲透。
1. 漏洞画像:Flex 3与DOM型XSS为什么会扯上关系
1.1 很多人的第一个误区:Flash怎么会产生DOM XSS
听到“Adobe Flex 3 DOM型XSS漏洞”,第一反应往往是:Flex写的是SWF,运行在Flash运行时里,和浏览器DOM能有什么关系?这其实是把“Flash应用”和“宿主页面”割裂开看了。
实际架构中,Flex 3开发的SWF不是孤立存在的。它被HTML页面通过object或embed标签嵌入,页面和SWF之间至少有两座桥:一是FlashVars,用来把页面参数传进SWF;二是ExternalInterface,用来让ActionScript和JavaScript互相调用。攻击者构造特殊URL参数传给FlashVars,Flex侧读取后,再用ExternalInterface把数据原样丢给JavaScript,而JavaScript又直接用innerHTML拼到页面上——经典的DOM型XSS链路就形成了。
很多早期Flex项目卡在这里:ActionScript开发人员认为“数据要交给JS去处理”,JavaScript开发人员认为“SWF传过来的数据已经过滤过”,结果两边都没做校验。漏洞就这样被“商量”出来了。
1.2 影响面和攻击条件
理论上说,所有Flex 3应用都可能踩中这个模式,但有三个前提容易命中:
- 应用通过FlashVars、loadVariables、URLLoader等获取外部可控制数据;
- ActionScript中通过ExternalInterface调用JavaScript函数;
- JavaScript回调函数将参数写入innerHTML、outerHTML、insertAdjacentHTML、document.write这类危险DOM接口。
满足这三条,基本就具备了一个可触发的DOM XSS。实际重灾区往往是后台管理系统、报表平台、监控大屏这类“内部系统”,因为这些系统常用Flex 3做复杂交互,又普遍低估内网应用的安全要求。攻击者一旦通过钓鱼、供应链投毒、内网横向等手段接触到入口,就能在受害人的浏览器上下文里执行任意脚本。
| 组件层 | 常见入口 | 风险状态 |
|---|---|---|
| 浏览器地址栏 | URL参数、hash、window.name | 攻击者可构造 |
| HTML嵌入页 | FlashVars、SWF参数 | 数据可能未过滤 |
| Flex 3 SWF | LoaderInfo.parameters、ExternalInterface.call | 极易原样转发 |
| 父页面JavaScript | innerHTML、document.write | 危险操作直接触发 |
2. 漏洞原理深度剖析:一条从URL到DOM执行的通路
2.1 先分清什么叫“DOM型XSS”
DOM型XSS和传统的反射型、存储型XSS最大的区别是:恶意代码不经过服务器。服务器可能完全不知情,响应里也没有任何恶意内容,所有“非法操作”都发生在浏览器端运行时。
举个例子,反射型XSS是“服务端拿到参数就回填到HTML”,恶意字符串会在响应页面里出现。而DOM型XSS是“JavaScript拿到参数后自己修改DOM”,恶意字符串只存在于客户端,扫描器如果只看服务端响应,根本找不到痕迹。
Flex 3场景下的DOM XSS更特殊:参数先进SWF,再由ExternalInterface送出去。理论上这段数据从“输入到输出”绕了一圈Flash运行时,服务器端连参数长什么样都不知道,防火墙和Web应用防火墙几乎无感知。所以这类漏洞的隐蔽性很强,传统漏洞扫描器经常漏报。
2.2 以Flex 3应用为例的完整触发链路
我用一个最小Demo还原过这条链路,代码不长,但非常直观。
第一步,Flex侧从FlashVars读取用户参数:
// Flex 3 / ActionScript 3 var params:Object = LoaderInfo(this.root.loaderInfo).parameters; var userName:String = params["user"] || ""; // 直接转发给JavaScript,没有做任何校验 ExternalInterface.call("showUser", userName);第二步,宿主页面里定义了一个很不安全的回调函数:
// 宿主页面中的JavaScript function showUser(userName) { var box = document.getElementById("userBox"); box.innerHTML = "欢迎:" + userName; }第三步,攻击者构造这样的URL:http://flex-app.example/demo.swf?user=%3Cimg%20src=x%20onerror=alert(document.domain)%3E
当页面加载并执行到ExternalInterface.call时,JavaScript收到的就是一个完整的<img src=x onerror=alert(document.domain)>。innerHTML把它插入DOM,图片加载失败触发onerror,脚本原地执行。
整个过程服务器没有存储任何恶意内容,也没有在响应页面中输出它。恶意载荷完全是在浏览器内存里“旅行”了一遍,这就是典型的DOM型XSS。
2.3 为什么“看起来已过滤”还是被打穿
很多团队不是完全没防守,而是防错了地方。有人只把ActionScript侧收到的<替换成了<,觉得这样就算过滤了,结果JavaScript拿到的是<img...>,一旦写入innerHTML,浏览器解析实体后仍然会还原成合法的HTML标签。因为HTML实体解码发生在DOM解析阶段,innerHTML注入时数据已经被还原了。所以ActionScript里的那种“字符串替换式”过滤,在DOM上下文里经常是无效的。
还有人只在JavaScript侧做了黑名单,比如把<script>标签替换掉。攻击者换成<img src=x onerror=...>、<svg onload=...>、<details open ontoggle=...>,黑名单根本堵不完。更麻烦的是,payload经过URL解码、Unicode实体编码、DOM Clobbering等操作后,会变得更加难缠。
这类问题根子上不是“某个字符没处理干净”,而是数据进入不同上下文时没有做对应的编码策略。HTML标签上下文、属性上下文、JavaScript上下文、URL上下文,需要完全不同的处理方式。
2.4 容易被忽略的第三方Flex组件
Flex 3生态时代有很多第三方组件库,图表、网格、日期控件一抓一大把。这些组件内部经常内置自己的JavaScrip桥接层,甚至封装了网页弹窗、自动补全、数据导出功能。第三方组件里的ExternalInterface调用往往没人审查,数据流转比业务代码更隐蔽。
我还碰到过一个场景:某个第三方图表组件会把JSON配置直接序列化后传给JavaScript,配置里又包含图表标题、提示模板等可以插值的内容。攻击者只要控制了一个字段,就能从SWF一路打穿到DOM。所以排查的时候,不要只盯着自己写的ActionScript,第三方SWC组件和它的JS适配层都要纳入攻击面。
3. 危害研判:从“脚本能执行”到“业务被控制”
3.1 为什么这类漏洞不能只看“弹个框”
漏洞演示时大家常用alert(document.domain)证明XSS存在,但这只是“能执行JavaScript”的证明,不代表实际危害就这么点。恶意脚本一旦在受害者的父页面上下文里执行,就和页面本身拥有一样的权限,包括当前标签页的Cookie、localStorage、sessionStorage、页面DOM内容,甚至可以发起同源请求。
这意味着攻击者已经继承了“页面里的用户身份”。后续能做多少事,取决于这个页面对应业务系统有多大的权力。一个只能看新闻的公开页面,和一个月报审批、资金操作、数据导出的后台系统,危害完全不是一个量级。
3.2 四类典型攻击路径
| 攻击路径 | 利用方式 | 典型后果 |
|---|---|---|
| 会话劫持 | 读取Cookie后发送到指定服务器 | 管理员或用户账号被冒用 |
| 页面钓鱼 | 直接修改当前页面DOM,替换成登录表单 | 窃取账号密码、二次验证码 |
| 数据窃取 | 遍历DOM、读取接口返回数据 | 敏感业务数据泄露 |
| 恶意操作 | 以用户身份请求接口、提交订单、改配置 | 业务数据被篡改 |
这四个方向不是互相排斥的,实际攻击中往往组合使用。攻击者先插入一个键盘记录脚本,等用户输入密码后,再把数据回传到后台Bot;也可以直接伪造一个“会话已过期”的提示框,诱导用户输入当前密码,实时回传。
3.3 危害等级评估逻辑
很多人问这类漏洞到底算高危还是中危。我的评估逻辑是三步:
- 第一步看页面访问条件:完全无需登录的匿名页面,危害控制在“可骚扰”级别;需要登录的管理后台,危害直接上升到“高风险”。
- 第二步看数据敏感度:页面里能看到身份证、手机号、合同内容,或者有转账、审批、发布功能,就属于高风险业务。
- 第三步看可利用复杂度:Flex 3场景通常不需要特殊交互,攻击者丢一个URL给受害者点击,或者诱导受害者打开一个恶意构造的链接即可,复杂度低,更能拉高评级。
综合下来,大部分真正影响业务系统的Flex 3 DOM XSS,我都会按高危来响应。如果还处于开发阶段、无真实数据、又是公网演示环境,可以降级处理。
4. 修复方案:从根源到最后一公里的分层落地
4.1 第一层:收紧ActionScript侧的“嘴巴”
修复不能只修一端,要先从源头开始。Flex 3侧要做的两件事:输入验证和接口收敛。
输入验证上,对FlashVars、URL参数、外部配置文件里读到的每个值都按“业务预期”做白名单校验。比如userName字段只期待字母、数字、下划线,那就直接用正则约束白名单,而不是替换危险字符。不能用“把容易触发XSS的字符都删掉”这种思路,因为绕过手段远比可枚举的字符多。
// 白名单校验示例 var raw:String = params["user"] || ""; var userName:String = "guest"; if (raw && /^[a-zA-Z0-9_\u4e00-\u9fa5]{1,20}$/.test(raw)) { userName = raw; } ExternalInterface.call("showUser", userName);接口收敛上,减少对外暴露的函数很关键。不需要暴露给JavaScript的Internal方法,一律不调用ExternalInterface.addCallback。必须暴露的,每个参数都默认是恶意输入,在ActionScript侧进行一次基础净化,再配合JS侧二次处理。不要依赖单一语言层的过滤,多层防御才是正确姿势。
4.2 第二层:修复JavaScript的“最后一公里”
真正决定漏洞是否还成立的关键点,是危险DOM操作是否被安全API替代。只要JavaScript回调函数还在用innerHTML拼接口数据,上游再怎么过滤,总会有绕过空间。
最简单、最稳妥的替代方案是textContent:
function showUser(userName) { var box = document.getElementById("userBox"); box.textContent = "欢迎:" + userName; }textContent会按纯文本方式插入内容,任何HTML标签都只能原样展示,不会变成可执行节点。如果业务确实需要展示富文本,比如显示外部传来的带格式公告,就必须走结构化解析:要么用createElement一个一个建立节点,要么在执行HTML渲染前,用一个成熟的HTML净化逻辑把标签白名单、属性白名单过一遍。
这里我特别提醒一句:不要为了“省事”自己写一个一行的escape函数就完事,不同上下文需要不同转义。如果只是把数据插入文本节点,textContent就够了;如果必须生成HTML属性,那就要做属性上下文编码,引号、空格、/等字符都不能漏。安全库的好处是它按标准处理“HTML上下文”,不是简单replace。
4.3 第三层:从架构和权限上降低风险
代码修完,还要看架构上有没有放大风险的地方。
首先,对Flex 3老系统尽量做到“隔离”。如果短期无法从Flex迁移到HTML5,就把SWF服务部署到独立子域,通过postMessage和主站通信,避免SWF直接访问主站DOM和Cookie。这样即使某个SWF还是被注入执行,攻击者拿到的也只是隔离域的权限,影响面会明显缩小。
其次,给宿主页面加上内容安全策略(CSP)。对仅需加载自己脚本和老旧Flex模块的页面,配置script-src 'self',禁止unsafe-inline和unsafe-eval。这样即使存在DOM型XSS,内联的恶意脚本也会被CSP直接拦在门外,形成一道非常有效的兜底防线。CSP不是业务代码,但它是安全“最后一公里”的保险丝。
再次,用升级淘汰老技术栈。Flex 4、4.6虽然继续沿用RIA架构,但后续的SDK、容器和生态已经明显收敛了这类问题。条件允许时,逐步把高价值业务迁移到HTML5,从源头上断掉ExternalInterface这条危险通路。迁移过程中,新旧系统同时跑一段时间,用代理层做灰度切换,把风险控制在可接受范围。
4.4 修复后的回归验证清单
修完后要做一轮回归测试,而不是改完代码就算结束。我自己的验证清单是这样:
- 用原始payload重新访问,确认不再弹窗;
- 构造绕过变体:大小写混合、HTML实体编码、URL双重编码、svg/onload、details/ontoggle,看是否还有漏网;
- 打开浏览器开发者工具,在Sources面板全局搜索innerHTML、outerHTML、insertAdjacentHTML、document.write,确保没有其他危险调用点;
- 对Flex侧ExternalInterface.call做全局搜索,逐个确认每个调用参数都有下游安全处理;
- 如果在JS侧用了textContent替代,需要确认富文本功能是否受影响,影响的话补白名单方案;
- 回归一次会话管理、权限判断等核心流程,确认修复没有破坏原有业务。
5. 常见问题与排查技巧实录
5.1 我已经把敏感参数改成textContent了,为什么测试时还能弹窗?
这个很常见。往往你只修了一条链路,但应用里还有第二条、第三条链路绕过。比如一个页面先通过FlashVars把数据读进SWF,SWF又通过ExternalInterface把数据传给了某个公共函数,而公共函数在另外一个位置被别的业务也调用了,那个调用点还在使用innerHTML。
排查时,不能只看来报告漏洞的那个URL,要把整个页面的所有JavaScript执行路径过一遍。最好的方式是做数据流图:从输入节点开始,沿着变量流向,把每一条通往危险函数的路径都画出来。你会发现同一个变量往往不止一个出口。
5.2 我已经在JavaScript里做了decodeURI和编码转换,为什么还是被打穿?
这类问题通常出在“把解码后的内容当安全内容”的思维上。DOM XSS要求的是在代码运行时,根据当前上下文去做正确编码。不是先解码一次再过滤一次就能一劳永逸。比如你先decodeURIComponent处理用户输入,再数据里面包含%253C,解码一次变成%3C,再进入innerHTML时浏览器再次解析就会还原成<。多层编码之后,黑名单根本不认识原始攻击载荷。
正确的修复方向是,不要让数据经过“解码再注入危险接口”的路径,直接在最终注入点使用textContent或白名单解析。前面的解码步骤只会增加不确定性。
5.3 如何判断漏洞到底是Flex侧还是宿主页面侧?
这个问题在工单沟通里经常出现。很多Flex开发觉得是页面JavaScript写脏了,前端开发又觉得是ActionScript把不可信数据传过来了。最好的办法是用一个最小化实验来判断:
先固定Flex侧不变,把ExternalInterface.call注释掉,改用一个手工构造的固定字符串去调用JavaScript函数,如果此时注入不生效,说明风险主要在Flex侧的数据进口;反过来,如果固定字符串照样弹窗,说明JavaScript侧的危险DOM操作本身就是问题。
两个位置都不干净,那就两边一起修。只修一端,后续还可能被另一端的其他数据路径再次触发。
5.4 老系统不太好改源码,有没有快速止血方法?
如果短期不能改动SWF源码,可以先把宿主页面的关键危险函数从innerHTML改为textContent,这是最快的止血方案。同时部署CSP、限制页面权限、在网关层对可疑参数做拦截。虽然不解决根本问题,但能把可利用性降下来很多。
注意:网关层拦截只能作为临时手段,因为Flex应用的完整参数链路未必全走同一个网关,而且拦截规则很容易被编码绕过。长时间兜底不能只靠它。
5.5 开发环境中没有复现,为什么生产环境就弹窗了?
开发环境常用本机直接访问SWF,参数入口比较简单。生产环境里可能多了前端路由、统一登录跳转、统计脚本等额外代码,这些代码会把URL参数再加工一次,形成新的注入点。所以排查时要尽量复制生产环境的浏览器扩展、统一脚本配置和入口页面逻辑,不能只看一个孤立的SWF文件。
6. 写在最后的实操体会
前前后后处理过几个Flex 3遗留项目,我感觉这类问题最大的坑不是技术难度,而是Flex开发与前端开发之间很容易互踢皮球。ActionScript侧觉得自己传的是“内部数据”,JavaScript侧觉得Flash是“隔离环境”,最后漏洞一直挂着没人处理。
我最常用的一招是“统一出口”。不管SWF要调多少个JavaScript函数,在宿主页面只暴露一个入口函数,例如safeHandle(eventType, data),所有参数都必须经过统一的安全DOM写入逻辑,再加上textContent或者白名单解析。这样后续新增业务时,只要走这个出口,就不会再漏出新洞口。这个设计改动量不大,但对老项目的长期维护非常有用。
还有一个小地方值得分享:修复完这类漏洞后,记得把测试payload留到内部回归用例集里,而不是直接从代码里删掉。下次再有人动相关模块,跑一遍就能立刻发现是不是把危险写法又带回来了。安全修复的意义不在于“现在改完”,而在于“以后不再犯”。