浅谈XSS和CSRF

338 阅读12分钟

XSS

什么是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 "&lt;";
            case ">":
                return "&gt;";
            case "&":
                return "&amp;";
            case "\"":
                return "&quot;";
            case "\'":
                return "&#x27;";
            case "/":
                return "&#x2F;";
        }
    })
}

经过了转义函数的处理后,最终浏览器接收到的响应为:

<input type="text" value="&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;">
<button>搜索</button>
<div>
  您搜索的关键词是:&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;
</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(&#x27;XSS&#x27;)">跳转...</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 才能生效,攻击者往往会结合多种手段诱导用户点击。

image.png

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 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:

'' onclick=alert(/xss/)

用户提交之后,页面代码就变成了:

<a href onlick="alert(/xss/)">testLink</a>

此时,用户点击生成的链接,就会执行对应的脚本:

image.png

XSS 攻击的预防

预防DOM型 XSS 攻击

在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。

如果用 Vue/React 技术栈,使用 v-html/dangerouslySetInnerHTML 功能时,可以配合DOMPurify插件来处理插入的数据。

DOM 中的事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

<!-- 内联事件监听器中包含恶意代码 -->
<img onclick="UNTRUSTED" onerror="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:" 策略

image.png

通过网页的<meta>标签:

<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">
default-srcnone’; script-src ‘self’; connect-src ‘self’; img-src ‘self’; style-src ‘self’;

该策略允许加载同源的图片、脚本、AJAX和CSS资源,并阻止加载其他任何资源,对于大多数网站是一个不错的配置。

Content Security Policy 入门教程

输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

其他安全措施
  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
Set-Cookie: Name=Value; expires=Wednesday, 01-May-2014 12:45:10 GMT; HttpOnly
  • 验证码:防止脚本冒充用户提交危险操作。

XSS小游戏

CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

image.png

一个典型的CSRF攻击有着如下的流程:

  • 用户登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱用户访问了b.com
  • b.coma.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超链接CORSForm提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

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;
  1. 页面头部增加meta标签
<meta name="referrer" content="no-referrer|no-referrer-when-downgrade|origin|origin-when-crossorigin|unsafe-url">
  1. 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中了,否则又会被攻击者冒用。

  1. 页面提交的请求携带这个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

参考文章

# 前端安全系列(一):如何防止XSS攻击?

# 前端安全系列之二:如何防止CSRF攻击?

# 浅说 XSS 和 CSRF

# 内容安全策略(CSP)详解

# JavaScript:svg标签是否影响了解码顺序?