前言
我们从攻击、防御两个视角,简要学习前端范畴内常见的安全问题,包括 XSS、CSRF、SQL 注入、DOS 等。那我们开始吧!
开门见山,我们首先应该从两个方面看待web开发的安全问题,如果web开发是一座城,那么hacker就是攻城的敌人,而我们应该作为守城人对其进行防御。所以,守城的第一步是了解敌人!
常见的安全问题及防御
Cross-Site Scripting(XSS)
也就是跨站脚本攻击:是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS。
用户是通过哪种方法“注入”恶意脚本的呢?
不仅仅是业务上的“用户的 UGC 内容”可以进行注入,包括 URL 上的参数等都可以是攻击的来源。在处理输入时,以下内容都不可信:
- 来自用户的UGC信息
- 来自第三方的链接
- URl参数
- POST参数
- Referer(可能来自不可信的来源)
- Cookie (可能来自其他子域注入)
XSS的类型
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。
1. 反射型(reflected xss)
基于反射性攻击,依靠服务端返回的脚本,在客户端执行,形成web攻击。
发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,XSS随响应内容一起返回给浏览器,最后浏览器解析执行XSS代码,这个过程就像一次发射,所以叫反射型XSS。
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
public async render(ctx){
const {param}=ctx.query;
ctx.status=200;
//下面{param}部分就存在危险
cte.body=`<div>${param}</div>`;
}
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
2. 存储型(stored)
基于存储型的攻击,存储在服务器端,带有恶意的文章或者帖子,用户每次访问时都会触发。
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在HTML中返回给浏览器
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
存储型XSS和反射型的XSS差别就在于,存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
3. 基于DOM的基本类型(DOM-based XSS)
基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。
具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,或者是一个钓鱼网站。它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这里提供一个可能会造成DOM-based XSS攻击的demo示例
const content=new URL(loaction.href).searchParams.get("param");
const div=document.createElement("div");
//恶意脚本植入
div.innerHTML=content;
document.body.append(div);
4. Mutation-based XSS
这个就很灵活了,它会利用浏览器的差异性,利用大家可能会忽略掉的title属性来攻击,比如下边的例子:
//充分利用title作为过滤工具
<noscript><p title="<noscript><img src=x onerror=alert(1)>">
<div>
<noscript><p title="<noscript>
<img src="X" onerror="alert(1)">
"">"
</div>
XSS的防御机制
通过了解后,我们知道存储型 XSS 攻击和反射型 XSS 攻击都是需要经过 Web 服务器来处理的,因此可以认为这两种类型的漏洞是服务端的安全漏洞。而基于 DOM 的 XSS 攻击全部都是在浏览器端完成的,因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。
但无论是何种类型的 XSS 攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。
所以,XSS 攻击有两大要素:
- 攻击者提交恶意代码。
- 浏览器执行恶意代码。
针对第一个要素:我们是否能够在用户输入的过程,过滤掉用户输入的恶意代码呢?
输入过滤
用户提交时前端进行过滤,然后直接提交到后端可行吗?
不行!!攻击者可以绕过前端过滤,直接构造请求,就可以提交恶意代码.
那么,换一个过滤时机:后端在写入数据库前,对输入进行过滤,然后把“安全的”内容,返回给前端。这样是否可行呢?
我们举一个例子,一个正常的用户输入了 5 < 7 这个内容,在写入数据库前,被转义,变成了 5 < 7。
问题是:在提交阶段,我们并不确定内容要输出到哪里。
这里的“并不确定内容要输出到哪里”有两层含义:
1.用户的输入内容可能同时提供给前端和客户端,而一旦经过了 escapeHTML(),客户端显示的内容就变成了乱码( 5 < 7 )。
2.在前端中,不同的位置所需的编码也不同。
- 当
5 < 7作为 HTML 拼接页面时,可以正常显示:<div title="comment">5 < 7</div> - 当
5 < 7通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等。
所以,输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。
当然,对于明确的输入类型,例如数字、URL、电话号码、邮件地址等等内容,进行输入过滤还是必要的。
既然输入过滤并非完全可靠,我们就要通过“防止浏览器执行恶意代码”来防范 XSS。这部分分为两类:
- 防止 HTML 中出现注入。
- 防止 JavaScript 执行时,执行恶意代码。
预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。
预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,把代码和数据分隔开。
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(
.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如
onload事件和href中的javascript:xxx等,请参考下文”预防 DOM 型 XSS 攻击“部分)。在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。
- 对 HTML 做充分转义。
如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
常用的模板引擎,如 doT.js、ejs、FreeMarker 等,对于 HTML 转义通常只有一个规则,就是把
& < > " ' /这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善
预防 DOM 型 XSS 攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML、.outerHTML、document.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent、.setAttribute() 等。
如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。
DOM 中的内联事件监听器,如 location、onclick、onerror、onload、onmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()、setTimeout()、setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
<!-- 内联事件监听器中包含恶意代码 -->

<!-- 链接内包含恶意代码 -->
<a href="UNTRUSTED">1</a>
<script>
// setTimeout()/setInterval() 中调用恶意代码
setTimeout("UNTRUSTED")
setInterval("UNTRUSTED")
// location 调用恶意代码
location.href = 'UNTRUSTED'
// eval() 中调用恶意代码
eval("UNTRUSTED")
</script>
如果项目中有用到这些的话,一定要避免在字符串中拼接不可信数据。
其他XSS防范措施
- Content Security Policy
CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码。
严格的 CSP 在 XSS 的防范中可以起到以下的作用:
- 禁止加载外域代码,防止复杂的攻击逻辑。
- 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
- 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
- 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
- 合理使用上报可以及时发现 XSS,利于尽快修复问题。
- 输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
- HTTP-only Cookie
由于很多 XSS 攻击都是来盗用 Cookie 的,服务器可以在HTTP响应头中,将某些 Cookie 设置为 HttpOnly 标志,,即set-cookie 属性值使用 HttpOnly 来标记该 Cookie。顾名思义,使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript 来读取这段 Cookie(document.cookie)。
- 验证码
防止脚本冒充用户提交危险操作。
- 利用一些现成工具
Cross-site request forgery (CSRF)
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中向攻击网站发送跨站请求。利用受害者在在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击网站进行某项操作的目的。
简单来说:攻击者在受害者不知情的情况下,冒充受害者,让被攻击的网站执行了自己定义的操作。
典型的CSRF攻击流程:
- 受害者登录a.com,并保留了登录cookie
- 攻击者诱导用户访问b.com
- b.com向a.com发送了一个请求:a.com/act=xx,浏览器会默认携带用户对的Cookie
- a.com接收到请求后误以为是用户自己发的请求
- a.com以用户的名义去执行act=xx
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
可以这么理解CSRF攻击:你打开一个网站没有注销,然后又打开了一个恶意攻击的网站盗用你的身份,以你的名义向你未注销的网站发送恶意请求。这样能做到利用你的身份发邮件,发短信,进行交易转账,甚至盗取账号信息。下面是一个关于访问银行页面的例子。
CSRF的类型
1.GET类型的CSRF
这类攻击非常简单,只需要一个HTTP请求:
<img style="display:none;" src="http://bank.com/transfer?to=hacker&amount=100">

2. 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>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。
POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
3.链接类型的 CSRF
链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!
<a/>
而由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问这个页面,则表示攻击成功。
CSRF的特点
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作而不是直接窃取信息
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是冒用
- 跨站请求可以是各种方式:图片URl、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
CSRF的防御机制
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。
针对CSRF的特点:
- CSRF通常发生在第三方域名
- CSRF攻击者不能获取到Cookie等信息,只是使用
针对这两点,我们可以专门制定防护策略,如下:
-
阻止不明外域的访问
- 同源检测
- Samesite Cookie
-
提交时要求附加本域才能获取的信息
- CSRF Token
- 双重Cookie验证
同源检测
CSRF通常发生在第三方域名,那么就直接禁止外域(或者不受信任的域名)对我们发起的请求
问题来了:如何判断请求是否来自于外域?
在HTTP协议中共,每一个异步请求都会携带两个Header,用于标记来源域名:
- Origin Header
- Referer Header
这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个Header中的域名,确定请求的来源域。
使用Origin Header确定来源域名
在部分CSRF有关的请求中,请求的Header中会携带Origin字段。字段中包含请求的域名(不包含path以及query)
如上,如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。
如果不存在呢?
在以下两种情况不存在:
- IE11同源策略:IE 11 不会在跨站CORS请求上添加Origin标头,Referer头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考MDN Same-origin_policy#IE_Exceptions
- 302重定向:在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。
使用Referer Header确定来源域名
根据HTTP协议,在HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址。 对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。
但是由于Referer是浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证Referer值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上讲,这样不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。
2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。新版的Referrer Policy规定了五种Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三种策略:never、default和always,在新标准里换了个名称。他们的对应关系如下:
把Referrer Policy的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。例如:aaa.com引用bbb.com的资源,不会发送Referer。
设置Referrer Policy的方法有三种:
- 在CSP设置
- 页面头部增加meta标签
- a标签增加refererpolicy属性
所以,攻击者可以在自己的请求中隐藏Referer。如果攻击者将自己的请求这样填写:
那么这个请求发起的攻击将不携带Referer。
以下几个情况referer可能没有或者不可信
- IE6、7下使用window.location.href=url进行界面的跳转,会丢失Referer。
- IE6、7下使用window.open,也会缺失Referer。
- HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失。
- 点击Flash上到达另外一个网站的时候,Referer的情况就比较杂乱,不太可信。
直接阻止外域请求
假如Origin和Referer头文件都没有怎么办,这种情况下建议阻止,特别是如果没有使用随机CSRF Token作为第二次检查。
另外,前面说过,CSRF大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。
综上所述:同源验证是一个相对简单的防范方法,能够防范绝大多数的CSRF攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。
CSRF Token
由于攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息,而且CSRF之所以能成功是因为服务器以为是用户自己的请求。
所以。我们可以要求用户请求都携带一个CSRF攻击者都无法获取到的Token。服务器通过校验请求是否携带了正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
🔮原理:
CSRF Token的防护策略分为三个步骤:
- 将CSRF Token输出到页面中
首先,用户打开页面的时候,服务器为这个用户生成一个Token(该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合),然后存在服务器的Session中。之后在每次页面加载时使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。
这里是因为如果还放在Cookie里面的话又会被攻击者冒用
- 页面提交的请求携带这个Token
对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
这样,就把Token以参数的形式加入请求了。
- 服务器验证Token是否正确
用户提交后服务器得判断Token是否有效,即解密后对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的
这种方法要比之前检查Referer或者Origin要安全一些,Token可以在产生并放于Session之中,然后在每次请求时把Token从Session中拿出,与请求中的Token进行比对,但这种方法的比较麻烦的在于如何把Token以参数的形式加入请求。
下面将以Java为例,介绍一些CSRF Token的服务端校验逻辑,代码如下:
注意:Token必须是随机生成的,不能让攻击者找到规律。通常一次会话只会生成一个Token,在初始化Token后将该值存在会话中,并用于后续请求直到会话过期。当用户发出请求时,服务器端必须验证请求中Token的存在性和有效性,与会话中找到的Token相比较。如果在请求中找不到Token,或者提供的值与会话中的值不匹配,则应中止请求,应重置Token并将事件记录为正在进行的潜在CSRF攻击。
分布式校验
在大型网站中,利用Session存储CSRF会带来很大的压力。
访问单台服务器session是同一个,但是现在我们的服务器一般不止一台,而且很多机房也可能在不同的省份,用户发起的HTTP请求通常要经过像Ngnix之类的负载均衡器之后,再路由到具体的服务器上,由于Session默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿到之前的HTTP请求存储在服务器中的Session数据,从而使得Session机制在分布式环境下失效,因此在分布式集群中CSRF Token需要存储在Redis之类的公共存储空间。
由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题。
解决方法:
目前很多网站采用Encrypted Token Pattern方式。这种方法的Token是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的Token,只用再次计算一次即可。
通常是使用UserID、时间戳和随机数,通过加密的方法生成。这样既可以保证分布式服务的Token一致,又能保证Token不容易被破解。
在token解密成功之后,服务器可以访问解析值,Token中包含的UserID和时间戳将会被拿来被验证有效性,将UserID与当前登录的UserID进行比较,并将时间戳与当前时间进行比较。
总结
Token是一个比较有效的CSRF防护方法,只要页面没有XSS漏洞泄露Token,那么接口的CSRF攻击就无法成功。但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个Form及Ajax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理,而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,且有可能遗漏。
验证码和密码其实也可以起到CSRF Token的作用哦,而且更安全。
这就是为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码的原因
双重Cookie
在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。
那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。
双重Cookie采用以下流程:
- 用户访问页面时候向请求与注入一个Cookie(
csrfcookie=v8g9e4ksfhw)
- 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例
POST ``https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。 - 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。
当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因我们举例进行说明。
由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
- 如果用户访问的网站为
www.a.com,而后端的api域名为api.a.com。那么在www.a.com下,前端拿不到api.a.com的Cookie,也就无法完成双重Cookie认证。- 于是这个认证Cookie必须被种在
a.com下,这样每个子域都可以访问。- 任何一个子域都可以修改
a.com下的Cookie。- 某个子域名存在漏洞被XSS攻击(例如
upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的Cookie。- 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向
www.a.com下,发起CSRF攻击。
总结:
用双重Cookie防御CSRF的优点:
- 无需使用Session,适用面更广,易于实施。
- Token储存于客户端中,不会给服务器带来压力。
- 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。
缺点:
- Cookie中增加了额外的字段。
- 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
- 难以做到子域名的隔离。
- 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。
Samesite Cookie属性
为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax,下面分别讲解:
Samesite=Strict
这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:
Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
我们在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会。举个实际的例子就是,假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个 Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。
Samesite=Lax
这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求,则这个Cookie可以作为第三方Cookie。比如说 b.com设置了如下Cookie.
Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则bar也不会发送。
生成Token放到Cookie中并且设置Cookie的Samesite,Java代码如下:
private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
//生成token
String sToken = this.generateToken();
//手动添加Cookie实现支持“Samesite=strict”
//Cookie添加双重验证
String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI());
httpResponse.addHeader("Set-Cookie", CookieSpec);
httpResponse.setHeader(CSRF_TOKEN_NAME, token);
}
代码源自OWASP Cross-Site_Request_Forgery #Implementation example
如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。
但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。
如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。
另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版Chrome和Firefox支持以外,Safari以及iOS Safari都还不支持,现阶段看来暂时还不能普及。
而且,SamesiteCookie目前有一个致命的缺陷:不支持子域。例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。
总之,SamesiteCookie是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望
防止网站被利用
前面所说的,都是被攻击的网站如何做好防护。而非防止攻击的发生,CSRF的攻击可以来自:
- 攻击者自己的网站。
- 有文件上传漏洞的网站。
- 第三方论坛等用户内容。
- 被攻击网站自己的评论功能等。
对于来自黑客自己的网站,我们无法防护。但对其他情况,那么如何防止自己的网站被利用成为攻击的源头呢?
- 严格管理所有的上传接口,防止任何预期之外的上传内容(例如HTML)。
- 添加Header
X-Content-Type-Options: nosniff防止黑客上传HTML内容的资源(例如图片)被解析为网页。 - 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。
- 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)。
总结
简单总结一下上文的防护策略:
- CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
- CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
- 保证页面的幂等性,后端接口不要在GET页面中做用户操作。
为了更好的防御CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前Web应用程序自身的情况做合适的选择,才能更好的预防CSRF的发生。
SQL Injection
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。
而造成SQL注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。
简单理解SQL注入就是在人为可以构造参数的地方加入一些非法敏感语句,绕过后端的处理,并带入到数据库中执行,然后返回敏感数据的过程。
举个例子
读取请求字段后直接以字符串的形式拼接SQL语句到数据库用select语句
public async rendForm (ctx) {
const { username ,form_id }=ctx.query;
const result = await sql.query (`
SELECT a,b,c FROM table
WHERE username =${username}
AND form_id=${form_id}
`)
ctx.body=readersForm(result);
}
再举个例子
这次的例子可以达成你被动删库跑路的成就!
fetch("/api",{
method:"POST",
headers:{
"Content-Type":"application/json"
},
body:JSON.stringfy({
username:"select ** from ***;DROP TABLE table;",
})
})
SQL注入的特点
-
广泛性
任何一个基于SQL语言的数据库都可能被攻击,很多开发人员在编写Web应用程序时未对从输入参数、Web表单、cookie等接受到的值进行规范性验证和检测,通常会出现SQL注入漏洞。
-
隐蔽性
SQL注入语句一般都嵌入在普通的HTTP请求中,很难与正常语句区分开,所以当前许多防火墙都无法识别予以警告,而且SQL注入变种极多,攻击者可以调整攻击的参数,所以使用传统的方法防御SQL注入效果非常不理想。
-
危害大
攻击者通过SQL注入获取到服务器的库名、表名、字段名,从而获取到整个服务器中的数据,对网站用户的数据安全有极大的威胁。攻击者也可以通过获取到的数据,得到后台管理员的密码,然后对网页页面进行恶意篡改。这样不仅对数据库信息安全造成严重威胁,对整个数据库系统安全也影响重大。
-
操作方便
互联网上有很多SQL注入工具,简单易学,攻击过程简单,不需要专业知识也能自如运用。
那么我们应该怎么防范呢?
SQL注入的防御机制
1、分级管理
即对用户进行分级管理,严格控制用户的权限,对于普通用户,禁止给予数据库建立、删除、修改等相关权限,只有系统管理员才具有增、删、改、查的权限。例如上述实例中用户在查询语句中加入了drop table。肯定是不能让其执行的,否则系统的数据库安全性就无法保障。所以通过权限的设计限制,使得即使恶意攻击者在数据提交时嵌入了相关攻击代码代码也不能执行,从而减少SQL注入对数据库的安全威胁。
2、基础过滤与二次过滤
SQL注入攻击前,入侵者通过修改参数提交“and”等特殊字符,判断是否存在漏洞,然后通过select、update等各种字符编写SQL注入语句。因此防范SQL注入要对用户输入进行检查,确保数据输入的安全性,在具体检查输入或提交的变量时,对于单引号、双引号、冒号等字符进行转换或者过滤,从而有效防止SQL注入。当然危险字符有很多,在获取用户输入提交的参数时,首先要进行基础过滤,然后根据程序的功能及用户输入的可能性进行二次过滤,以确保系统的安全性。
3、数据库信息加密
传统的加解密的方法大致可以分为三种:
(1)对称加密:即加密方和解密方都使用相同的加密算法和密钥,这种方案的密钥的保存非常关键,因为算法是公开的,而密钥是保密的,一旦密匙泄露,黑客仍然可以轻易解密。常见的对称加密算法有:AES、DES等。
(2)非对称加密:即使用不同的密钥来进行加解密,密钥被分为公钥和私钥,用私钥加密的数据必须使用公钥来解密,同样用公钥加密的数据必须用对应的私钥来解密,常见的非对称加密算法有:RSA等。
(3)不可逆加密:利用哈希算法使数据加密之后无法解密回原数据,这样的哈希算法常用的有:md5、SHA-1等。
其他注入
除了上文提到的SQL注入,其实还有cli,OScommand,Server-Side Request Forgery(SSRF),其中SSRF是服务端伪造请求,且严格而言不是injection,但是原理类似。
1. cli
调用虚拟视频格式转换脚手架,脚手架接收一些参数,这些参数允许用户自定义options
攻击者可以传入这样的option
const command=`convert-cli video - o && rm -rf xxx`
2.OS command
如果你的服务端暴露了一些敏感的重要文件,将会导致被攻击者读取和修改。比如,通过nginx的配置,将访问流量转发到第三方网站,让他们的服务器不堪重负,直接下线。
此时我们可以将流量转发到真实的第三方,当第三方扛不住新增流量时就会导致服务挂掉。
3. Server-Side Request Forgery(SSRF)
服务端伪造请求(严格⽽⾔,SSRF 不是 injection,但是原理类似)
举个例子 第一步先请求用户自定义的callback URL,而webServer通常由内网访问权限,这样可能会将内网的信息暴露给外部,导致外部可能会对内网的一些配置进行攻击。
public async webhook (ctx) {
//callback 可能是内网url
//比如:hrrp://secret.com/get_employ_payrolls
ctx.body=await fetch (ctx.query.callback);
}
此时访问callback===暴露内网信息
Denial of Service attack (DOS)
即是一种让运行中的服务停止状态的攻击,即通过某种⽅式(构造特定请求),导致服务器资源被显著消耗,来不及响应更多请求,导致请求挤压,进而雪崩效应。有时也叫做服务停止攻击或拒绝服务攻击。DoS攻击的对象不仅限干Web网站,还包括网络设备及服务器等。
DOS的类型
1. ReDoS 基于正则表达式的Dos
我们先复习一下正则表达式中的贪婪模式,然后就举个例子。
重复匹配时「?」 vs 「no ?」:满⾜“⼀个” 即可 vs 尽量多
//有多少匹配多少
const greedyRegExp=/a+/;
//有一个就行
const nonGreedyRegExp=/a+?/;
const str="aaaaaa";
//"aaaaaa"
console.log(str.match(greedyRegExp)[0]);
//a
console.log(str.match(nonGreedyRegExp)[0]);
ReDoS 基于正则表达式的Dos就是一种贪婪模式,它会实行一种暴力回溯。
举个例子
右边每次匹配不到就会去掉最后两位继续验证,不断回溯,这样将会使得响应时间大大增加,接口吞吐量也会下降。
2. Distributed DoS DDoS 分布式DoS
即短时间内,来自大僵尸设备的请求流量,服务器不能及时完成全部请求,导致请求堆积,进而雪崩效应,无法响应新请求
特点
- 直接访问IP
- 任意API
- 消耗大量带宽(耗尽)
举个例子,Syn-Flood攻击,在具体介绍Syn-Flood攻击之前,我们先了解一下三次握手。所谓的三次握手,就是在建立TCP连接的时候,客户端和服务器需要做几次通讯,确认相互之间的信息。它是保证TCP协议可靠性的重要手段之一。下面我们就来看一下它的具体流程:
1、客户端(Client)首先向服务器(Server)发送连接请求,请求内容是:SYN=1 Seq=X;
2、服务器收到这个包以后,将这个数据放入到一个队列中,这个队列叫syn_table。并且发送一个返回包,作为响应,这个返回包有自己的序列号(Seq=Y),以及一个Ack,Ack的值就是客户端发来的Seq值加一;
3、客户端收到返回信息以后,将服务器的Seq加一作为Ack又发给服务器;
4、服务器收到这第三个包,验证没问题以后,就将这个连接放入到request_sock_queue
,三次握手完成。
所以,如果攻击者发很多次一个次的SYN不完成第三次的握手,导致大量连接未得到释放,当达到最大连接数的时候,新请求就无法进来了!!!
DOS的防御措施
1、 对于ReDoS 基于正则表达式的Dos来说:
2、对于Distributed DoS DDoS 分布式DoS来说:
最后,我们再介绍一种中间人攻击。
中间人攻击
即当双方在进行通信的时候,可能以为对方是方,但是如果有一个中间人进行的拦截,中间加了一层,双方可能无法意识到中间人的存在,他可能会进行一些攻击。
中间人攻击的前提:
- 明文传输
- 信息篡改不可知
- 对方身份未验证
中间人攻击的防御
1、使用HTTPS:确保你访问的每个网站都使用HTTPS。
2、不要忽略警告:如果你的浏览器告诉你正在访问的网站有问题,相信它。安全证书警告可能是将凭据授予攻击者和保持安全之间的区别。
3、不要使用公共Wi-Fi:如果你能帮上忙,就不要使用公共Wi-Fi。有时,使用公共Wi-Fi是不可避免的。如果您必须使用公共Wi-Fi连接,则应下载并安装VPN,为您的连接添加一些安全性。此外,在使用公共Wi-Fi连接时,请注意浏览器安全警告。如果浏览器警告的数量突然增加,则可能表明存在MITM攻击或漏洞。
4、运行并更新防病毒软件:确保您的防病毒软件是最新的。
至此,安全问题就告一段落了,暂时结束守城阶段。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!