[网络安全] 如何预防XSS
XSS (Cross-Site Scripting,跨站脚本攻击) 是一种代码注入攻击。攻击者通过在目标网站注入恶意脚本,使其在用户浏览器中执行,从而窃取用户敏感信息如 Cookie 和 SessionID。
CSS 在前端已经被用了,为了避免歧义用了 XSS 作为缩写。
XSS 的本质是恶意代码与网站正常代码混在一起,浏览器无法分辨它们的可信度,最终导致恶意代码被执行。
XSS的危害
浏览器无法区分恶意代码和正常代码,它们拥有相同的权限。恶意代码可以读取用户的敏感数据,比如 cookie 和 sessionID 等等。
可能的危害如下:
- 由于恶意代码可以获取cookie和sessionID等数据,它可以获取用户的隐私数据并发送到攻击者的服务器进行收集;
- 恶意代码以用户(被攻击者)的名义,构造带有恶意代码的 URL 进行传播。
XSS的分类
根据攻击者注入恶意脚本的方式的不同,XSS 可以被分为三类:
存储型 XSS
-
存储区:后端数据库
-
插入点:HTML
-
攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库。
- 用户打开目标网站时,恶意代码从数据库取出并嵌入 HTML。
- 用户浏览器执行恶意代码,导致用户数据被窃取或伪造操作被执行。
-
特点:持久性长,危害性大。
以 POST 评论和 GET 评论为例,攻击者只要发布一条带有恶意脚本的评论,这个攻击就可能影响到所有浏览到这一条评论的其它用户。并且这条评论是存储在数据库里的,假如没有对用户提交的评论做过滤,那么这个恶意的攻击会一直存储在数据库里直到有用户浏览到它,因此说它是持久的。
下图中的步骤 1 和步骤 2 之间可能间隔很久。
案例
假如用户发布了一条带有恶意代码的评论,这条评论被存储到了数据库里。
{ "comment": "<script>alert('XSS!');</script>" }
如果网站使用服务端渲染,那么在服务端会发生模板的数据填充:
<div> 用户评论:<%= comment %> </div>
最终前端拿到的 HTML 是:
<div> 用户评论:<script>alert('XSS!');</script> </div>
恶意脚本将被执行。
客户端渲染在使用innerHTML
的时候也要十分注意。
反射型 XSS
- 插入点:HTML
- 攻击步骤:
- 攻击者构造一个带有恶意代码的 URL。
- 用户打开该 URL 时,网站从 URL 中提取恶意代码并插入 HTML。
- 用户浏览器执行恶意代码,导致数据泄露或伪造操作。
- 特点:反射型的特点是必须用户点击这个经过恶意构造的URL才会被攻击,并且脚本显现在URL上,相对来说比较容易被发现。反射型XSS有经过后端。
案例
一个搜索页面,搜索的 query 通过 url 带参。
php代码:
<!-- search.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索结果</title> </head> <body> <h1>搜索结果</h1> <div> 你搜索了: <span id="search-query"><?php echo $_GET['q']; ?></span> </div> </body> </html>
攻击示例:
攻击者可以构造一个携带恶意脚本的URL:
http://example.com/search.html?q=<script>alert('XSS Attack');</script>
用户点击该链接后,服务器会直接将
q
参数的值插入到页面中,生成的页面 HTML 如下:<div> 你搜素了: <span id="search-query"><script>alert('XSS Attack');</script></span> </div>
浏览器解析并执行了
script
标签内的内容,从而触发了 XSS 攻击。
DOM 型 XSS
- 插入点:前端 JavaScript
- 攻击步骤
- 攻击者构造一个特殊的 URL。
- 用户打开该 URL 后,前端 JavaScript 处理时会执行恶意代码。
- 恶意代码在浏览器中运行,可能导致数据泄露或伪造操作。
- 特点:DOM 型 XSS 仅发生在前端。
案例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索结果</title> </head> <body> <h1>搜索结果</h1> <div> 正在搜索: <span id="search-query"></span> </div> <script> // 获取 URL 参数中的 'q' 值 var query = new URLSearchParams(window.location.search).get('q'); // 直接将其插入到 DOM 中 document.getElementById('search-query').innerHTML = query; </script> </body> </html>
攻击者可以构造一个包含恶意代码的链接:
http://example.com/search.html?q=<script>alert('DOM XSS Attack');</script>
当链接被访问时,恶意脚本就会被插入到界面中并执行:
document.getElementById('search-query').innerHTML = "<script>alert('DOM XSS Attack');</script>";
XSS的预防
XSS 有两大要素,一是攻击者提交恶意代码,二是浏览器执行恶意代码。
转义用户输入(不实用)
为了预防攻击者提交恶意代码,可以考虑对用户的输入进行过滤。
使用合适的转义函数,例如 escapeHTML()
,将用户输入的特殊字符转换为安全的 HTML 实体。
function escapeHTML(str) {
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\//g, "/");
}
这种方法只能用于一些简单的场景。它的缺点在于:
- 如果这个转义仅发生在前端提交请求之前,攻击者仍可以手动构造请求。
- 如果这个转义发生在后端将数据写入数据库之前,也是不合理的,因为我们不知道这个数据将来应用的环境是哪里。上面这段代码的转义只能应用于浏览器环境的HTML里,转义后的数据无法在桌面客户端、安卓端等等其它环境上正常显示。
总结:
- 转义用户输入只能应用于简单的场景;
- 转义最好应用在Web前端渲染数据时,而不是用户提交数据时;
- 对于明确的类型,比如数字、电话、URL、邮箱等数据,做一下数据过滤还是很有必要的。
用户输入环节并不能很好地预防 XSS,预防 XSS 的主要工作集中在预防浏览器执行恶意代码上。
关于这一点又有两类操作:
- 防止 HTML 被注入;
- 防止 JavaScript 执行恶意代码。
预防反射型和存储型
这两种类型都是在后端服务器上完成模板拼接的过程中,将恶意代码嵌入到了 HTML 中。
两种解决方法分别是:客户端渲染、转义HTML。
纯客户端渲染
纯客户端渲染:客户端访问页面,服务端返回的页面不带有任何业务数据,由前端的 JavaScript 通过 AJAX 异步请求数据后进行填充。
纯客户端渲染可以通过 .innerText
,.setAttribute
,.style
等 API 来插入后端返回的业务内容(这些内容可能存在恶意代码),通过这种方式插入的内容不会被浏览器当成代码执行。
局限:
- 纯客户端渲染仍要注意 DOM型 XSS 攻击。
- 部分页面对于性能有较高需求,或者需要考虑SEO,仍需要服务端渲染。
转义 HTML
对于服务端渲染,在拼接带有业务数据的 HTML 时,要对 HTML 的各个插入点进行充分的转义。
一些模板引擎可能仅对 & < > " ' /
进行转义,这其实是远远不够的,插入点有很多种,包括但不限于:
-
在 HTML 内嵌的文本中,以 script 标签的形式注入;
-
在内联的 javascript 中,拼接的数据突破了原本的限制(字符串、方法名等等);
例如,a 标签的 href 可能理想状态下是插入一个路径链接,如果这个 href 插入的字符串是一个
javascript:恶意代码
的形式,当 a 标签被点击时,恶意代码就会被执行。 -
对于标签的属性,如果注入的值包含双引号,就会导致这个属性的值提前结束,从而可以注入其它属性或者标签;
例如:
注入其它属性示例
// src 是用用户数据拼接的 <img src="<%= imgSrc %>" /> // 如果用户数据如下 $imgSrc = './not-exist-img.png" onerror="javascript:恶意代码' // 那么拼接后的 img 标签就是 <img src="./not-exist-img.png" onerror="javascript:恶意代码" />
图片加载不到,会触发 onerror 中的恶意代码。
注入其它标签示例
<img src="<%= imgSrc %>" />
如果 imgSrc 插入的内容是
" /> <script src="恶意代码
,那么最后就会变成:<img src="" /> <script src="恶意代码" />
综上,转义 HTML 是一个很复杂的工作,通常在项目中会采用比较成熟的转义库。
预防DOM型
DOM型的XSS攻击主要是前端的 Javascript 代码不够严谨,把不可信的用户数据当作代码执行了。
嵌入HTML
当需要把用户输入添加到页面上时,尽量不要使用 innerHTML 和 document.write 这些API,而是使用 textContent。
同理,使用 Vue 或者 React 开发的时候,应该谨慎使用 v-html 和 dangerouslySetInnerHTML。
事件监听
避免将不可信的数据拼接给 DOM 上的 location、onclick、onerror、onload、onmouseover等属性。
动态执行JS
在 JS 中,可以通过执行 eval 函数、添加 script 标签、Function构造函数等方法动态执行 JS 代码。请确保动态执行的 JS 代码是可信的。
动态执行 JS 是一种性能很差的做法,尽量不要使用。
其它预防措施
CSP
CSP,全称 Content Security Policy。CSP 的主要目的是减少和防止 XSS 攻击。通过严格控制可以在页面上执行的脚本来源,CSP 可以有效阻止 XSS。
工作原理
通过 HTTP 头 Content-Security-Policy
来定义的。这个头部包含一系列指令,每个指令指定允许从哪些来源加载特定类型的资源。例如,script-src
指令可以用来限制从哪些域加载 JavaScript 脚本。
常见指令
default-src
: 为其他未显式指定的资源类型定义默认策略。script-src
: 限制 JavaScript 资源的加载来源。style-src
: 限制 CSS 样式表的加载来源。img-src
: 限制图像的加载来源。connect-src
: 限制 AJAX、WebSocket 等请求的目的地。font-src
: 限制字体的加载来源。child-src
:限制 Web Worker 脚本文件或者其它 iframe 等内嵌到文档中的资源来源。
语法
不同的指令之间使用 ;
分隔,一条指令由指令和允许的若干个源构成。
常用的源包括:
'self'
: 允许资源从当前域加载(同源)。'none'
: 禁止加载任何资源。'unsafe-inline'
: 允许页面内联的资源(如<style>
或<script>
标签中的内容),但存在安全风险,不推荐使用。'unsafe-eval'
: 允许使用eval()
等动态代码执行功能,也有安全风险,不推荐使用。- 具体域名: 如
https://example.com
,允许资源仅从指定的域加载。 - 协议:如
http:
、https:
,尾部有冒号,但是不需要单引号。 - 通配符
\*
: 允许资源从任何来源加载,不建议用于生产环境。
示例
Content-Security-Policy:
// 默认同源
default-src 'self';
// 只允许js从同源和具体的域名加载
script-src 'self' https://trusted-scripts.example.com;
// 允许css从同源加载,并允许内联样式(不建议)
style-src 'self' 'unsafe-inline';
// 只允许图像从指定域名加载
img-src https://images.example.com;
// 只允许网络请求(AJAX)连接到同源和指定的地址
connect-src 'self' https://api.example.com;
// 只允许字体从同源加载
font-src 'self';
// 禁止嵌入子文档
frame-src 'none';
// 禁止加载任何插件资源
object-src 'none';
// 违反CSP的行为会被报告到指定的地址
report-uri /csp-report-endpoint;
除了使用 HTTP 头部字段,也可以通过 meta 标签配置:
// meta tag <meta http-equiv="Content-Security-Policy" content="default-src https:">
限制输入内容长度
限制用户输入内容的长度可以提高构造XSS攻击的难度。
HttpOnly Cookie
当 cookie 被设置为 Httponly 时,Cookie 只能通过 HTTP 请求头(如 Set-Cookie
或 Cookie
)进行传输,而不能被 JavaScript 通过 document.cookie
等方式读取。
这种机制可以防止 cookie 信息被恶意脚本窃取。
示例HTTP头
Set-Cookie:
sessionId=abc123;
HttpOnly;
Secure;
SameSite=Strict;
sessionId=abc123
是 Cookie 的键值对;HttpOnly
:表示这个 Cookie 只能通过 HTTP 请求传输,JavaScript 不能访问;Secure
:表示这个 Cookie 只能通过 HTTPS 传输;SameSite=Strict
:限制 Cookie 仅在同站点请求时发送,防止 CSRF 攻击。
XSS的检测
在开发阶段要预防XSS漏洞,对于已经上线的项目,检测XSS漏洞的方法有:
-
使用通用 XSS 攻击字符串手动检测:
将下面这个字符串输入到网站的各个输入框,或者拼接到URL参数上,就可以进行检测。只要
alert()
被执行,就说明发现了XSS漏洞。(注意,前面的
jaVasCript:
也要一起复制)jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
-
使用一些自动扫描工具寻找XSS漏洞。
总结
- 责任:XSS防范需要前端后端共同参与;
- 转义规则:需要根据业务场景选择;
- 避免拼接HTML、避免使用内联事件。
引用
[1] 前端安全系列(一):如何防止XSS攻击? - 美团技术团队 (meituan.com)