ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DOM型XSS原理与防御:从浏览器渲染到前端代码审计

DOM型XSS原理与防御:从浏览器渲染到前端代码审计 1. 先说清楚这次要攻克的靶子DOM型XSS1.1 从浏览器渲染流程看DOM型XSS把XSS系列写到第四篇前面已经系统梳理了反射型XSS和存储型XSS两者的共同点是恶意代码都先进了服务端再被拼接到响应页面里回显给用户。而DOM型XSS是完全不同的另一条链路——恶意代码根本不需要去服务端转一圈浏览器自己就把用户可控的数据当成了页面脚本执行。不要小看这个差异它直接决定了排查思路、利用手法和防御方案的完全不同。先补背景知识。DOM是浏览器把HTML文档解析成的内存对象树页面里的JavaScript通过DOM接口可以动态修改页面结构。DOM型XSS发生的核心条件就是某个JS函数把外部可控数据比如URL参数、window.name、postMessage消息原样写进了页面的危险位置比如innerHTML、document.write、eval这些然后浏览器把插入的内容执行了。举一个典型的老站点例子很多网站有这样一个跳转逻辑const target new URLSearchParams(window.location.search).get(url); document.getElementById(backLink).href target;如果把访问链接改成https://example.com/details?urljavascript:alert(document.domain)用户点击那个返回链接时一个包含JS代码的URL被塞进了a标签的href属性浏览器在点击动作触发后执行了它。整个过程里这个参数没有经过任何服务端逻辑服务端日志里连痕迹都没有全靠前端JavaScript自己完成了读取输入、拼进DOM、触发执行这三个动作。这就是DOM型XSS和其他两类XSS最本质的区别。这篇文章适合三类人看一是做Web安全测试的朋友尤其是想系统补上客户端漏洞检测能力的二是前端开发者很多人对v-html、dangerouslySetInnerHTML背后藏着什么风险并没有真正意识到三是刚入门想刷XSS训练平台的新手知道payload能弹窗但不知道为什么会弹、换个环境为什么不弹。这篇把原理、入口、payload构造、大模型辅助挖掘、防御加固和排错全部串起来讲清楚。1.2 和前几篇讲的反射型/存储型XSS到底差在哪这里做一张对比表实际排查时对照着看会非常直观对比维度反射型XSS存储型XSSDOM型XSS恶意代码存储位置不存储一次性反射存储在服务端数据库不经过服务端触发方式构造恶意URL诱导点击正常访问被存储页面即可触发构造恶意URL前端JS自行处理触发关键切入点服务端模板拼接收参数据库读取后再拼接前端JS的source到sink数据流特征检测服务端日志可能看到payload数据库有脏数据服务端完全看不见payload排查难度找服务端回显点找写入点与渲染点需要逐行审计前端JS数据流表格最后一行是最容易被忽视的。实际做授权测试时如果目标页面把整个前端逻辑都放在静态JS文件里服务端天然就没有过滤点传统的WAF规则也大概率拦不住——因为请求里的payload在服务端看起来就是普通URL字符危险行为发生在浏览器侧的DOM渲染阶段。这也就是为什么测试DOM型XSS必须带着浏览器开发者工具逐行看实际渲染后的DOM树来确认插入点不能只靠抓包工具和扫描器。还有一点要注意DOM型XSS经常和反射型、存储型混着出现。最典型的场景是用户输入先被服务端存进数据库前端拿到数据后没有用textContent输出而是拼到innerHTML里。这种存储型DOM型的混合体攻击链长、排查成本高在真实业务站点里比单纯的DOM型更常见。别死板地把XSS按三类分开看数据流在哪里被消费才是重点。2. DOM型XSS的常见入口与触发场景2.1 高危入口source与写入sink清单前面讲完了原理现在进入实操第一步拿到一个页面后该把眼睛盯在哪些JS函数上我习惯按数据源source和写入口sink两个维度去拆。先看source也就是用户可控数据从哪里进来数据源说明典型写法实际利用提示location.search当前URL的查询参数urlParams.get(p)最常见入口POST之外的参数基本都靠它location.hashURL锚点部分不会发给服务器location.hash.substr(1)老站喜欢拿它做单页跳转隐蔽性强document.referrer来路页面地址location document.referrer配合跨站跳转可绕过部分过滤window.name窗口名字跨页面共享被iframe继承隐蔽性极高很多过滤器不检测postMessage窗口间跨域通信数据message事件里的event.data接收端如果不校验origin必出问题document.cookieCookie中的部分字段被误拼进DOM少见但存在再看sink也就是数据最终被写到了哪里innerHTML / outerHTML / insertAdjacentHTML会直接解析HTML标签事件属性能触发执行document.write / document.writeln老代码里极其常见一次性把字符串写进页面eval / Function / setTimeout / setInterval当参数是字符串时本质就是JS代码执行location.href / location.assign / location.replace / window.open可传javascript:协议也可传data:协议a.href / iframe.src / form.action / embed.src把URL渲染进属性值element.setAttribute配合上述危险属性名实际测站时我会在画出source到sink完整路径的同时把中间的字符处理过程也标出来。例如location.search里的值在进入innerHTML之前如果代码调用了encodeURIComponent那什么payload都起不来因为尖括号和引号全被转成了百分号编码。这一步千万不能省很多人半天没弹窗问题就出在中间多了一层编码。2.2 框架时代的隐性DOM XSS现在纯粹的jQuery拼字符串项目少了很多但这不意味着DOM型XSS被框架消灭了它只是换了个战场。Vue的v-html、React的dangerouslySetInnerHTML、Angular的bypassSecurityTrustHtml和bypassSecurityTrustUrl这三个接口本质上都等价于innerHTML是框架给开发者留下的信任后门。写业务代码时图省事把后台返回的一段富文本内容直接渲染进页面如果后台数据里混入了恶意HTML那用户一访问就能被打。这类存储型DOM型混合场景在企业站里比纯DOM型更常见因为数据入库后再被前端拼到innerHTML里服务端入口和WAF全都看不见危险。还有一类很容易漏掉的是把JSON数据放在URL参数里的SPA站点。例如const initData JSON.parse(new URLSearchParams(location.search).get(config)); document.body.insertAdjacentHTML(beforeend, initData.widgetHtml);这类代码直接把JSON里的HTML字段丢进DOMpayload的构造空间非常大。我之前测试过一个数据可视化项目它用hash路由存id再把id拼进svg标签的onload事件里。payload被URL的#符号包裹了服务端日志没有记录防护系统也没有报最后完全靠手动审查JS文件里的source到sink路径才抓住。框架场景下还有一个高危点是第三方SDK和组件库。很多图表库、富文本编辑器、Markdown渲染器为了功能完整性允许用户传入HTML字符串并直接渲染。如果业务层没有在前面做白名单清洗这些组件就成了绕过前端框架默认转义的通道。审查代码时除了业务代码本身依赖里的dangerouslySetInnerHTML、v-html、dangerouslySetInnerHTML出现频率也要追一遍。3. XSS攻击语句速查从入门到能实战3.1 不同上下文的基础payloadXSS攻击语句没有标准通解核心是看代码上下文选语法。上下文决定了哪些字符敏感、哪些写法能触发执行。我习惯把payload按四种上下文分类这对刚入门的朋友特别重要。第一类是在HTML标签属性值内比如value用户输入。最有效的思路是提前闭合引号和标签再挂一个事件属性 onmouseoveralert(document.domain) autofocus onfocusalert(1) xss idx tabindex1 onmouseoveralert(1)第二类是直接嵌入script标签里的JS变量比如var name用户输入。需要先逃逸出字符串边界;alert(document.domain);// \;alert(document.domain)// /scriptscriptalert(document.domain)/script第三类是能放javascript协议的地方典型是href、location、iframe.srcjavascript:alert(document.domain) JaVaScRiPt:alert(1) java%0ascript:alert(1)第四类是通过事件属性自动触发这是我日常测试里使用频率最高的img srcx onerroralert(document.domain) svg/onloadalert(1) details open ontogglealert(1) video srcx onerroralert(1) iframe srcdocscriptalert(1)/scriptsvg和details的写法非常稳因为它们几乎不需要额外条件就能触发。需要注意测试目标如果是XSS训练平台弹个窗只是开始真正做授权测试时要把alert换成能证明漏洞利用链的payload比如fetch到自己的接收服务器带上cookie或页面内容。这是区分会玩靶场和能做实战的分水岭。3.2 编码绕过与过滤绕过思路过滤规则五花八门但绕过逻辑有规律可循最常用的方向有四个。第一HTML实体编码。很多过滤只转义了尖括号和双引号对单引号或HTML实体本身不做二次处理。如果目标位置在标签属性内可以用、来构造引号闭合。比如某站把双引号转成了但单引号能原样进入那就可以用 onmouseoveralert(1) 第二JS中的Unicode编码。如果输入被拼进script标签的字符串里可以把关键字转成\u0061lert这种Unicode转义。JS引擎在解析字符串字面量时先解码再执行因此img srcx onerror\u0061lert(1)在某些只匹配alert关键字的WAF规则下可以绕过。第三URL编码与双重编码。location.search读取到的参数浏览器可能会自动做一次解码如果代码里又调用了decodeURIComponent那就在payload里预编码两层让它解码一次后仍然保留可执行结构。第四利用HTML解析器的标签怪癖。比如过滤了script标签但没过滤math、style标签mathmtext/mtextmglyphstyle!--/styleimg srcx onerroralert(1)这条思路利用的是浏览器解析引擎在不同标签模式下的容错机制在不少弱过滤规则下能直接突破。不过要泼一盆冷水不要一上来就上重型混淆payload。很多目标过滤其实做得很差一个简单的img onerror就过了。只有确认目标明确检测了尖括号、alert、script这些关键字时才需要祭出编码和标签怪癖。想练手的朋友建议本地搭一个简单的过滤脚本每次只加一条规则逐个验证payload的变化练上两天就能形成肌肉记忆。4. 大模型如何切入XSS漏洞挖掘4.1 让大模型做静态风险审查这两年大模型在安全圈出镜率越来越高我的真实感受是它不能替代实战经验但能把重复劳动大幅压缩。XSS漏洞挖掘恰好就是这种场景前端JS文件动辄几千行人工一行行找source到sink的数据流非常费眼让大模型做第一轮预处理则非常高效。具体做法是把审计目标聚焦在source和sink的关键调用点给大模型一个明确的提示词例如你是一名Web安全审计专家。请分析以下JavaScript代码找出所有把外部输入写入DOM的行为。 只关注这些sourcelocation.search、location.hash、document.referrer、window.name、postMessage。 只关注这些sinkinnerHTML、document.write、eval、insertAdjacentHTML、location.href、window.open。 对每个发现输出代码位置、source到sink的完整路径、是否存在中间过滤、建议的验证方式。 以下是代码 ...我第一次用这个思路处理一个3000行的SPA项目人工审要半天大模型几十秒就产出十几条候选路径再人工逐条验证后筛出4条真实可利用的。需要提醒的是把代码贴给大模型要注意保密企业环境里优先用本地部署的开源模型别把敏感代码直接发到云端服务。4.2 用大模型生成payload变体和fuzz字典大模型的第二个实用价值是生成payload变体。很多XSS测试卡在知道有漏洞但绕不过过滤器这时候让大模型批量产出变体效率极高。我试过这样一个提示词现有过滤规则删除尖括号和alert、script关键字不区分大小写。 请生成30个能触发JavaScript执行的XSS payload变体覆盖HTML实体、Unicode转义、标签怪癖、CSS表达式等不同思路。 每个payload只输出一行不要解释。大模型给出的结果里经常有灵光一闪的写法比如利用iframe srcdoc、利用插入后浏览器不会执行它需要换成img onerror这类事件属性或svg标签。这条在自动化测试里特别要紧很多时候测试半天没效果不代表漏洞不存在只是用了浏览器不执行的方式。location.hash自动编码。给hash赋值时浏览器会自动做URL编码导致某些payload被改得面目全非。测hash入口时要么拼完整URL让浏览器自行解析要么先手动编码好再赋值否则payload容易失效。console警告要重视。很多浏览器在即将执行被CSP拦截的脚本时会给出提示比如Blocked script execution in ... because the documents frame is sandboxed。看到这类报错直接把代码的sink逻辑和过滤条件对照一遍往往是过滤路径里含有CSP或者沙箱限制。过滤正则不一定匹配大小写。有的开发用转义、有的用编码但最常忘记处理大小写变形。所以payload变体里顺手写上
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进