1. 项目概述:从“Stage #2”看XSS的进阶战场
如果你已经玩转了DVWA、Pikachu靶场里的那些基础反射型和存储型XSS,觉得弹个
alert(1)
已经索然无味,那么“Stage #2”这个概念的出现,很可能就是你Web安全技能树需要点亮的下一个关键节点。这个标题里的“Stage #2”并非某个特定工具或框架的版本,它在安全研究社区里,尤其是在深入挖掘客户端漏洞时,常常被用来指代一种更隐蔽、更刁钻的攻击面探索阶段。简单来说,当常规的输入框、URL参数这些“Stage #1”的明显入口被防护得滴水不漏时,攻击者的目光就会转向那些看似“只读”或“非用户直接可控”的地方——HTML标签的属性(Attribute)。
属性中的XSS,正是这样一个典型的“Stage #2”战场。它不像在搜索框里直接输入
<script>alert(1)</script>
那样直白,而是需要你理解前端代码是如何动态拼接、如何将数据渲染到DOM属性中的。比如,一个看似无害的
<img src=”
后面跟着一个由后端返回、但前端未经验证就拼接上去的URL参数,就可能成为漏洞的起点。最近社区里讨论的“no light found in stage”这种略显晦涩的短语,某种程度上反映了研究者们在挖掘这类深层、非显性漏洞时,面对复杂代码流和模糊上下文的那种探索与挫败感并存的体验。
这篇文章,我们就来彻底拆解“属性中的XSS注入技术”。我会假设你已经了解XSS的基本原理(反射型、存储型、DOM型),我们将直接越过基础,聚焦于如何发现、利用并最终防御那些隐藏在HTML属性里的安全风险。无论是面对jQuery时代遗留的
$.html()
不当使用,还是现代前端框架中可能出现的动态属性绑定问题,你都能在这里找到系统的分析思路和实操方法。我们的目标不是复现靶场,而是建立一套适用于真实黑盒与白盒测试的深度挖掘逻辑。
2. 属性XSS的核心原理与攻击面解析
要攻击属性,首先得明白属性在什么情况下会变得“危险”。HTML属性本身大多用于配置元素行为,如
src
、
href
、
action
、
data-*
、
style
,甚至是事件处理器属性如
onload
、
onerror
、
onmouseover
。XSS发生的根本条件始终未变:
用户可控的输入,未经充分过滤或转义,被当作代码(HTML/JavaScript)解析执行
。属性XSS的特殊性在于,注入点发生在属性值内部,而浏览器解析属性值的方式与解析HTML标签体内容的方式有所不同。
2.1 属性值上下文与突破策略
属性值通常被引号(单引号
'
或双引号
"
)包裹,或者在某些情况下可以省略引号(当值是简单数字、字母且无空格时)。这就构成了一个初始的“上下文”。攻击者的首要任务就是
打破这个属性上下文
,并
开启一个新的、可执行代码的上下文
。
经典案例:未正确转义的
href
或
src
属性
假设一个用户资料页面,头像URL来自用户输入:
<img src=”USER_INPUT” alt=”avatar”>
如果
USER_INPUT
是
” onerror=”alert(1)
,那么渲染后的HTML变为:
<img src=”” onerror=”alert(1)” alt=”avatar”>
这里,攻击者通过输入一个闭合的双引号
”
,提前结束了
src
属性,然后添加了一个新的
onerror
事件处理器属性。当图片加载失败(
src
为空),
onerror
内的JavaScript就会执行。这就是典型的
跳出原属性,构造新事件属性
的攻击模式。
更隐蔽的案例:在
data-*
或自定义属性中
现代应用常用
data-*
属性存储信息供JavaScript使用。如果这些数据由用户输入且被不安全地使用,例如:
<div data-user-info=”USER_INPUT”></div>
后端可能未过滤,而前端JavaScript可能直接这样读取并使用:
var userInfo = $(‘div’).data(‘user-info’);
// 或者更危险的
$(‘.some-placeholder’).html(userInfo);
如果
USER_INPUT
包含HTML实体,但前端使用
.html()
或
.innerHTML
时,就可能造成二次渲染XSS。这要求攻击者不仅需要找到注入点,还需要理解前端的数据流。
注意 :属性值内部的字符转义至关重要。在HTML上下文中,
&、<、>、”、’等字符需要被转换为实体(如&、<等)。但如果后端只在全局做了“转义HTML标签”这种粗粒度过滤,却忽略了属性值内部的引号,那么上述攻击依然可能成功。
2.2 基于事件处理器(Event Handler)的属性XSS
这是属性XSS中最常见、也最直接有效的一类。除了上面提到的
onerror
,还有数十种事件处理器可以被利用,如
onload
、
onmouseover
、
onfocus
、
onblur
等。攻击场景非常广泛:
-
图片/媒体标签
:
<img>、<audio>、<video>的onerror事件是经典入口,因为源加载失败是常见情况。 -
表单输入框
:
<input>、<textarea>的onfocus、onblur、onchange事件,可以在用户与元素交互时触发。 -
锚点标签
:
<a>标签的onmouseover事件,可以实现“鼠标划过即触发”的非常隐蔽的攻击。 -
SVG标签
:SVG作为XML,其内嵌的
<script>标签或事件属性(如onload)在嵌入HTML时可能被某些解析器执行,这构成了更复杂的攻击向量。
实操中的关键点
:现代浏览器对某些事件处理器在部分标签上的使用有所限制(例如,
<img>
的
onerror
依然有效,但一些限制策略可能会被CSP等缓解),但在绕过过滤时,大小写混淆、插入无关属性或空白字符、使用十进制/十六进制HTML实体编码等方式,常常能绕过简单的黑名单过滤。
2.3 基于
style
属性与CSS注入的旁路
当事件处理器被严格过滤时,
style
属性可以作为一个有趣的跳板。纯粹的CSS本身不能直接执行JavaScript,但它可以导致意想不到的行为,再结合其他漏洞。
-
表达式注入(旧版IE)
:在很久以前,IE浏览器支持CSS
expression(),如style=”width: expression(alert(1))”。虽然现代浏览器已不支持,但在内部系统或特定环境下仍是知识点。 -
结合
@import或behavior:这些已基本被淘汰,但体现了攻击思路的多样性。 - 用于信息泄露 :更实用的方法是利用CSS选择器通过属性值进行数据窃取,但这通常属于CSRF或信息泄露范畴,是另一种攻击模式。
对于属性XSS,
style
的更常见利用方式是
构造一个能触发其他事件的行为
。例如,通过
style
设置一个无效的
background-image
URL,然后结合
onerror
事件(但
onerror
本身是独立属性)。更高级的可能会研究在
style
中注入
-moz-binding
(Firefox旧特性)等,但这已非常边缘化。
当前更值得关注的是,
style
属性值如果完全可控,且页面存在将
style
值动态应用到
<script>
标签或
svg
标签的情况,那可能打开全新的漏洞链
。这要求对前端框架的数据绑定机制有深入理解。
3. 深入挖掘:从黑盒模糊测试到代码审计
知道了原理,我们该如何系统地发现这类漏洞?单纯的手工测试效率低下,需要结合系统性的方法。
3.1 黑盒测试:精准的Payload构造与模糊测试
面对一个未知应用,你可以遵循以下步骤:
-
识别潜在注入点
:关注所有回显到页面上的用户输入,特别是那些出现在
src、href、value、data-*、placeholder、title、alt等属性里的参数。使用浏览器开发者工具,仔细检查网络响应和最终DOM结构。 -
测试上下文突破
:首先尝试注入引号。
-
输入
’或”,观察页面是否报错、布局是否错乱、或者引号是否被转义(变成'或")。 -
输入
’><script>alert(1)</script>,这是一个试探性Payload,用于判断是否同时存在标签闭合和脚本执行的可能。
-
输入
-
构造属性XSS专用Payload
:如果发现引号未被过滤,立即系统性地测试事件处理器。
-
基础测试集
:
" onmouseover="alert(1) ' onload='alert(1) " onerror="alert(1) -
绕过技巧测试集
(针对简单过滤):
" OnMoUsEoVeR="alert(1) // 大小写混淆 "onmouseover="alert(1) // 省略事件前的空格(某些解析器可能容忍) "%0aonmouseover%3d"alert(1) // 插入URL编码的换行符和等号 " onmouseover=alert(1) // 事件值不加引号 -
利用资源加载失败
:对于
img的src,可以结合onerror。" onerror="alert(1) x" onerror="alert(1) // 让src值为x”,触发404错误
-
基础测试集
:
-
使用自动化工具辅助
:工具如Burp Suite的Intruder、XSS Hunter、以及各种XSS模糊测试字典(如
fuzzdb中的XSS相关列表),可以帮你快速发送大量变种Payload。但切记,工具不能替代思考,尤其是对于属性上下文,需要人工判断回显位置。
实操心得 :在黑盒测试中,最容易忽略的是那些“间接可控”的属性。例如,个人昵称可能显示在页面标题
<title>里,也可能通过JavaScript动态设置为某个div的data-username属性。你需要追踪一个输入参数在整个应用中的数据流,不只看它第一次回显的地方。
3.2 白盒审计:聚焦前端代码的数据流
对于有代码访问权限的情况,审计效率可以极大提升。关键不在于逐行看,而是追踪数据从接收到展示的完整链条。
-
寻找数据接收点
:在后端代码(如PHP、Java、Python控制器)中,搜索接收用户参数的函数(如
$_GET[‘name’]、request.getParameter(“id”))。 -
追踪数据处理与转义
:查看这些参数是否经过了过滤或转义函数。常见的危险信号包括:
- 使用了过时或不完整的黑名单过滤。
-
只调用了类似
strip_tags()的函数(可能移除标签但不管属性)。 -
使用了
htmlspecialchars()但设置了错误的标志位(如ENT_QUOTES标志未设置,导致不转义单引号)。 - 最关键的是:转义时机错误 。数据在入库时转义了一次,出库时又转义了一次,可能导致双重转义;或者该在HTML上下文转义的数据,却只在JavaScript上下文中进行了处理。
-
定位前端渲染点
:这是审计属性XSS的重中之重。
-
搜索字符串拼接
:在前端JavaScript中,搜索使用
+、+=、模板字符串(反引号)或concat()方法拼接HTML字符串的地方。特别是拼接的字符串中包含了用户数据。// 危险示例 element.innerHTML = ‘<img src=”‘ + userAvatarUrl + ‘“>’; $(‘#profile’).html(‘<div data-info=”‘ + userInput + ‘“></div>’); -
检查jQuery的
.html()、.append()、.before()等方法 ,以及原生innerHTML、outerHTML、document.write()的调用,看其参数是否包含未经验证的数据。 -
分析现代前端框架
:对于Vue/React/Angular,重点检查:
-
Vue
:
v-html指令(等同于innerHTML)、在属性绑定:href、:src、:style中使用了未过滤的复杂表达式或直接拼接用户数据。 -
React
:
dangerouslySetInnerHTMLAPI是明显的风险点。另外,检查是否在href、src等属性中直接使用了{userControlledString}。 -
Angular
:
[innerHTML]属性绑定、bypassSecurityTrust系列API的滥用。
-
Vue
:
-
审查属性设置API
:如
setAttribute()、element.attributeName = value,看设置的属性值是否来自用户。
-
搜索字符串拼接
:在前端JavaScript中,搜索使用
一个典型的漏洞链示例
:
后端从数据库取出用户昵称
nickname
,未做转义直接传递给模板。模板引擎将其放入一个
data-nick
属性:
<div data-nick=”{{ nickname }}”>
。前端某处JavaScript为了显示效果,执行了
$(‘.chat-msg’).html(‘<b>’ + $(this).data(‘nick’) + ‘</b>’)
。如果
nickname
是
” onmouseover=”alert(1)
,那么它首先作为
data-nick
的属性值被安全存储(因为HTML解析时,属性值里的引号是合法的)。但当jQuery的
.data()
方法读取时,它会智能地解析出字符串
” onmouseover=”alert(1)
,然后
.html()
方法将其作为HTML拼接,最终导致XSS。这个漏洞的根源在于
将HTML上下文中安全的属性值,未经处理就用于了另一个HTML上下文的拼接
。
4. 高级利用技巧与绕过实战
当目标应用部署了基础的防护(如输入过滤、输出转义、简单的WAF规则)时,就需要更精巧的绕过技术。
4.1 编码与混淆的艺术
浏览器在解析HTML和JavaScript时,会经历多层次的解码过程。利用这种特性,可以构造多层编码的Payload。
-
HTML实体编码
:在HTML属性值中,浏览器会解码实体。
-
Payload:
" onmouseover="alert(1) -
编码后:
" onmouseover="alert(1)(但这样直接输入,后端可能识别为实体而原样输出) -
更有效的方式是,
在输入被过滤后,寻找一个解码环节
。例如,如果后端只过滤了
<和>,但允许&,你可以尝试输入" onmouseover="alert(1)。如果前端某个环节(如一个JavaScript的innerHTML赋值)对这个字符串进行了解码,那么它就会还原成可执行的Payload。
-
Payload:
-
JavaScript Unicode编码
:在事件处理器属性值(这是一个JavaScript上下文)中,可以使用Unicode转义。
-
" onmouseover="\u0061\u006c\u0065\u0072\u0074(1) -
这可以绕过一些基于关键词(如
alert)的过滤。
-
-
混合编码与非常规语法
:
-
利用换行符、制表符等空白字符分隔关键词:
onmouseover\r\n=\r\nalert(1)。 -
利用JavaScript的
eval()和String.fromCharCode()动态构造代码:" onmouseover="eval(String.fromCharCode(97,108,101,114,116,40,49,41))。
-
利用换行符、制表符等空白字符分隔关键词:
4.2 利用DOM操作与Sink点
属性XSS的最终执行,往往依赖于前端的某个“接收器”(Sink)。除了直接的
innerHTML
,还有一些容易被忽略的Sink:
-
location.href/location.assign()/location.replace():如果可控数据被赋给这些属性,可能构成JavaScript伪协议XSS(如javascript:alert(1)),但这通常发生在href属性中,也属于属性利用范畴。 -
eval()/setTimeout()/setInterval():如果它们的参数部分可控。 -
Function构造函数。 -
.outerHTML属性:与innerHTML类似但范围更大。 -
document.write()/document.writeln():古老但可能仍存在。
绕过案例
:假设一个过滤规则是删除所有
on\w+=
这样的模式。你可以尝试:
-
使用
<svg>标签的onload事件:<svg onload=alert(1)>。因为onload在SVG上下文中,可能不被同一套正则匹配。 -
使用
<iframe>的srcdoc属性:<iframe srcdoc=”<script>alert(1)</script>”>。srcdoc属性内的内容会作为一个独立的HTML文档解析,可能绕过针对当前页面的过滤。
4.3 针对框架与库的特定技巧
-
jQuery
:老版本(特别是1.x早期版本)的
.html()方法在某些情况下有执行脚本的行为差异。另外,jQuery的.data()方法如前所述,会智能解析属性值,这可能将存储的字符串中的HTML实体解码,造成意想不到的漏洞。 -
AngularJS
:在1.x版本中,如果未使用
ng-bind-html的$sce服务进行严格信任,或者存在沙箱绕过漏洞(历史上存在多个),用户输入在表达式中可能被执行。 -
现代框架的警告
:虽然Vue/React默认的插值(
{{ }}或{})是安全的(会进行文本转义),但一旦开发者使用了“危险”的API(v-html,dangerouslySetInnerHTML)或通过ref直接操作DOM并设置了innerHTML,所有框架提供的安全屏障就失效了。
5. 防御策略:从开发到部署的纵深防线
理解了攻击,防御就有了方向。防御属性XSS需要多层次、全链路的措施。
5.1 输入验证与输出转义(黄金法则)
-
严格的输入验证 :在服务器端,根据数据的预期用途,进行白名单验证。例如,头像URL应该符合URL格式,昵称可以限制字符集和长度。但这只是第一道防线,不能依赖它来防止XSS。
-
上下文相关的输出编码/转义(最关键) :
-
HTML上下文(标签体)
:转义
<、>、&。 -
HTML属性上下文
:转义
”、’、<、>、&。 必须转义引号 。使用如htmlspecialchars($string, ENT_QUOTES)(PHP)或类似的库函数,确保ENT_QUOTES标志被设置以处理单引号。 -
JavaScript上下文
:将数据嵌入
<script>标签或事件属性时,必须进行JavaScript编码(如转义\、”、’、换行符,或使用JSON.stringify())。 -
URL上下文
:在
href、src等属性中,使用URL编码。 -
CSS上下文
:在
style属性或<style>标签中,进行CSS编码。
最佳实践是:在数据最终被渲染的那个点上,根据其所在的上下文,进行相应的转义。 不要相信任何上游“已经转义过”的数据,除非你能百分百确定其上下文与当前一致。
-
HTML上下文(标签体)
:转义
5.2 安全的前端开发实践
-
避免不安全的API
:除非绝对必要,否则不要使用
innerHTML、outerHTML、document.write()以及jQuery的.html()。优先使用textContent或jQuery的.text()来设置文本内容。 -
使用安全的属性设置方法
:对于动态设置属性,使用
setAttribute()或框架的属性绑定语法(如Vue的:href、React的href={safeValue}),并确保绑定的值是经过验证或转义的。 -
谨慎处理
data-*属性 :从data-*属性读取数据时,如果数据将用于构建HTML,必须将其视为不可信输入,进行相应的转义。 -
利用框架的安全特性
:在Vue中,除非必要,避免使用
v-html;如果使用,必须通过$sce.trustAsHtml()显式信任(并确保信任的数据源绝对安全)。在React中,尽量避免dangerouslySetInnerHTML;如果使用,必须确保内容是可信或已净化的。
5.3 内容安全策略(CSP)——最后的坚固堡垒
CSP是一个强大的深度防御工具。它通过HTTP头告诉浏览器,哪些来源的资源(脚本、样式、图片等)是可以加载和执行的。
一个针对属性XSS有较好防护效果的CSP策略示例:
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; style-src ‘self’ ‘unsafe-inline’; img-src ‘self’ data: https:;
但请注意,
‘unsafe-inline’
允许内联脚本和样式,这
会极大削弱CSP对XSS的防护能力
,因为属性XSS(如
onmouseover=”…”
)正是内联事件处理器。理想情况下,应该完全禁止内联脚本和样式。
更安全的CSP配置 :
Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted-cdn.example.com; style-src ‘self’; object-src ‘none’;
这个策略禁止了所有内联脚本(包括事件处理器)和
eval
,只允许从同源和指定的可信CDN加载脚本。要使用这种策略,你必须将所有的JavaScript代码移到外部文件中,事件处理器通过
addEventListener
绑定。这能从根本上杜绝属性XSS(以及大多数其他XSS),因为即使攻击者成功注入了
onclick=”maliciousCode”
,CSP也会阻止其执行。
实施严格的CSP可能需要重构前端代码,但它是目前防御XSS最有效的手段之一。
5.4 其他辅助措施
-
使用安全的Cookie属性
:设置
HttpOnly标志,防止JavaScript通过document.cookie窃取Cookie。设置SameSite标志为Strict或Lax,增加CSRF防护。 -
输入净化库
:对于富文本等必须允许部分HTML的场景,使用成熟的净化库(如DOMPurify for JavaScript,
html-sanitizerfor PHP/Python等),而不是自己写正则表达式。 - 定期安全审计与自动化扫描 :将XSS检查纳入代码审查流程,并定期使用动态应用安全测试(DAST)工具进行扫描。
6. 实战案例复盘与排查清单
最后,我们通过一个虚构但融合了常见问题的案例,来串联所有知识点。
案例场景 :一个社交网站的“个性签名”功能。签名支持少量HTML(如加粗、斜体),在用户主页和评论区显示。
漏洞发现过程 :
-
白盒审计
:发现后端对签名处理逻辑是:先使用一个简单的黑名单过滤
<script>、on\w+=等,然后存入数据库。前端渲染评论时,使用$(‘.comment’).html(userSignature)。 -
黑盒测试
:输入
<b>test</b>,成功加粗。输入<img src=x onerror=alert(1)>,发现onerror被过滤了。输入<img src=x oNerrOr=alert(1)>(大小写混淆),弹框成功!说明过滤规则不健全。 -
深入挖掘
:进一步测试发现,属性值中的引号未被转义。输入
” onmouseover=”alert(1),在个人主页的签名区域(该区域使用.html()渲染)未触发。但查看源码发现,签名被放在了data-signature属性里。追踪前端代码,发现评论区为了快速显示,直接执行了$(this).find(‘.sig’).html($(this).data(‘signature’))。于是,在签名处输入” onmouseover=”alert(1),当鼠标悬停在评论区的用户签名上时,成功触发XSS。
根本原因 :
- 后端过滤采用黑名单且可绕过(大小写不敏感匹配)。
-
数据在HTML属性上下文(
data-signature)中存储时,其中的引号未被转义。 -
前端错误地将存储在属性中的、来自不可信源的数据,直接通过
.html()方法插入到HTML上下文中,而没有进行任何处理。
修复方案 :
-
后端
:移除黑名单过滤。在将签名输出到HTML属性时,使用正确的HTML属性编码(转义
&、<、>、”、’)。对于需要保留的少量安全HTML标签(如<b>、<i>),使用白名单净化库(如DOMPurify的服务器端版本)进行处理,处理后的 纯文本 再存储或输出。 -
前端
:评论区的渲染逻辑必须修改。从
data-*属性读取数据后,如果要作为HTML显示,必须确保该数据是可信的(来自后端已净化的输出)。更安全的做法是,后端直接返回净化后的HTML片段,前端使用.html();或者后端返回纯文本,前端使用.text()设置。绝对避免将用户输入的原始数据从属性中读出后直接当作HTML使用。
属性XSS快速排查清单 :
| 检查点 | 问题描述 | 检查方法 |
|---|---|---|
| 输出点上下文 | 用户数据被放入HTML属性值中 |
审查所有模板文件和前端JS,搜索
src=”…“
、
href=”…“
、
data-xxx=”…“
等模式,看引号内的内容是否包含动态变量。
|
| 转义完整性 | 属性值中的引号、尖括号未转义 |
输入包含
”
、
’
、
<
、
>
的测试字符串,查看页面源码,检查这些字符是否被转换为HTML实体(
"
、
'
、
<
、
>
)。
|
| 前端DOM操作 |
使用
.innerHTML
/
.html()
等API,其参数包含来自属性(如
.data()
)的用户数据
|
搜索前端JS代码中的
.innerHTML
、
.outerHTML
、
.html()
、
.append()
,检查其参数来源。特别关注来自
.data()
、
.attr()
或
getAttribute()
的数据是否直接拼接。
|
| 框架危险API |
使用了
v-html
、
dangerouslySetInnerHTML
等
|
在Vue项目中搜索
v-html
指令;在React项目中搜索
dangerouslySetInnerHTML
。检查其绑定值是否可能包含用户控制的、未净化的数据。
|
| CSP策略强度 |
CSP头允许
unsafe-inline
,使得内联事件处理器可执行
|
使用浏览器开发者工具查看
Content-Security-Policy
响应头,检查
script-src
指令是否包含
‘unsafe-inline’
。理想情况应禁止。
|
挖掘和防御属性XSS是一个细致活,它考验的是你对Web应用数据流贯穿始终的理解。从后端的参数接收,到中间的数据处理与存储,再到前端的拼接与渲染,任何一个环节的疏忽都可能打开一道缺口。真正的安全,来自于将“对用户输入保持不信任,并在正确的上下文中进行编码”这一原则,落实到每一行代码和每一次部署中。
8164

被折叠的 条评论
为什么被折叠?



