前言
随着互联网的高速发展,信息安全问题已经成为行业最为关注的焦点之一,而 Web 开发又是引发安全问题的高危据点。在移动互联网时代,Web 开发人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇 Injection(代码注入)、DoS、中间人攻击等新型安全问题。当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要 Web 开发人员不断进行"查漏补缺"。本文将介绍一些常用 Web 开发遇到的安全问题及如何防范。
注:本文为前端开发者编辑,大多从前端开发者的角度出发,涉及后端开发安全的部分缺乏深入探讨
Cross-Site Scripting(XSS)
XSS 简介
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
XSS 的特点:
- 通常难以从 UI 上感知(暗地执行脚本)
- 窃取用户信息(cookie/token)
- 绘制 UI(如弹窗),诱骗用户点击/填写表单
XSS 的类型
类型 | 存储区* | 插入点* |
---|---|---|
Stored XSS(存储型 XSS)(涉及数据库) | 后端数据库 | HTML |
Reflected XSS(反射型 XSS)(基于URL) | URL | HTML |
DOM-based XSS(DOM 型 XSS)(基于浏览器) | 后端数据库 / 前端存储 / URL | 前端 JavaScript |
- 存储区:恶意代码存放的位置。
- 插入点:由谁取得恶意代码,并插入到网页上。
存储型XSS
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据 的网站功能,如论坛发帖 、商品评论 、用户私信等。
反射型XSS
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索 、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM型XSS
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据 并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS 的防御
通过前面的介绍可以得知,XSS 攻击有两大要素:
- 攻击者提交恶意代码。
- 浏览器执行恶意代码。
输入过滤
针对 XSS 攻击的第一个要素:我们是否能够在用户输入的过程,过滤掉用户输入的恶意代码呢?
输入过滤有两个时机,一个是在用户提交的时候,一个是在后端获取数据写入数据库前。
对于前者,一旦攻击者绕过前端过滤,直接构造请求,就可以提交恶意代码了。后者把过滤后的"安全的"内容存入数据库,但是在提交阶段,我们并不确定内容要输出到哪里,在前端中不同位置所需的编码不同,我们无法保证同一种编码方式在所有位置都能正常显示。
所以,输入侧过滤能够在某种情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。
当然,对于明确的输入类型,例如数字、URL、电话号码、邮箱地址等等内容,进行输入过滤还是必要的。
既然输入过滤并非完全可靠,我们就要通过"防止浏览器执行恶意代码"来防范 XSS。这部分分为两类:
- 防止 HTML 中出现注入。
- 防止 JavaScript 执行时,执行恶意代码。
预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的"数据"被内嵌到"代码"中,被浏览器所执行。
预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,把代码和数据分隔开。
- 对 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 防护作用,但并不完善。
XSS 安全漏洞 | 简单转义是否有防护作用 |
---|---|
HTML 标签文字内容 | 有 |
HTML 属性值 | 有 |
CSS 内联样式 | 无 |
内联 JavaScript | 无 |
内联 JSON | 无 |
跳转链接 | 无 |
所以要完善 XSS 防护措施,我们要使用更完善更细致的转义策略。HTML 的编码是十分复杂的,在不同的上下文里要使用不同的转义规则。
预防 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,很容易产生安全隐患,请务必避免。
html
<!-- 内联事件监听器中包含恶意代码 -->
<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 防范措施
虽然在渲染页面和执行 JavaScript 时,通过谨慎的转义可以防止 XSS 的发生,但完全依靠开发的谨慎仍然是不够的。以下介绍一些通用的方案,可以降低 XSS 带来的风险和后果。
Content Security Policy(CSP)
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。
严格的 CSP 在 XSS 的防范中可以起到以下的作用:
- 禁止加载外域代码,防止复杂的攻击逻辑。
- 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
- 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
- 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
- 合理使用上报可以及时发现 XSS,利于尽快修复问题。
通常可以通过两种方式来开启 CSP:
服务器的响应头部:
less
// 只允许加载本站资源
Content-Security-Policy: default-src 'self'
// 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
浏览器meta
html
<meta http-equiv="Content-Security-Policy" content="script-src self">
输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
其他安全措施
- HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
- 验证码:防止脚本冒充用户提交危险操作。
XSS 的检测
对于已经上线的代码,如何去检测其中有没有 XSS 漏洞?
- 使用通用 XSS 攻击字符串手动检测 XSS 漏洞
markdown
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
// 拼接在 URL 参数上
http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E
它能够检测到存在于 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等多种上下文中的 XSS 漏洞,也能检测 eval()
、setTimeout()
、setInterval()
、Function()
、innerHTML
、document.write()
等 DOM 型 XSS 漏洞,并且能绕过一些 XSS 过滤器。
- 使用扫描工具自动检测 XSS 漏洞,如Arachni、Mozilla HTTP Observatory、w3af
XSS 总结
整体的 XSS 防范是非常复杂和繁琐的,我们不仅需要在全部需要转义的位置,对数据进行对应的转义。而且要防止多余和错误的转义,避免正常的用户输入出现乱码。
虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
- 利用模板引擎 开启模板引擎自带的 HTML 转义功能。
- 避免内联事件 尽量不要使用拼接内联事件的写法。在 JavaScript 中通过
.addEventlistener()
事件绑定会更安全。 - 避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用
createElement
、setAttribute
之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。 - 时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
- 增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
- 主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。
Cross-site request forgery(CSRF)
CSRF 简介
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
一个典型的CSRF攻击有着如下的流程:
- 受害者登录 a.com ,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了 b.com
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带 a.com 的 Cookie
- a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com 以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
常见的 CSRF 攻击类型
- GET 类型的 CSRF
GET 类型的 CSRF 利用非常简单,只需要一个 HTTP 请求,一般会这样利用:
html
<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
POST 类型的 CSRF 通常使用的是一个自动提交的表单。
html
<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操作。
- 链接类型的 CSRF
链接类型的 CSRF 并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击。
html
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!
<a/>
CSRF 的特点
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是"冒用"。
- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
CSRF 的防御
上文中讲了CSRF的两个特点:
- CSRF(通常)发生在第三方域名。
- CSRF攻击者不能获取到Cookie等信息,只是使用。
针对这两点,我们可以专门制定防护策略,如下:
-
阻止不明外域的访问
- 同源检测
- Samesite Cookie
-
提交时要求附加本域才能获取的信息
- CSRF Token
- 双重 Cookie 验证
以下我们对各种防护方法做详细说明:
同源检测
既然CSRF大多来自第三方网站,那么我们就限制或直接禁止外域(或者不受信任的域名)对我们发起的请求。
在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
Origin
Referer
在浏览器发起请求时,大多数情况会自动带上这两个Header,并且不能由前端自定义内容。 服务器可以通过解析这两个 Header 中的域名,确定请求的来源域。
使用Origin
确定来源域名
请求标头 Origin
表示了请求的来源(协议、主机、端口)。如果Origin
存在,那么直接使用Origin
中的字段确认来源域名就可以。
但是Origin
在以下两种情况下并不存在:
-
IE11同源策略: IE 11 不会在跨站 CORS 请求上添加Origin 标头,Referer 头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同。
-
302重定向: 在302重定向之后 Origin 不包含在重定向的请求中,因为 Origin 可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的 URL,因此浏览器不想将 Origin 泄漏到新的服务器上。
使用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,在新标准里换了个名称。他们的对应关系如下:
策略名称 | 属性值(新) | 属性值(旧) |
---|---|---|
No Referrer | no-Referrer | never |
No Referrer When Downgrade | no-Referrer-when-downgrade | default |
Origin Only | (same or strict) origin | origin |
Origin When Cross Origin | (strict) origin-when-crossorigin | - |
Unsafe URL | unsafe-url | always |
设置 Referrer Policy 的方法有三种:
- 在 CSP 设置
- 页面头部增加 meta标签
- a标签、img标签增加 referrerpolicy 属性
所以如果攻击者不想让自己的请求不携带,可以将自己的请求这样填写:
html
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
如何阻止外域请求
通过 Header 的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域,或者子域名,或者有授权的第三方域名,又或者来自不可信的未知域名。
我们已经知道了请求域名是否是来自不可信的域名,我们直接阻止掉这些的请求,就能防御 CSRF 攻击了吗?
当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似 CSRF 攻击。所以在判断的时候需要过滤掉页面请求情况,通常 Header 符合以下情况:
vbnet
Accept: text/html
Method: GET
但相应的,页面请求就暴露在了 CSRF 的攻击范围之中。如果你的网站中,在页面的 GET 请求中对当前用户做了什么操作的话,防范就失效了。
另外,前面说过,CSRF 大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。
同源验证是一个相对简单的防范方法,能够防范绝大多数的 CSRF 攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。
SameSite Cookie
为了从源头上解决这个问题,Google 起草了一份草案来改进 HTTP 协议,那就是为 Set-Cookie 响应头新增 Samesite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击。
Samesite 有两个属性值,分别是 Strict 和 Lax。
- Samesite=Strict。这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。
- Samesite=Lax。这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个 GET 请求,则这个 Cookie 可以作为第三方 Cookie。
如何使用 SameSite Cookie
如果 SamesiteCookie 被设置为 Strict,浏览器在任何跨域请求中都不会携带 Cookie,新标签重新打开也不携带,所以说 CSRF 攻击基本没有机会。
但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的 Cookie 都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。
如果 SamesiteCookie 被设置为 Lax,那么其他网站通过页面跳转过来的时候可以使用 Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。
但是该属性目前并不是所有浏览器都兼容,现阶段除了新版 Chrome 和 Firefox 支持以外,Safari 以及 iOS Safari 都还不支持。
而且,SamesiteCookie 目前有一个致命的缺陷:不支持子域。这就导致了当我们网站有多个子域名时,不能使用 SamesiteCookie 在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。
总之,SamesiteCookie 是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望。
CSRF Token
CSRF Token 防范攻击的基础是:攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。 因此可以让所有的用户请求都携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token,来把正常的请求和攻击的请求区分开,也可以防范 CSRF 的攻击。 服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。
CSRF Token 防护步骤
- 将Token 输出到页面
首先,用户打开页面的时候,服务器需要给这个用户生成一个 Token,该 Token 通过加密算法对数据进行加密,一般 Token 都包括随机字符串和时间戳的组合。 显然在提交时 Token 不能再放在 Cookie 中,否则又会被攻击者冒用。 因此,为了安全起见 Token 最好还是存在服务器的 Session 中,之后在每次页面加载时,使用 JS 遍历整个 DOM 树,对于 DOM 中所有的 a 和 form 标签后加入 Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 HTML 代码,这种方法就没有作用,还需要程序员在编码时手动添加 Token。
- 页面提交的请求携带这个 Token
对于 GET 请求,Token 将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue 。而对于 POST 请求来说,要在 form 的最后加上:
html
<input type="hidden" name="csrftoken" value="tokenvalue"/>
这样,就把 Token 以参数的形式加入请求了。
全局添加 Token 可以通过 request filter 实现,比如 axios 提供了 axios.interceptors.request.use 实现对所有请求的过滤。
- 服务器验证 Token 是否正确
当用户从客户端得到了 Token,再次提交给服务器的时候,服务器需要判断 Token 的有效性,验证过程是先解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个 Token 就是有效的。
分布式校验 Token
在大型网站中,使用 Session 存储 CSRF Token 会带来很大的压力。访问单台服务器 Session 是同一个。但是现在的大型网站中,我们的服务器通常不止一台,可能是几十台甚至几百台之多,用户发起的 HTTP 请求通常要经过像 Ngnix 之类的负载均衡器之后,再路由到具体的服务器上,由于 Session 默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次 HTTP 请求可能会先后落到不同的服务器上,导致后面发起的 HTTP 请求无法拿到之前的 HTTP 请求存储在服务器中的 Session 数据,从而使得 Session 机制在分布式环境下失效,因此在分布式集群中 CSRF Token 需要存储在 Redis 之类的公共存储空间。
由于使用 Session 存储,读取和验证 CSRF Token 会引起比较大的复杂度和性能问题,目前很多网站采用 Encrypted Token Pattern 方式。这种方法的 Token 是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的 Token,只用再次计算一次即可。
Token 总结
Token 是一个比较有效的 CSRF 防护方法,只要页面没有 XSS 漏洞泄露 Token,那么接口的 CSRF 攻击就无法成功。但是此方法的实现比较复杂,需要给每一个页面都写入 Token(前端无法使用纯静态页面),每一个 Form 及 Ajax 请求都携带这个 Token,后端对每一个接口都进行校验,并保证页面 Token 及请求 Token 一致。
双重 Cookie 验证
在会话中存储 CSRF Token 比较繁琐,而且不能在通用的拦截上统一处理所有的接口。如果这样做,会导致所有请求共享同一个 Token,从而失去了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 的使用这种方式也会有风险。
CSRF 总结
简单总结一下上文的防护策略:
- CSRF 自动防御策略:同源检测(Origin 和 Referer 验证)。
- CSRF 主动防御措施:Token验证 或者 双重 Cookie 验证 以及配合 Samesite Cookie。
- 保证页面的幂等性,后端接口不要在 GET 页面中做用户操作。
为了更好的防御 CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前 Web 应用程序自身的情况做合适的选择,才能更好的预防 CSRF 的发生。
Injection
代码注入(Injection)攻击指的是攻击者在应用程序中注入源代码,以改变程序的运行,这类攻击通常是由于缺乏对用户输入/输出数据验证和过滤导致的。前面讲的XSS也算是代码注入的一种。
SQL Injection
SQL 注入攻击(SQL injection),是发生于数据库的安全漏洞,是 Web 开发中最常见的一种安全漏洞。它利用 Web 应用程序对用户输入数据的处理不当,通过在用户输入中注入恶意的 SQL 代码,从而达到欺骗数据库服务器执行非授权操作的目的。
常见的 SQL 注入
- 真值条件攻击(True Value)
通过构造永远为真的SQL查询条件的语句片段,来重构SQL的条件语句从而绕过信息查询的条件过滤机制。
sql
-- 原语句(参数为 :1):
SELECT * FROM Users WHERE UserId = 1 ;
-- 注入后的语句(注入参数为: " 1 or 1=1"):
SELECT * FROM Users WHERE UserId = 1 OR 1=1;
这样的查询就会得到这个表中所有的记录。扩大了数据和信息的范围,让攻击者可以得到或者收集本来无权访问的数据。
- SQL 语句栈攻击 (SQL Stack)
如果应用服务器允许一次执行多个SQL语句,就有可能通过构造多个 SQL 语句来执行攻击。
sql
-- 原语句(参数为 :10 ):
SELECT * FROM products WHERE id = 10;
-- 注入后的语句(注入参数为 " 10; Drop members"):
SELECT * FROM products WHERE id = 10; DROP members;
SQL 语句栈攻击的核心在于通过注入";"号(通常在 SQL 语法中用于分隔多个语句,依次执行)。这样就可以在一个正常的语句中,扩展加入了攻击者想要的指令,从而达到攻击的目的。
- 注释信息攻击(Comment)
攻击者通过构造注释信息,可以让一些过滤或者检查机制失效。
sql
-- 原语句:
SELECT * FROM health_records WHERE date = '22/04/1999' AND id = 33
-- 注入后(改写参数):
SELECT * FROM health_records WHERE date = '22/04/1999'; -- ' AND id = 33
这种攻击的方式是通过在参数中加入标准注释符号"--",让此参数后的语句无效,就可能改变查询的条件,达到扩大数据范围的目的。
SQL 注入防护
输入检查
首先应当对涉及到数据库操作的输入进行检查,提前发现和过滤不合理的输入信息。对前后端分离的Web应用系统来说,如果注重用户体验和系统性能的话,这个检查可以首先在前端进行。但最终,还是需要在接口上来执行(防止直接构造Web请求进行攻击的情况)。
使用参数化 SQL 语句
SQL 本质上就是一个字符串,但业务的需求又让开发者不能事先明确的定义字符串的格式和内容。要使程序处理能够参数化来使用各种业务场景,我们会很自然的想到通过拼接的方式,来达到使 SQL 语句动态化的效果。根据上面讨论的 SQL 注入原理,这种方式显然具有很大的安全隐患。
针对这种情况,主流的 SQL 数据库系统,都提供了参数化查询和操作的机制。就是允许开发者将主 SQL 语句和执行参数分别提交到数据库系统进行执行,这样就可以单独的对参数进行检查和处理,而非需要考虑和分析整个 SQL 语句。这样可以明确的排除不应当出现参数内容中的文本,比如注释信息和条件判断等。这样就基本杜绝了几种常见的注入攻击作用的机制和方式。
在实际的处理中,数据库系统还可以以此对执行过程进行优化,比如 MySQL 的预编译语句,预先编译好需要执行的语句,然后基于参数来执行,从而减少解析和准备的环节,提高处理效率和性能。
限制查询或操作结果
开发人员在开发时应当充分考虑业务特点,设计和优化数据操作。作为一个最佳实践,在任何查询操作中,都考虑通过 limit 子句限制查询结果,而非全面依赖查询条件。这样做不但提高了安全性,还能够避免不可预料的查询结果,提高查询性能,可限定的查询结果,也可以优化数据库资源占用。
优化数据库设置和管理
其实在数据库系统层面上,也提供了很多的机制来抵御和防护注入攻击。例如:
- 使用视图来限制数据范围
- 合理的为应用系统分配数据库账号
- 只读、查询类或者数据交换类的应用
- 合理部署和设置数据库访问
当然,这些操作会额外的引入一些管理和运维的复杂性。需要充分评估安全需求和可操作性后进行实施。
其他代码注入
- CLI
- OS command (针对操作系统)
- Server-Side Request Forgery(SSRF),服务端伪造请求, 不是 Injection 但原理类似
防御思路
- 最小权限原则
- 建立允许名单 + 过滤
- 对 URL 类型参数进行协议、域名、ip 等限制
Denial of Service(DoS)
DoS 简介
DoS(Denial of Service)攻击,拒绝服务攻击,是一种网络攻击方式,旨在使目标系统或网络资源无法提供正常的服务,从而导致服务中断或不可用。
DoS 攻击的原理是通过向目标系统发送大量的请求 或占用系统资源 的行为,使系统超出其处理能力范围,导致系统性能下降或崩溃,无法正常为合法用户提供服务。DoS 攻击通常是有意的 、有目的性的攻击,旨在破坏或干扰目标系统的正常运行。
DoS 类型
- Distributed DoS(DDoS)(分布式拒绝服务攻击)
- ReDoS (基于正则表达式的DoS)
DDoS
Distributed Dos(DDoS),分布式拒绝服务,利用大量合法的分布式服务器在短时间内对目标发送大量请求,服务器不能及时完成全部请求,导致请求堆积,进而雪崩效应,无法响应新请求,从而导致正常合法用户无法获得服务。
DDoS 攻击的策略可分为三类:
- 带宽过载
带宽过载的目的是令计算机无法访问。DoS 和 DDoS 攻击直接针对系统网络及其各自的连接设备。路由器一次只能处理一定数量的数据,如果超出此容量,其他用户将无法再使用服务。带宽过载中典型的 DDoS 攻击是 Smurf 攻击。
Smurf 攻击: 是一种病毒攻击,以最初发动这种攻击的程序 "Smurf" 来命名,smurf 攻击也属于 DoS 攻击,是一种泛洪攻击,使用受害者的 IP 地址作为源 IP 地址伪造广播 ping,结合使用了 IP 欺骗和 ICMP 回复方法使大量网络传输充斥目标系统,引起目标系统拒绝为正常系统进行服务。
- 系统资源过载
针对 Web 服务器的 DDoS 攻击 是最为常见的。攻击者利用了 Web 服务器只能建立有限的数量连接。如果这些连接被无效请求占用,那么就能有效地阻止正常用户请求。这就是 Flood 泛洪。针对系统资源的经典 DDoS 攻击模式有 Ping Flood、SYN Flood 和 UDP Flood。
HTTP Flood: 这是最简单的 DDoS 资源过载攻击的变体。攻击者通过大量 HTTP 请求淹没目标的 Web 服务器。他们只需访问网页中任何一个元素,就能让服务器因请求量过载而崩溃。
Ping Flood: 又称 ICMP 泛洪,此类攻击下,攻击者会使用 ICMP 应答请求数据包令服务器过载。这些请求通常由僵尸网络大规模发送。由于这些请求必须用来自目标系统的数据包来回答,数量过多就会消耗主机资源,主机资源耗尽后就会瘫痪或者无法提供其他服务。
SYN Flood: 这种攻击属于滥用了 TCP 三次握手连接。TCP(传输控制协议)是一种网络协议,它与 IP 一起确保互联网上的数据流量通畅。TCP 连接在三步验证中建立,该过程从客户端向服务器发送同步数据包(SYN)开始;然后服务器接收到它,服务器用自己的同步数据包(SYN)和确认(ACK)确认请求;最后连接过程以客户端确认(ACK)结束。SYN flood 会在服务器上创建大量的半开连接,比如一直停留在最后一步,服务器会将这些没有最终确认的连接存储在内存中,直到服务器资源被完全耗尽。
UDP Flood: 这种类型的攻击主要依赖于无连接的用户数据报协议(UDP)。与 TCP 协议传输不同,数据可以通过 UDP 传输而无需事先建立连接。对于 DDoS 攻击,UDP 数据包被发送到目标系统上的随机端口。系统会尝试确定哪些应用程序正在等待传输数据,确认未成功的情况下,会将 ICMP 数据包连同消息 "Destination Unreachable(目的地无法到达)" 一起发送回发送方。当请求量过大时,系统资源过载,就会影响普通用户的请求。
- 利用软件错误和安全漏洞
如果黑客在操作系统或程序中发现某些安全漏洞,他们也可以策划 DoS 或 DDoS 攻击来引发系统崩溃。此类攻击的类型包括死亡之 Ping 和 LAND(局域网拒绝服务)攻击等。
DDoS 防御
了解了各种类型的 DDoS 攻击,然而我们并没有什么非常完美的办法阻止黑客发起攻击,只能提前加以防范。主要的防范思路有两点,一是过滤 ,过滤不必要的服务和端口,过滤掉不规则的数据包,也可以定义指定时间段内的访问数量限制,或对请求进行限速;二是抗量,提高网络带宽和服务器的处理能力,以抵御大流量的攻击。
- 使用防火墙和入侵检测系统(IDS)(过滤)
- 流量治理
- 负载均衡(过滤)
- API 网关(过滤)
- CDN 加速(抗量)
- 快速自动扩容(抗量)
- 非核心服务降级(抗量)
Regex DoS
当你需要搜索和替换文本时,正则表达式会派上用场。但是,在某些情况下,它们可能会导致系统变慢甚至容易受到 ReDoS 攻击。ReDoS 攻击的目的是通过低效的正则表达式停止应用程序或使其变慢。
ReDoS攻击可以分为两种类型:
- 带有恶意模式的字符串被传递给应用程序。然后将此字符串用作正则表达式,从而导致 ReDoS。
- 将特定格式的字符串传递给应用程序。然后这个字符串被易受攻击的正则表达式评估,也致 ReDoS。
任何 ReDoS 攻击的要点都是在应用程序中使用易受攻击的正则表达式。将特定格式的字符串传递给正则表达式会导致其计算时间过长。
如果 ReDoS 攻击成功,则正则表达式计算会导致灾难性的回溯。这是 Regex 引擎中回溯功能的结果,它会遍历可能的字符串匹配,直到找到正确的字符串。如果没有正确的匹配项,正则表达式将不会停止,直到遍历所有可能的选项。所有可能选项的完整迭代会导致正则表达式的计算时间长得令人无法接受。称为灾难性回溯。
ReDoS 防御
- 降低正则表达式的复杂度,尽量少用分组
- 代码扫描 + 正则性能测试
- 拒绝使用用户提供的正则
- 增加性能监控
中间人攻击(MITM)
中间人攻击(Man-in-the-Middle Attack,简称 MITM),是一种会话劫持攻击。攻击者作为中间人,劫持通信双方会话并操纵通信过程,而通信双方并不知情,从而达到窃取信息或冒充访问的目的。中间人攻击是一个统称,具体的攻击方式有很多种,例如 Wi-Fi 仿冒、邮件劫持、DNS 欺骗、SSL 劫持等。中间人攻击常用于窃取用户登录凭据、电子邮件和银行账户等个人信息,是对网银、网游、网上交易等在线系统极具破坏性的一种攻击方式。
中间人攻击主要有两个步骤:
-
攻击者想办法将自己插入到通信双方的链路中,拦截通信流量,为窃取数据或冒充访问做准备。攻击者会使用Wi-Fi仿冒、投放恶意软件、DNS 欺骗、ARP 欺骗等常用中间人攻击技术。
-
攻击者插入通信链路之后就可以操纵通信双方的通信,开始窃取数据、冒充访问等操作。这里可能涉及伪造网站、解密流量等技术。
常见中间人攻击类型
Wi-Fi 仿冒
这种攻击方式前文已经提到过,是最简单、常用的一种中间人攻击方式。攻击者创建恶意 Wi-Fi 接入点,接入点名称一般与当前环境相关,例如某某咖啡馆,具有极大迷惑性,而且没有加密保护。当用户不小心接入恶意 Wi-Fi 接入点后,用户后续所有的通信流量都将被攻击者截获,进而个人信息被窃取。
ARP 欺骗
ARP 欺骗也称为 ARP 投毒,即攻击者污染用户的 ARP 缓存,达到使用户流量发往攻击者主机的目的。局域网用户发起访问都需要由网关进行转发,用户首先发起 ARP 请求获取网关IP地址对应的 MAC 地址,此时攻击者冒充网关向用户应答自己的 MAC 地址,用户将错误的MAC地址加入自己的 ARP 缓存,那么后续用户所有流量都将发往攻击者主机。
DNS 欺骗
DNS 欺骗也称为 DNS 劫持。目标将其 DNS 请求发送到攻击者这里,然后攻击者伪造 DNS 响应,将正确的 IP 地址替换为其他 IP,之后你就登陆了这个攻击者指定的 IP,而攻击者早就在这个 IP 中安排好了一个伪造的虚假网站如某银行网站,从而骗取用户输入他们想得到的信息,如银行账号及密码等。
邮件劫持
攻击者劫持银行或其他金融机构的邮箱服务器,邮箱服务器中有大量用户邮箱账户。然后攻击者就可以监控用户的邮件往来,甚至可以冒充银行向个人用户发送邮件,获取用户信息并引诱用户进行汇款等操作。
SSL 劫持
当今绝大部分网站采用 HTTPS 方式进行访问,也就是用户与网站服务器间建立 SSL 连接,基于 SSL 证书进行数据验证和加密。HTTPS 可以在一定程度上减少中间人攻击,但是攻击者还是会使用各种技术尝试破坏 HTTPS,SSL 劫持就是其中的一种。
SSL 劫持攻击即 SSL 证书欺骗攻击,攻击者为了获得 HTTPS 传输的明文数据,需要先将自己接入到客户端和目标网站之间;在传输过程中伪造服务器的证书,将服务器的公钥替换成自己的公钥,这样,中间人就可以得到明文传输带 Key1、Key2 和 Pre-Master-Key,从而窃取客户端和服务端的通信数据。
但是对于客户端来说,如果中间人伪造了证书,在校验证书过程中会提示证书错误,由用户选择继续操作还是返回,由于大多数用户的安全意识不强,会选择继续操作,此时,中间人就可以获取浏览器和服务器之间的通信数据。
SSL 剥离
这种攻击方式也需要将攻击者设置为中间人,之后见 HTTPS 范文替换为 HTTP 返回给浏览器,而中间人和服务器之间仍然保持 HTTPS 服务器。由于 HTTP 是明文传输的,所以中间人可以获取客户端和服务器传输数据。
如何防止中间人攻击?
中间人攻击虽然多种多样、难以识别,但是我们还是可以采取一些措施,降低风险。以下列举一些常见的防止中间人攻击的措施:
- 不随意连接公共 Wi-Fi,仅连接已知可信的 Wi-Fi,避免流量被恶意劫持。自有 Wi-Fi 不要使用路由器默认密码,设置高强度加密保护,避免被破解。
- 确保访问 HTTPS 网站,可以安装开源 HTTPS Everywhere 浏览器插件,使浏览器自动连接 HTTPS 网站。
- 不随意忽略证书不安全告警,如果产生告警说明浏览的网站可能不安全。
- 远程访问使用 VPN,保护通信流量。
- 不要打开钓鱼邮件,钓鱼邮件一般会伪装成合法来源,并要求用户点击链接。注意识别钓鱼邮件,尤其不能点击邮件中的链接,否则可能会下载恶意软件或被重定向到恶意网站。
- 安装并及时更新杀毒软件。
- 企事业单位部署防火墙、终端安全软件,阻断恶意攻击,并对员工进行安全意识培训。业务系统尽量采用多因子认证,提升黑客破解难度。
总结
Web 开发安全是一个非常重要的话题,在实际应用中需要根据具体情况进行针对性的防护措施。在本文中我们介绍了常见的 Web 开发安全问题及其解决方法,包括 XSS 攻击、CSRF 攻击、DoS 攻击、Injection 攻击和中间人攻击等。通过对这些攻击方式的了解,我们可以更好地保障Web应用程序的安全性,避免敏感信息泄露和其他恶意行为的发生。保障 Web 应用程序的安全需要多方面的措施,只有不断提高安全意识,采取有效的防范措施,才能更好地保护用户数据和隐私。