# 前端安全防护
# 1 代码注入XSS
跨网站指令码(英语:
Cross-site scripting
,通常简称为:XSS
)是一种网站应用程式的安全漏洞攻击,是代码注入的一种。它允许恶意使用者将程式码注入到网页上,其他使用者在观看网页时就会受到影响。这类攻击通常包含了HTML
以及使用者端脚本语言
XSS
分为三种:反射型,存储型和DOM-based
# 1.1 如何攻击
XSS
通过修改HTML
节点或者执行JS
代码来攻击网站。- 例如通过
URL
获取某些参数
<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>
上述
URL
输入可能会将HTML
改为<div><script>alert(1)</script></div>
,这样页面中就凭空多了一段可执行脚本。这种攻击类型是反射型攻击,也可以说是DOM-based
攻击
# 1.2 如何防御
最普遍的做法是转义输入输出的内容,对于引号,尖括号,斜杠进行转义
function escape(str) {
str = str.replace(/&/g, "&");
str = str.replace(/</g, "<");
str = str.replace(/>/g, ">");
str = str.replace(/"/g, "&quto;");
str = str.replace(/'/g, "&##39;");
str = str.replace(/`/g, "&##96;");
str = str.replace(/\//g, "&##x2F;");
return str
}
通过转义可以将攻击代码
<script>alert(1)</script>
变成
// -> <script>alert(1)<&##x2F;script>
escape('<script>alert(1)</script>')
对于显示富文本来说,不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。这种情况通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式
var xss = require("xss");
var html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>');
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html);
以上示例使用了
js-xss
来实现。可以看到在输出中保留了h1
标签且过滤了script
标签
# 1.3 cookie 如何防范 XSS 攻击
XSS(跨站脚本攻击)是指攻击者在返回的 HTML 中嵌入 javascript 脚本,为了减轻这些 攻击,需要在 HTTP 头部配上,set-cookie
httpOnly
这个属性可以防止 XSS,它会禁止 javascript 脚本来访问cookie
secure
- 这个属性告诉浏览器仅在请求为https
的时候发送cookie
# 2 跨站请求伪造CSRF
CSRF
就是利用用户的登录态发起恶意请求CSRF(Cross-site request forgery)
跨站请求伪造,是一种常见的攻击方式。是指A
网站正常登陆后,cookie
正常保存登录信息,其他网站 B 通过某种方式调用 A 网站接口进行操作,A
的接口会在请求时会自动带上cookie
。
- 同源策略可以通过
html
标签加载资源,而且同源策略不阻止接口请求而是拦截请求结果,CSRF
恰恰占了这两个便宜。 - 对于
GET
请求,直接放到<img>
就能神不知鬼不觉地请求跨域接口。 - 对于
POST
请求,很多例子都使用form
提交:
<form action="<nowiki>http://bank.com/transfer.do</nowiki>" method="POST">
<input type="hidden" />
<input type="hidden" />
<input type="submit" value="View my pictures" />
</form>
浏览器同源策略不能作为防范 CSRF 的方法 浏览器允许这么做,归根到底就是因为你无法用 js 直接操作获得的结果。
如何攻击
假设网站中有一个通过 Get 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口
<img src="http://www.domain.com/xxx?comment='attack'"/>
res.setHeader('Set-Cookie', `username=poetry2;sameSite = strict;path=/;httpOnly;expires=${getCookirExpires()}`)
在B网站,危险网站向A网站发起请求
<!DOCTYPE html>
<html>
<body>
<!-- 利用img自动发送请求 -->
<img src="http://localhost:8000/api/user/login" />
</body>
</html>
会带上A网站的cookie
// 在A网站下发cookie的时候,加上sameSite=strict,这样B网站在发送A网站请求,不会自动带上A网站的cookie,保证了安全
// NAME=VALUE 赋予Cookie的名称及对应值
// expires=DATE Cookie 的有效期
// path=PATH 赋予Cookie的名称及对应值
// domain=域名 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie 的服务器的域名) (一般不指定)
// Secure 仅在 HTTPS 安全通信时才会发送 Cookie
// HttpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问
// SameSite Lax|Strict|None 它允许您声明该Cookie是否仅限于第一方或者同一站点上下文
res.setHeader('Set-Cookie', `username=poetry;sameSite=strict;path=/;httpOnly;expires=${getCookirExpires()}`)
如何防御
Get
请求不对数据进行修改- 不让第三方网站访问到用户
Cookie
- 阻止第三方网站请求接口
- 请求时附带验证信息,比如验证码或者
token
SameSite Cookies
: 只能当前域名的网站发出的http请求,携带这个Cookie
。当然,由于这是新的cookie属性,在兼容性上肯定会有问题
CSRF攻击,仅仅是利用了http携带cookie的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的cookie。这个和XSS不同,XSS是直接通过拿到Cookie等信息进行攻击的
在CSRF攻击中,就Cookie相关的特性:
- http请求,会自动携带Cookie。
- 携带的cookie,还是http请求所在域名的cookie。
# CSRF怎么获取用户的登录态
攻击全称不需要获取cookie,只是在危险的网站欺骗用户去点击已登录的网站链接,利用已登录的网站的自动发送cookie达到目的。因为http请求都会带着请求目标域下的
cookie
的,向同一个服务器发请求时会带上浏览器保存的对于那个服务器的cookie,而不管你从哪个网站向目标网站发请求
# cookie通常是不能跨域访问的,那问什么会有csrf攻击
疑问:
csrf说用户访问了A网站,然后又访问恶意网站B, B中也发送请求到A,携带A站的cookie,这样就构成了csrf。 可是cookie好像是不支持跨域的吧?
回答
- 浏览器会依据加载的域名附带上对应域名
cookie
,又不是发送b站的cookie
。 - 就是如果用户在
a
站登录了生成了授权的cookie
之类的,然后访问b
站,b站故意构造请求a站的请求,如删除操作之类的,用script
,img
或者iframe
之类的加载a
站着个地址,浏览器会附带上a
站此登录用户的授权cookie
信息,这样就构成crsf
,会删除掉当前用户的数据
# 总结
XSS
攻击: 注入恶意代码cookie
设置httpOnly
- 转义页面上的输入内容和输出内容
CSRF
: 跨站请求伪造,防护:get
不修改数据- 不被第三方网站访问到用户的
cookie
- 设置白名单,不被第三方网站请求
- 请求校验
# 3 浏览器同源策略 SOP
# 3.1 同源
先解释何为同源:协议、域名、端口都一样,就是同源。
url | 同源 |
---|---|
https://niconico.com(opens new window) | 基准 |
https://niconico.com/spirit | o |
https://sub.niconico.com/spirit | x |
http://niconico.com/spirit | x |
https://niconico.com:8080/spirit | x |
# 3.2 限制
- 你之所以会遇到 跨域问题,正是因为 SOP 的各种限制。但是具体来说限制了什么呢?
- 如果你说 SOP 就是“限制非同源资源的获取”,这不对,最简单的例子是引用图片、css、js 文件等资源的时候就允许跨域。
- 如果你说 SOP 就是“禁止跨域请求”,这也不对,本质上 SOP 并不是禁止跨域请求,而是在请求后拦截了请求的回应。
其实表面上 SOP 分两种情况:
- 可以正常引用 iframe、图片等各种资源,但是限制对其内容进行操作
- 直接限制 ajax 请求,准确来说是限制操作 ajax 响应结果,这会引起后面说到的 CSRF
但是,本质上这两条是一样的:总之,对于非同源的资源,浏览器可以“直接使用”,但是程序员和用户不可以对这些数据进行操作,杜绝某些居心不良的行为。这就是现代安全浏览器对用户的保护之一。
下面是 3 个在实际应用中会遇到的例子:
- 使用 ajax 请求其他跨域 API,最常见的情况,前端新手噩梦
- iframe 与父页面交流(如 DOM 或变量的获取),出现率比较低,而且解决方法也好懂
- 对跨域图片(例如来源于
<img>
)进行操作,在 canvas 操作图片的时候会遇到这个问题
如果没有了 SOP:
iframe
里的机密信息被肆意读取- 更加肆意地进行
CSRF
- 接口被第三方滥用
# 3.3 绕过跨域
SOP
虽然让用户更安全,同时也会对程序员带来一定程度的麻烦,因为有时候业务上就是有跨域的需求。绕过跨域的方案由于篇幅所限,并且网上也很多相关文章,所以不在这里展开解决跨域的方案,只给出几个关键词:
对于 ajax
- 使用
JSONP
- 后端进行
CORS
配置 - 后端反向代理
- 使用
WebSocket
对于 iframe
- 使用
location.hash
或window.name
进行信息交流 - 使用
postMessage
# 3.4 浏览器同源策略与ajax
对于 ajax 请求,在获得数据之后你能肆意进行 js 操作。这时候虽然同源策略会阻止响应,但依然会发出请求。因为执行响应拦截的是浏览器而不是后端程序。事实上你的请求已经发到服务器并返回了结果,但是迫于安全策略,浏览器不允许你继续进行 js 操作,所以报出你熟悉的
blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
。
所以再强调一次,同源策略不能作为防范 CSRF 的方法。
不过可以防范 CSRF 的例外还是有的,浏览器并不是让所有请求都发送成功,上述情况仅限于简单请求,相关知识会在下面 CORS 一节详细解释。
# 4 跨域资源共享 CORS
跨域是浏览器限制,跨域资源共享(Cross-origin resource sharing)也是服务器与浏览器协调的结果。
如果服务器设置了 CORS 相关配置,在返回浏览器的请求头会加上
Access-Control-Allow-Origin
,浏览器看到这个字段的值与当前的源匹配,就会解锁跨域限制。
HTTP/1.1 200 OK
Date: Sun, 24 Apr 2016 12:43:39 GMT
Server: Apache
Access-Control-Allow-Origin: http://www.acceptmeplease.com
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/xml
Content-Length: 423
对于 CORS,请求分两种。
# 4.1 简单请求
- 请求方法使用
GET
、POST
或HEAD
Content-Type
设为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
符合上面两个条件的都为 CORS
简单请求。简单请求都会直接发到服务器,会造成 CSRF
。
# 4.2 预检请求
不符合简单请求要求的请求都需要先发送预检请求(Preflight Request)。浏览器会在真正请求前发送 OPTION 方法的请求向服务器询问当前源是否符合 CORS 目标,验证通过后才会发送正式请求。
例如使用 application/json 传参的 POST 请求就是非简单请求,会在预检中被拦截。
再例如使用 PUT
方法请求,也会发送预检请求。
上面提到的可以防范 CSRF 的例外,就是指预检请求。即使跨域成功请求预检,但真正请求并不能发出去,这就保证了 CSRF
无法成功。
# 4.3 CORS 与 cookie
- 与同域不同,用于跨域的
CORS
请求默认不发送Cookie
和HTTP
认证信息,前后端都要在配置中设定请求时带上cookie
。 - 这就是为什么在进行
CORS
请求时axios
需要设置withCredentials: true
。
下面是 node.js
的后台 koa
框架的 CORS 设置:
/**
* CORS middleware
*
* @param {Object} [options]
* - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
* - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
* - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
* - {String|Array} allowHeaders `Access-Control-Allow-Headers`
* - {String|Number} maxAge `Access-Control-Max-Age` in seconds
* - {Boolean} credentials `Access-Control-Allow-Credentials`
* - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
* @return {Function} cors middleware
* @api public
*/
顺带一提,
Access-Control-Allow-Credentials
设为true
时,Access-Control-Allow-Origin
强制不能设为*
,为了安全,也是挺麻烦
# 5 密码安全
加盐
对于密码存储来说,必然是不能明文存储在数据库中的,否则一旦数据库泄露,会对用户造成很大的损失。并且不建议只对密码单纯通过加密算法加密,因为存在彩虹表的关系
- 通常需要对密码加盐,然后进行几次不同加密算法的加密
// 加盐也就是给原密码添加字符串,增加原密码长度
sha256(sha1(md5(salt + password + salt)))
但是加盐并不能阻止别人盗取账号,只能确保即使数据库泄露,也不会暴露用户的真实密码。一旦攻击者得到了用户的账号,可以通过暴力破解的方式破解密码。对于这种情况,通常使用验证码增加延时或者限制尝试次数的方式。并且一旦用户输入了错误的密码,也不能直接提示用户输错密码,而应该提示账号或密码错误
前端加密
虽然前端加密对于安全防护来说意义不大,但是在遇到中间人攻击的情况下,可以避免明文密码被第三方获取