XSS
一、什么是XSS?
- Cross-Site Scripting(跨站脚本攻击)为了与CSS作区分故简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
- 本质
- 恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
- 可能被注入恶意脚本的内容
- 来自用户的 UGC 信息
- 来自第三方的链接
- URL 参数
- POST 参数
- Referer (可能来自不可信的来源)
- Cookie (可能来自其他子域注入)
二、XSS攻击方式
-
分为三种:存储型 、反射型 、DOM 型
-
存储型XSS
- 它不需要用户手动触发,所有浏览者访问页面时都会被XSS
- 常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等
-
反射型XSS
- 需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击
- 常见于通过 URL 传递参数的功能,如网站搜索、跳转等
-
DOM型XSS
- 属于前端 JavaScript 自身的安全漏洞
- 属于前端 JavaScript 自身的安全漏洞
-
区别
特征 存储型 XSS 反射型 XSS DOM 型 XSS 存储位置 恶意代码存储在服务器数据库 恶意代码不存储,仅存在于URL参数 恶意代码不存储,存在于URL片段 触发条件 用户访问被感染的页面 用户点击包含恶意参数的链接 用户点击包含恶意片段(#)的链接 数据流 服务器 → 用户浏览器 服务器反射参数 → 用户浏览器 完全在客户端处理(不经过服务器) 持久性 长期存在(直到数据被删除) 一次性(仅当用户点击链接时) 一次性(仅当用户点击链接时) 攻击范围 所有访问该页面的用户 仅点击特定链接的用户 仅点击特定链接的用户
三、常见的XSS攻击场景
-
社交媒体评论——存储型XSS
- 攻击者在评论区提交恶意脚本(如
<script>sendCookiesToAttacker()</script>
),该评论被存储到服务器。其他用户访问该页面时,恶意脚本自动加载并窃取其会话Cookie。
<!-- 用户评论内容 --> <div class="comment"> 这篇文章写得真好!<script>sendCookiesToAttacker()</script> </div>
- 攻击者在评论区提交恶意脚本(如
-
搜索功能——反射型XSS
- 网站搜索功能未过滤输入参数,攻击者构造恶意链接:
https://example.com/search?q=<script>alert('XSS攻击')</script>
,用户点击链接后,服务器返回的页面包含未过滤的恶意脚本。
- 网站搜索功能未过滤输入参数,攻击者构造恶意链接:
-
单页面应用路由——DOM型XSS
- SPA前端路由根据URL参数动态加载内容,但未对参数过滤。攻击者构造链接:
https://example.com/#/profile?username=<script>alert('XSS攻击')</script>
,用户点击链接,前端脚本将username
参数直接渲染到页面,触发XSS。
- SPA前端路由根据URL参数动态加载内容,但未对参数过滤。攻击者构造链接:
四、防御策略
-
预防存储型XSS
-
输入过滤
-
对于明确的输入类型
- 如数字、URL、电话号码、邮件,使用
escapeHTML()
可以把用户的输入内容进行转义
原始字符 转义后实体编码 <
<
>
>
&
&
"
"
'
'
- 如数字、URL、电话号码、邮件,使用
// 用户输入:<script>alert('XSS攻击')</script> const escaped = escapeHTML(userInput); document.getElementById('content').innerHTML = escaped; // 输出:<script>alert('XSS攻击')</script>
- 更推荐使用成熟且完善的现有库
// 使用 DOMPurify(支持更复杂的净化) import DOMPurify from 'dompurify'; DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] });
- 特殊场景补充
场景 额外需要转义的字符 方法 URL参数 %
#
配合使用 encodeURIComponent
CSS属性 \
()
使用 CSS.escape()
JSON数据 \u2028
\u2029
JSON.stringify
-
-
对于不明确的类型
- 最好不要使用输入过滤,因为当把转义后的数据发送到后端再回到前端,赋值给一个变量之后,展示出来的效果会变成乱码(如
5<7
会变成5<7
)
- 最好不要使用输入过滤,因为当把转义后的数据发送到后端再回到前端,赋值给一个变量之后,展示出来的效果会变成乱码(如
-
-
预防存储型XSS和反射型XSS
-
纯前端渲染
-
现代框架的自动防护
-
React:默认转义所有插值内容
function UserContent({ text }) { return <div>{text}</div>; // 自动转义 `<` `>` 等字符 } // 用户输入 `<script>alert('XSS攻击')</script>` 会显示为文本,不会执行
-
Vue:模板插值自动编码
<template> <div>{{ userInput }}</div> <!-- 自动转换为文本 --> </template>
-
Angular:插值绑定安全处理
<div>{{ userInput }}</div> <!-- 输出内容自动转义 -->
安全API 危险替代品 场景 textContent
innerHTML
插入纯文本内容 setAttribute
innerHTML
设置元素属性 document.createElement
+appendChild
insertAdjacentHTML
动态创建节点 -
-
转义HTML
-
使用更完善更细致的转义库
org.owasp.encoder
,以下代码引用自 org.owasp.encoder 的官方说明。<!-- HTML 标签内文字内容 --> <div><%= Encode.forHtml(UNTRUSTED) %></div> <!-- HTML 标签属性值 --> <input value="<%= Encode.forHtml(UNTRUSTED) %>" /> <!-- CSS 属性值 --> <div style="width:<= Encode.forCssString(UNTRUSTED) %>"> <!-- CSS URL --> <div style="background:<= Encode.forCssUrl(UNTRUSTED) %>"> <!-- JavaScript 内联代码块 --> <script> var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>"; alert(msg); </script> <!-- JavaScript 内联代码块内嵌 JSON --> <script> var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>'); </script> <!-- HTML 标签内联监听器 --> <button onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');"> click me </button> <!-- URL 参数 --> <a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top"> <!-- URL 路径 --> <a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>"> <!-- URL. 注意:要根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等 --> <a href='<%= urlValidator.isValid(UNTRUSTED) ? Encode.forHtml(UNTRUSTED) : "/404" %>'> link </a>
-
-
-
预防DOM型XSS
- 使用
.innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用.textContent
、.setAttribute()
- DOM 中的内联事件监听器,如
location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的href
属性,JavaScript 的eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行
- 使用
-
其他预防方法
-
httpOnly: 在 cookie 中设置 HttpOnly 属性后,js脚本将无法读取到 cookie 信息。
// 后端实现 app.post('/login', (req, res) => { res.cookie('sessionID', 'xxxx', { httpOnly: true, // 关键:禁止JS访问 secure: true, // 仅通过HTTPS传输 sameSite: 'Lax' // 防御CSRF }); res.send('登录成功'); });
-
白名单:
-
输入白名单验证
function validateInput(input) { // 只允许字母、数字、空格以及特定的标签 const whiteListPattern = /^[a-zA-Z0-9\s<>/bB/iI]*$/; return whiteListPattern.test(input); } const userInput = "<script>alert('xss')</script>"; if (!validateInput(userInput)) { // 处理非法输入 console.log("Invalid input!"); }
-
HTML标签/属性白名单
// 使用DOMPurify库配置白名单 import DOMPurify from 'dompurify'; const dirtyHTML = '<b class="safe">合法内容</b><script>恶意代码</script>'; const cleanHTML = DOMPurify.sanitize(dirtyHTML, { ALLOWED_TAGS: ['b', 'i', 'em'], // 允许的标签 ALLOWED_ATTR: ['class', 'style'], // 允许的属性 FORBID_TAGS: ['style', 'script'] // 强制禁止的标签 }); console.log(cleanHTML); // 输出:<b class="safe">合法内容</b>
-
-
CSP:
-
基础配置
Content-Security-Policy: default-src 'self'; # 默认只允许同源资源 script-src 'self' # 脚本仅限同源 https://trusted.cdn.com 'nonce-abc123'; # 允许带特定nonce的内联脚本 style-src 'self' 'unsafe-inline'; # 允许内联样式(慎用) img-src * data:; # 允许所有图片源(根据需求收紧) font-src 'self'; frame-src 'none'; # 禁止嵌入iframe report-uri /csp-report; # 违规报告地址
-
-