Cross-Site Scripting (跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID等,进而危害数据安全。
Cross-Site Scripting 的英文首字母本应为 CSS,但因为 CSS 在网页涉及领域已经被广泛指层叠样式表(Cascading Style Sheets),所以将 Cross(意为“交叉”)改以交叉形的 “X” 作为缩写。
但早期的文件还是会使用 CSS 标识 Cross-Site Scripting。
XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
默认网站只能运行站内的代码,XSS 攻击能够实现把外部 JavaScript 代码注入网站内部,形成攻击。
而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。
在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。
XSS 攻击
以服务端渲染(ASP)为例:
<h1>XSS</h1>
<!-- 假设 getParameter 是后端方法,获取 URL 参数的值;此处将值直接插入在HTML中 -->
<div><%= getParameter("keyword") %><div>
如果访问 http://xxx.com?keyword=<script>alert(123)</script>,就会在页面中插入一段脚本。
不过这类注入主要针对的是传统的 web 应用,大多数浏览器已经解决了这类方式注入(通过对 URL 编码或其他方式)。
不同浏览器对 '、"、<、>等符号的 URL 编码机制不同,例如:
- IE不会对
'"<>编码,但会判断URL上的script

- Chrome、FireFox 会对
'"<>编码

以vue为例:
data: {
// 这种方式需要HTML转义 `/`,所以无法实现注入。
// content: '<script>alert(123)<\/script>',
content: '<img src="xxx" οnerrοr="alert(123)" />'
}
<div v-html="content"></div>
页面加载图片失败,会触发 onerror 事件,进而执行恶意代码 alert(123)
实际情况,攻击者可能会将 alert(123) 换成获取客户端缓存(用户信息)的代码:
data: {
content: `<img src="xxx" οnerrοr="localStorage.getItem('userInfo')" />`
}
XSS 攻击就是网页中有一块内容绑定了数据,这个数据按照 HTML 格式输出。
XSS 分类(攻击来源)
根据攻击来源,XSS 攻击可分为三类:
| 类型 | 存储区(恶意代码存放的位置) | 插入点(由谁取得恶意代码,并插入到网页上) |
|---|---|---|
| 反射型 XSS | URL | HTML |
| DOM 型 XSS | 后端数据库/前端存储/URL | 前端 JavaScript |
| 存储型 XSS | 后端数据库 | HTML |
反射型 XSS
反射型 XSS 就是利用 URL注入恶意代码。
攻击步骤
- 攻击者构造带有恶意代码的 URL
- 用户打开带有恶意代码的 URL 时,服务端将恶意代码从 URL 上取出,拼接在 HTML 中返回给浏览器。
- 浏览器解析页面,混在其中的恶意代码被执行。
- 恶意代码造成攻击。
示例
上面【XSS 攻击原理中的服务端渲染示例】就是反射型 XSS。
补充
反射型 XSS 主要就是将 URL 上的内容,未经处理(或处理的不完善)原封不动的插入到页面代码中。
攻击者一般会将带有 XSS 的链接发给用户,诱导点击。
这种漏洞需要满足的条件很多(诱导用户点击假链接、网站未作URL过滤、使用服务端渲染等),纯前端渲染中已经很少见,但由此可以衍生出来很多攻击点,需要开发者了解和注意。
DOM 型 XSS
攻击步骤
DOM 型 XSS 和 反射型 XSS 类似,只不过获取 URL 上的恶意代码是在客户端完成(前端 JavaScript)完成。
示例
<div v-html="$route.query.keyword"></div>
请求地址:
http://localhost:8080/#/?keyword=<a href="http://heiheihei.com">领10元话费</a>
或者
http://localhost:8080/#/?keyword=<img src="xxx" onerror="alert(123)" />

存储型 XSS
这种攻击常见于带有用户保存数据的网站功能,如富文本编辑器发布文章、评论等。
攻击步骤
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 浏览器解析页面,混在其中的恶意代码被执行。
- 恶意代码造成攻击。
示例
发布文章的表单:
<form action="">
<div>
<label>文章标题</label>
<input name="title" />
</div>
<div>
<label>文章内容</label>
<!--
内容一般使用富文本编辑器。
富文本编辑器最终生成的内容:HTML 格式的字符串
Web 后台会把文章内容存储到数据库
当用户查看文章详情的时候,就会按照 HTML 格式渲染这个字符串(文章内容)
-->
<Editor name="content" />
</div>
</form>
文章详情页面(以 Vue 为例):
<h1>{{ article.title }}</h1>
<div v-html="article.content"></div>
服务端渲染示例
反射型 和 存储型 在纯前端渲染中已经很少见了,尤其是在大量使用第三方框架(Vue、React、Angular等)的前提下。因为它们已经帮我们对 XSS 攻击进行了处理。
反射型 和 存储型 多发生于服务端渲染。
前端开发者可以使用 nodejs 开启一个 web 服务,使用模板引擎渲染 html 内容实现服务端渲染(不要直接使用第三方框架,因为它们的模板引擎都对 XSS 做了处理)。
反射型 XSS 示例
1、初始化项目,安装依赖
mkdir demo-ssr
cd ./demo-ssr
npm init -y
npm i express art-template express-art-template
创建目录:
├─ views
│ └─ index.html # html 模板文件
├─ app.js # web 服务启动入口文件
2、开启 web 服务,响应 html 文件内容
views/index.html:
<!--
这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server-Side Rendering: XSS</title>
</head>
<body>
<form action="/" method="GET">
<div>
<label>输入搜索关键字:</label>
<input name="keyword" type="text" />
</div>
<div>
<button>搜索</button>
</div>
</form>
<div>
xxx 的搜索结果如下:
<ul>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ul>
</div>
</body>
</html>
app.js
const express = require('express')
const fs = require('fs');
const app = express()
app.get('/', (req, res) => {
const html = fs.readFileSync('./views/index.html')
res.end(html)
})
app.listen(3000, () => {
console.log('running on http://localhost:3000')
})
nodemon ./app.js

3、使用模板引擎(art-template)渲染 HTML 内容,用数据替换模板内容
app.js:
const express = require('express')
const app = express()
app.engine('html', require('express-art-template'))
app.get('/', (req, res) => {
// render 方法:
// 1. 读取要渲染的页面模板内容(默认在 views 目录下查找文件)
// 2. 渲染:用数据(第二个参数)替换模板中的内容
// 3. 将渲染结果发送给客户端
res.render('index.html', {
keyword: 'bar'
})
})
app.listen(3000, () => {
console.log('running on http://localhost:3000')
})
index.html:
<!--
这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server-Side Rendering: XSS</title>
</head>
<body>
<form action="/" method="GET">
<div>
<label>输入搜索关键字:</label>
<input name="keyword" type="text" />
</div>
<div>
<button>搜索</button>
</div>
</form>
<div>
<!-- 修改了这里 -->
{{ keyword }} 的搜索结果如下:
<ul>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ul>
</div>
</body>
</html>

4、绑定表单输入的内容:
app.js:
const express = require('express')
const app = express()
app.engine('html', require('express-art-template'))
app.get('/', (req, res) => {
// render 方法:
// 1. 读取要渲染的页面模板内容(默认在 views 目录下查找文件)
// 2. 渲染:用数据(第二个参数)替换模板中的内容
// 3. 将渲染结果发送给客户端
res.render('index.html', {
keyword: req.query.keyword, // 通过地址栏传递文本框输入的内容,例如 ?keyword=xxx
})
})
app.listen(3000, () => {
console.log('running on http://localhost:3000')
})
修改模板,根据搜索关键字展示内容:
<!--
这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server-Side Rendering: XSS</title>
</head>
<body>
<form action="/" method="GET">
<div>
<label>输入搜索关键字:</label>
<input name="keyword" type="text" />
</div>
<div>
<button>搜索</button>
</div>
</form>
<!-- 当 keyword 有值时展示搜索结果 -->
{{ if keyword }}
<div>
{{ keyword }} 的搜索结果如下:
<ul>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ul>
</div>
{{ /if}}
</body>
</html>

5、原文输出
默认模板引擎会将内容进行转义处理,使用原文输出 @ 取消转义。
<!-- 当 keyword 有值时展示搜索结果 -->
{{ if keyword }}
<div>
<!-- 原文输出 -->
{{@ keyword }} 的搜索结果如下:
<ul>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ul>
</div>
{{ /if}}

以上是直接在 URL 上编写 keyword 的值,也可以直接在文本框中输入,点击【搜索】。
现在的页面就有 反射型 XSS 攻击风险。
存储型 XSS 示例
1、 创建文章详情页面 article.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>存储型 XSS</title>
</head>
<body>
<h1>文章标题:{{ article.title }}</h1>
<div>文章内容:</div>
<div>{{@ article.content }}</div>
</body>
</html>
2、服务端渲染文章详情页面,返回文章详情数据
app.js 添加内容:
// 模拟数据库存储的文章信息
const articles = [
{ id: 1, title: 'article 1', content: 'article 1 content' },
{ id: 2, title: 'article 2', content: 'article 2 content' },
{ id: 3, title: 'article 3', content: 'article 3 content' },
]
app.get('/article/:id', (req, res) => {
const article = articles.find(item => item.id == req.params.id)
if (!article) {
return res.status(404).send('文章不存在')
}
res.render('article.html', {
article
})
})

假如攻击者发布的文章包含恶意代码:
// 模拟数据库存储的文章信息
const articles = [
{ id: 1, title: 'article 1', content: '<script>alert(123)</script>' },
{ id: 2, title: 'article 2', content: 'article 2 content' },
{ id: 3, title: 'article 3', content: 'article 3 content' },
]

XSS 总结
攻击注入方式
- 用户的 UGC 内容:用户生成的内容
- 用户通过文本框、编辑器等方式生成的内容
- 来在第三方的链接
- URL 参数
- POST 参数
- Referer(可能来自不可信的来源)
- Cookie(可能来自其他子域注入)
注入点
- 在 HTML 中内嵌的文本中,以
<script>标签注入恶意代码
<!-- dangerCode = '<script>alert(123)</script>' -->
<div>{{@ dangerCode }}</div>
<!--
渲染为:
<div><script>alert(123)</script></div>
-->
- 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串、变量、方法名等)
<!-- dangerCode = 'null; alert(123)' -->
<script>
var data = {{ dangerCode }}
</script>
<!--
渲染为:
<script>
var data = null; alert(123)
</script>
-->
- 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或标签
<!-- dangerCode = '"></a><a href="xxx.html' -->
<a href="{{ dangerCode }}">链接</a>
<!--
渲染为:
<a href=""></a><a href="xxx.html">链接</a>
-->
- 在标签
href、src等属性中,包含javascript:等可执行代码
<!-- dangerCode = 'javascript: alert(123)' -->
<a href="{{ dangerCode }}">链接</a>
<!--
渲染为:
<a href="javascript: alert(123)">链接</a>
-->
- 在
onload、onerror、onclick等事件中,注入不受控制代码。
<!-- dangerCode = 'xxx" οnerrοr="alert(123)' -->
<img src="{{@ dangerCode }}" />
<!--
渲染为:
<img src="xxx" οnerrοr="alert(123)" />
-->
- 在
style属性和标签中,包含类似backgroung-image:url("javascript:...");的代码(新版本浏览器已经可以防范) - 在
style属性和标签中,包含类似expression(...)的 CSS 表达式代码(新版本浏览器已经可以防范)
总之,如果开发者没有将用户输入的文本进行合适的过滤,就贸然插入到 HTML 中,就很容易造成注入漏洞。
永远不要相信用户的输入
XSS 的危害
XSS 可以直接在网页中运行 web 脚本,所以可以进行很多恶意操作,例如:
- 窃取 Cookie
- 按键记录和钓鱼
- 未授权操作
- 获取页面数据
- 获取本地存储数据
- 劫持前端逻辑
- 发送请求
- 图片、表单等
- 偷取网站任意数据
- 偷取用户密码和登录状态
- 欺骗用户
- 利用虚假输入表单骗取用户个人信息
- 显示伪造的文章或图片
- 等
XSS 的预防
通过前面的介绍可以得知,XSS 攻击有两大要素:
- 攻击者提交恶意代码
- 浏览器执行恶意代码
预防攻击者提交恶意代码
结论:在输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。
针对第一个要素,也许我们可以在用户输入时过滤掉恶意代码。
但是这个方式范围有限,且有很多问题。
范围有限
- URL地址:我们无法阻止攻击者在地址栏直接添加恶意代码。
- 前端捕获用户输入过滤:对于提交用户输入(关键字搜索、发布文章等)的操作,我们也无法阻止攻击者绕过前端,直接构造请求。
- 服务端捕获用户输入过滤
所以在提交过程上,我们只能在服务端拦截用户输入,在存入数据库或响应给客户端前进行过滤,达到预防提交恶意代码的目的。
但是这仍然会因输出不确定,导致很多问题。
输出不确定
过滤恶意代码一般是对内容进行转义。
在服务端存储数据库前对输入进行过滤(转义),表示输出的内容一定是转义后带有“乱码符号”的字符串。
例如:5 < 7 转义后:5 < 7
而这个内容不确定要输出到哪里:
- 提供给客户端,可能会经过
escapeHTML()编码 - 前端展示
- 作为 HTML 拼接页面
- 作为 Ajax 请求的数据,用于Vue 模板、内容长度计算、alert 等
结论
这个方式解决范围小,且有很大的不确定性问题,所以不建议在输入的时候对数据进行转义,而应该在输出的时候。
对于明确的输入类型,例如数字、URL、电话号码、邮箱等内容,进行输入过滤还是必要的。
既然输入过滤并非完全可靠,我们就要通过防止第二个要素“浏览器执行恶意代码”来防范 XSS。这部分分为两类:
- 防止 HTML 中出现注入
- 防止 JavaScript 执行时,执行恶意代码
预防浏览器执行恶意代码
预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是传统 web 应用使用服务端渲染方式,在服务端取出攻击者编写的带有恶意代码的 “数据” 后,插入到响应的 HTML 里,才被浏览器执行的。
预防这两个漏洞,有两个常见做法:
- 改成纯前端渲染(客户端渲染),把代码和数据分隔开
- 对 HTML 做充分转义
纯前端渲染
纯前端渲染就是客户端渲染,如 Vue React 开发的项目,过程:
- 浏览器加载静态 html,不包含任何后台数据
- 前端 JavaScript 通过 Ajax 获取业务数据
- 使用 DOM API 插入到页面中,避免直接拼接 html 或 js 代码
纯前端渲染还需注意避免 DOM 型 XSS 漏洞,参见下文。
在很多后台管理系统中,采用纯前端渲染是非常合适的。
但对于性能要求高、或有 SEO 需求的、需要使用服务端渲染的页面,仍要面对拼接 HTML 的问题。
转义 HTML
如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
转义就是将敏感字符替换成 HTML 可以识别(JS 和 CSS 识别不了)的编码:
// 转义html
const escapeHTML = (str = '') => {
str = str.replace(/</g, '<')
str = str.replace(/>/g, '>')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, ''')
return str
}
// 转义js
const escapeJS = (str = '') => {
str = str.replace(/"/g, '\\"')
return str
}
大多数模板引擎都支持基本的 HTML 转义预防 XSS 攻击,不过通常只有一个规则,就是把 & < > " ' /这几个字符转义掉,虽然能起到一定的 XSS 防护作用,但并不完善:
| XSS 安全漏洞 | 简单转义是否有防护作用 |
|---|---|
| HTML 标签文字内容 | 有 |
| HTML 属性值 | 有 |
| CSS 内联样式 | 无 |
| 内联 JavaScript | 无 |
| 内联 JSON | 无 |
| 跳转链接 | 无 |
所以要完善 XSS 的防护措施,要使用更完善更细致的转义策略,在不同的HTML上下文里要使用相应的转义规则。
使用专业的转义库,例如 js-xss,它既能在 node 环境(服务端)运行,也可以在浏览器环境(客户端)运行。
预防 DOM 型 XSS 攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
本质上也是将恶意代码作为“代码”而不是“文本”去使用。
例如以下几个地方要特别注意使用的内容是否可信:
- document API,拼接 HTML 代码:
innerHTML()outerHTML()document.write()
- window API,将字符串参数作为 JS 代码执行:
setTimeOut()setInterval()
- JavaScript API,将字符串参数作为 JS 代码执行:
eval()
- DOM 中的内联事件监听器:
locationonclickonerroronloadonmouseover- 等任意可插入不可信数据的地方
- 标签属性:
<a>标签的 `href``<img>标签的src- 等任意可插入不可信数据的属性
对于文本内容应尽量使用 textContent()、setAttribute() 等。
如果用 Vue/React 开发,并且不使用 v-html/dangerouslySetInnerHTML,就会在前端 render 避免 XSS 的隐患(内部使用 text 文本类的document API,而不是 html)。
其他 XSS 预防措施
CSP
内容安全策略(CSP)用于检测和减轻用于 Web 站点的特定类型的攻击,例如 XSS 和 数据注入等。
该安全策略的实现基于一个称作
Content-Security-Policy的 HTTP 头。
CSP就是通过 HTTP 协议,允许网站过滤掉一些内容,主要目标就是减少和报告 XSS 攻击。
HTTP 响应头
Content-Security-Policy允许站点管理者控制用户代理能够为指定的页面加载哪些资源。除了少数例外情况,设置的政策主要涉及指定服务器的源和脚本结束点。这将帮助防止跨站脚本攻击(XSS)。
配置方式:
- 服务器返回
Content-Security-PolicyHTTP头部 - 使用
<meta>标签配置Content-Security-Policy
严格的 CSP 在 XSS 防范中可以起到以下作用:
- 禁止加载外域代码,防止复杂的攻击逻辑
- 禁止外域提交,网站被攻击后,用户的数据不会被泄漏到外域
- 禁止内联脚本执行
- 禁止未授权的脚本执行
- 合理使用上报可以及时发现 XSS,利于尽快修复问题
注意:IE 并不兼容此功能。
X-XSS-Protection
和 CSP 一样,也是通过配置 HTTP 响应头 X-XSS-Protection预防 XSS 攻击。
它主要靠 浏览器 来检测 来自 URL 上的 反射型 XSS 攻击,当检测到 XSS 攻击时,浏览器将停止加载页面。
相比 CSP 已经过时,现代浏览器几乎不支持,但仍可以作为不支持 CSP 的旧版浏览器的一种保护措施。
例如,京东就配置了该响应头:

1;mode=block
启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。
其他主动预防
Web 没有绝对的安全,只能尽可能地增加安全策略,增加攻击难度,例如:
- 对于不受信任的输入,限定一个合理的长度
- 使用验证码,防止脚本冒充用户提交危险操作
- 限制访问 Cookie,设置
Secure和HttpOnly - …
XSS 检测
学会了 XSS 以及如何预防,还需要学会如何检测,尽可能确保安全。
可以手动发起 XSS 攻击,也有自动扫描工具寻找 XSS 漏洞。
详细参考:前端安全系列(一):如何防止XSS攻击? 中的 《XSS 的检测》
XSS攻击全解析:原理、分类与防御策略
本文详细讲解了XSS攻击的原理,包括反射型、DOM型和存储型的区别,以及服务端渲染的示例。同时阐述了预防XSS的策略,从输入过滤到转义和安全措施如CSP和X-XSS-Protection的应用。
6276

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



