什么是XSS
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击
。攻击者通过在目标网站上注入恶意脚本
,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
XSS 漏洞的发生和修复
小案例
开发一个搜索页,根据 URL
参数决定关键词的内容。HTML代码如下:
<input type="text" value="<%= getParameter("keyword") %>"> <button>搜索</button> <div> 您搜索的关键词是:<%= getParameter("keyword") %></div>
当浏览器请求http://xxx/search?keyword="><script>alert('XSS')</script>
时,服务端会解析出请求参数keyword
,得到"><script>alert('XSS')</script>
,拼接到 HTML 中返回给浏览器。形成如下HTML:
<input type="text" value=""><script>alert('XSS');</script>"> <button>搜索</button> <div>您搜索的关键词是:"><script>alert('XSS');</script> </div>
浏览器无法分辨出 <script>alert('XSS');</script>
是恶意代码,因而将其执行。
这里不仅仅 div 的内容被注入了,而且 input 的 value 属性也被注入,alert会弹出两次。
其实,这只是浏览器把用户的输入当成了脚本进行了执行。那么只要告诉浏览器这段内容是文本就可以了。
对代码片段进行转译
<input type="text" value="<%= escapeHTML(getParameter("keyword")) %>"> <button>搜索</button> <div> 您搜索的关键词是:<%= escapeHTML(getParameter("keyword")) %> </div>
function escapeHTML(text) {return text.replace(/[<>\"&\'\/]/g, function(match, pos, originalText) {switch(match) {case "<":return "<";case ">":return ">";case "&":return "&";case "\"":return """;case "\'":return "'";case "/":return "/";}}) }
经过了转义函数的处理后,最终浏览器接收到的响应为:
<input type="text" value=""><script>alert('XSS');</script>"> <button>搜索</button> <div>您搜索的关键词是:"><script>alert('XSS');</script> </div>
恶意代码都被转义,不再被浏览器执行,而且搜索词能够完美的在页面显示出来。
结论
:
-
页面中包含的
用户输入内容
都在固定的容器或者属性内,以文本
的形式展示。* 攻击者利用这些页面的用户输入区域
,拼接特殊格式的字符串,突破原有位置的限制,形成了代码片段。* 攻击者通过在目标网站上注入脚本
,使之在用户的浏览器上运行,从而引发潜在风险。* 通过 HTML 转义,可以一定程度上
防止 XSS 攻击。#### 注意特殊的 HTML 属性、JavaScript API
有些情况,虽然对代码进行了转义,但还是会有风险:
<a href="<%= escapeHTML(getParameter("redirect_to")) %>">跳转...</a>
这段代码,当攻击 URL 为 http://xxx/?redirect_to=javascript:alert('XSS')
,服务端响应就成了:
<a href="javascript:alert('XSS')">跳转...</a>
虽然代码不会立即执行,但一旦用户点击 a
标签时,浏览器会就会弹出“XSS”。
用户的数据并没有在位置上突破我们的限制,仍然是正确的 href 属性。但其内容并不是我们所预期的类型。
原来不仅仅是特殊字符,javascript:
如果出现在特定的位置也会引发 XSS 攻击。
可以通过设置白名单的方式解决:
// 根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等 allowSchemes = ["http", "https"]; valid = isValid(getParameter("redirect_to"), allowSchemes); if (valid) {<a href="<%= escapeHTML(getParameter("redirect_to"))%>">跳转...</a> } else {<a href="/404">跳转...</a> }
结论:
-
仅仅对插入到html中的代码进行转译还不够。* 对于链接跳转,如
<a href="xxx"
或location.href="xxx"
,要检验其内容,禁止以javascript:
开头的链接,和其他非法的 scheme。漏洞总结:
-
在 HTML 中内嵌的文本中,恶意内容以
script 标签
形成注入。* 在标签属性
中,恶意内容包含引号,从而突破属性值的限制
,注入其他属性或者标签。* 在标签的 href、src 等属性中,包含javascript:
等可执行代码。### XSS 分类
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。
存储型 XSS(持久型)
存储型 XSS 的攻击步骤:
1.攻击者将恶意代码提交到目标网站的数据库
中。 2.用户打开目标网站时,网站服务端将恶意代码从数据库取出
,拼接在 HTML 中返回给浏览器。 3.用户浏览器接收到响应后解析执行
,混在其中的恶意代码也被执行。 4.恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为
,调用目标网站接口,执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖
、商品评论
、用户私信
等。
反射型 XSS(非持久型)
反射型 XSS 的攻击步骤:
1.攻击者构造出特殊的 URL
,其中包含恶意代码。 2.用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出
,拼接在 HTML 中返回给浏览器。 3.用户浏览器接收到响应后解析执行
,混在其中的恶意代码也被执行
。 4.恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为
,调用目标网站接口,执行攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索
、跳转
等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
DOM 型 XSS
基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击。
DOM 型 XSS 的攻击步骤:
1.攻击者构造出特殊的 URL
,其中包含恶意代码。 2.用户打开带有恶意代码的 URL
。 3.用户浏览器接收到响应后解析执行
,前端 JavaScript 取出 URL 中的恶意代码并执行。 4.恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为
,调用目标网站接口,执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端
完成,属于前端 JavaScript 自身的安全漏洞
,而其他两种 XSS 都属于服务端的安全漏洞
。
举个例子:
<h2>XSS: </h2> <input type="text" id="input"> <button id="btn">Submit</button> <div id="div"></div> <script> const input = document.getElementById('input');const btn = document.getElementById('btn');const div = document.getElementById('div');let val; input.addEventListener('change', (e) => {val = e.target.value;}, false);btn.addEventListener('click', () => {div.innerHTML = `<a href=${val}>testLink</a>`}, false); </script>
点击 Submit
按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:
'' οnclick=alert(/xss/)
用户提交之后,页面代码就变成了:
<a href onlick="alert(/xss/)">testLink</a>
此时,用户点击生成的链接,就会执行对应的脚本:
XSS 攻击的预防
预防DOM型 XSS 攻击
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等。
如果用 Vue/React 技术栈,使用 v-html
/dangerouslySetInnerHTML
功能时,可以配合DOMPurify
插件来处理插入的数据。
DOM 中的事件监听器,如 location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
<!-- 内联事件监听器中包含恶意代码 --> <img οnclick="UNTRUSTED" οnerrοr="UNTRUSTED" src="data:image/png,"> <!-- 链接内包含恶意代码 --> <a href="UNTRUSTED">1</a> <script> // setTimeout()/setInterval() 中调用恶意代码 setTimeout("UNTRUSTED") setInterval("UNTRUSTED") // location 调用恶意代码 location.href = 'UNTRUSTED' // eval() 中调用恶意代码 eval("UNTRUSTED") </script>
其他 XSS 防范措施
CSP(Content Security Policy)
严格的 CSP 在 XSS 的防范中可以起到以下的作用:
-
禁止加载外域代码
,防止复杂的攻击逻辑。 -
禁止外域提交
,网站被攻击后,用户的数据不会泄露到外域。 -
禁止内联脚本执行
(规则较严格,目前发现 GitHub 使用)。 -
禁止未授权的脚本执行
(新特性,Google Map 移动版在使用)。 -
合理使用上报
可以及时发现 XSS,利于尽快修复问题。
两种方法可以启用 CSP。一种是通过 HTTP 头信息的Content-Security-Policy
的字段。另一种是通过网页的<meta>
标签。
HTTP 头信息的Content-Security-Policy
(首选):
"Content-Security-Policy:" 策略 "Content-Security-Policy-Report-Only:" 策略
通过网页的<meta>
标签:
<meta http-equiv="content-security-policy" content="策略"> <meta http-equiv="content-security-policy-report-only" content="策略">
default-src ‘none’; script-src ‘self’; connect-src ‘self’; img-src ‘self’; style-src ‘self’;
该策略允许加载同源的图片、脚本、AJAX和CSS资源,并阻止加载其他任何资源,对于大多数网站是一个不错的配置。
输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
其他安全措施
-
HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
Set-Cookie: Name=Value; expires=Wednesday, 01-May-2014 12:45:10 GMT; HttpOnly
-
验证码:防止脚本冒充用户提交危险操作。
CSRF
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站
,在第三方网站中,向被攻击网站发送跨站请求
。利用受害者在被攻击网站已经获取的注册凭证
,绕过
后台的用户验证
,达到冒充用户对被攻击的网站执行某项操作的目的。
一个典型的CSRF攻击有着如下的流程:
-
用户登录a.com
,并保留了登录凭证(Cookie)。 -
攻击者引诱用户访问了b.com
。 -
b.com
向a.com
发送了一个请求:a.com/act=xx
-
a.com
接收到请求后,对请求进行验证,并确认是用户的凭证
,误以为是受害者自己发送的请求。 -
a.com
正常执行了act=xx。 -
攻击完成,攻击者在用户不知情的情况下,冒充用户,让
a.com
执行了自己定义的操作。
几种常见的攻击类型
-
GET类型的CSRF
GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker
发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。
-
POST类型的CSRF
CSRF通常使用的是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST><input type="hidden" name="account" value="xiaoming" /><input type="hidden" name="amount" value="10000" /><input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script>
-
链接类型的CSRF
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">重磅消息!! <a/>
由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。
CSRF的特点
-
攻击一般发起在
第三方网站
,而不是被攻击的网站。被攻击的网站无法防止攻击发生。* 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作
;而不是直接窃取数据。* 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”
。* 跨站请求可以用各种方式:图片URL
、超链接
、CORS
、Form提交
等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
防御策略
同源检测
因为CSRF通常发生在外域,针对外域做以下防御,在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
-
Origin Header* Referer Header这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。服务器可以通过解析这两个Header中的域名,确定请求的来源域。
Referrer Policy: strict-origin-when-cross-origin
Origin Header
Request Headers:origin: https://www.jd.com
在部分与CSRF有关的请求中,请求的Header中会携带Origin字段。字段内包含请求的域名(不包含path及query)。
如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。
但是Origin在以下两种情况下并不存在:
-
IE11同源策略: IE 11 不会在跨站CORS请求上添加Origin标头,Referer头将仍然是唯一的标识。* 302重定向: 在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。##### Referer Header
Request Headers:referer: https://m.knowbox.cn/
Referrer Policy States:
新的Referrer规定了五种策略:
-
No Referrer
:任何情况下都不发送Referrer信息 -
No Referrer When Downgrade
:仅当协议降级(如HTTPS页面引入HTTP资源)时不发送Referrer信息。是大部分浏览器默认策略。 -
Origin Only
:发送只包含host部分的referrer. -
Origin When Cross-origin
:仅在发生跨域访问时发送只包含host的Referer,同域下还是完整的。与Origin Only的区别是多判断了是否Cross-origin。协议、域名和端口都一致,浏览器才认为是同域。 -
Unsafe URL
:全部都发送Referrer信息。最宽松最不安全的策略。
设置Referrer Policy的方法有三种:
1.在CSP设置
Content-Security-Policy: referrer no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|unsafe-url;
2.页面头部增加meta标签
<meta name="referrer" content="no-referrer|no-referrer-when-downgrade|origin|origin-when-crossorigin|unsafe-url">
3.a标签增加referrerpolicy属性
<a href="http://example.com" referrer="no-referrer|origin|unsafe-url">xxx</a>
因此,攻击者可以在自己的请求中隐藏Referer:
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
那么这个请求发起的攻击将不携带Referer。所以这种设置安全性不太高。
另外在以下情况下Referer没有或者不可信:
1.IE6、7下使用window.location.href=url进行界面的跳转,会丢失Referer。2.IE6、7下使用window.open,也会缺失Referer。3.HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失。4.点击Flash上到达另外一个网站的时候,Referer不太可信。#### CSRF Token
CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
原理:
CSRF Token的防护策略分为三个步骤:
1.将CSRF Token生成输出到页面中
首先,用户打开页面的时候,服务器需要给这个用户生成一个Token
,该Token通过加密算法对数据进行加密
,一般Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了,否则又会被攻击者冒用。
2.页面提交的请求携带这个Token
对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
这样,就把Token以参数的形式加入请求了。
3.服务器验证Token
是否正确
当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。
但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个Form及Ajax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。
Samesite Cookie属性
防止CSRF攻击的办法已经有上面的预防措施。为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax
Samesite=Strict
如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie。
SamesiteCookie目前有一个致命的缺陷:不支持子域。跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。
另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版Chrome和Firefox支持以外,Safari以及iOS Safari都还不支持。
Samesite=Lax
如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。
总结
常见防御 XSS 攻击措施:
-
CSP
-
HttpOnly 防止劫取 Cookie
-
用户的输入检查
-
服务端的输出检查
常见防御 CSRF 攻击措施:
-
同源检测
-
Token验证
-
分布式校验
-
双重Cookie验证
-
Samesite Cookie