阅读 1496

2020年前端安全综述-填坑记

前言

上一篇面试总结中其实埋了很多坑,做到点到为止,但是坑还是需要填的,今天这篇文章就是填第一个坑。上篇总结中就有一个题目

33.能说一下你项目中遇到了哪些安全问题么,一般都是怎么解决的?

那么前端平时开发中涉及到哪些安全问题呢,又都是怎么解决的呢,本文将一网打尽,同时建议各大中小公司,能够在公司内部实施的安全措施都应该实施起来。

以下是正文

随着前端的发展,前端应用正在迅速变化。 前端代码承担着与后端代码几乎相同的责任,可以做更多的事情,随着公司体系越来越完善,开发框架和平台的不断成熟,需要开发者考虑的安全问题越来越少,但并不是开发者就不需要关心项目的安全问题。

本文主要介绍几种业务开发中经常遇到的几种前端安全问题,由于篇幅有限本文点到为止,后续有机会会逐一展开来讲,本文提供大量的图例来说明问题。

1. XSS

XSS是跨站脚本攻击的简写,攻击者想尽一切方法 将一段脚本内容放到目标网站的目标浏览器上解释执行。攻击者将恶意脚本输入到目标网站中。 当其他用户访问该网站的时候,由于浏览器不知道它是由网站提供服务的脚本还是攻击者埋入的脚本,因此将执行此该脚本。攻击者就可以很容易利用埋入的脚本进行攻击。

  1. 攻击者编写恶意攻击的脚本
  2. 攻击者访问前端页面,在输入框中输入编写好的恶意脚本
  3. 攻击者将恶意脚本进行提交,后端将恶意脚本存储在数据库中
  4. 当某些合法用户访问该网站的时候,该网站会获取存储在数据库中的恶意脚本,但是浏览器不知道它是恶意脚本所以执行了。

其实就相当于攻击者在用户端的页面上注入了一段脚本,有了这段脚本攻击者就可以为所欲为了

防范
  1. 永远不要相信用户的输入,对用户输入的特殊字符串进行转译,针对用户的输入设置标签白名单
  2. cookie设置HttpOnly,配合token或验证码防范
  3. 设置CSP安全策略-可以通过两种方式设置CSP,一种是meta标签,一种是HTTP响应头Content-Security-Policy

2. CSRF

CSRF是跨站请求伪造的简写,一种诱骗受害者提交恶意请求的攻击,攻击者盗用了你的身份,以你的名义发送恶意请求,请求到达后端时,服务器将无法区分恶意请求和合法请求。。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账等。

CSRF攻击必须具备两个流程

  1. 登录受信任网站A,并在本地生成Cookie。
  2. 在不登出A的情况下,访问危险网站B。
防范
  1. 同源检测,直接禁止外域(受信域可以开白名单)对我们发起请求
  2. CSRF Token,就把Token以参数的形式加入请求了,提交给服务器的时候,服务器需要判断Token的有效性
  3. Samesite Cookie属性,Samesite=Strict只允许同源网站提交请求携带cookie

3. 网络传输安全

中间人 (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独立的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过一个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制. 在中间人攻击中, 攻击者可以拦截通讯双方的通话并插入新的内容。

是不是觉得有了https网络传输安全问题就迎刃而解了呢,即使被中间人拦截了,数据也是加密的。其实不是这样的,不知道大家有没有使用过charles进行抓包呢,如果数据都是加密的,为啥charles抓包后我们能够看到传输的明文呢,其实这就是中间人攻击。

charles中间人劫持

防范
  1. 对于个人来说防止自己被中间人攻击最基本的就是不要乱连不信任的网络
  2. 公司APP来说应该配置禁止被抓包
  3. APP和浏览器都应该严格校验证书,不使用不安全的APP和浏览器

4. 接口加签

通过上面的例子我们知道https并不是绝对安全的,他是会被中间人劫持的,那么我们有什么方法防止数据被串改呢?

接口加签的目的是防止数据被串改!

举两个例子

例子1:正常用户提交转账申请,请求中携带正常用户的用户信息,他想转账N金额给用户A,这样的请求银行没法拒绝会正常转账,因为携带了正常的用户信息。但是当中间人劫持了这个请求,他修改了转账账号为B,修改了转账金额为M,这样我们的钱会不会转给其他人呢?

例子2:我们辛辛苦苦写了一个运营小游戏,违规用户随便玩了一下得分为0,但是他通过Charles拦截了这个请求,修改了得分为10000,然后进行提交,我们的正常服务器能否知道分数是被串改的呢?

为了解决上述问题,我们可以引入接口加签

服务端网关首先会校验签是不是对的,如果不对直接拒绝请求,而签的生成和请求参数密密相关,当接口请求中的参数被串改后,网关是没法进行验签通过的,直接拒绝了请求,抛出错误。

5. 接口加密

有时候我们的参数根本不想被人看见是啥,我们就可以利用参数加密了

接口防重放

![重放small]

防重放也叫防复用,简单来说,就是我获取到这个请求的信息之后, 我什么也不改, 我就拿着接口的参数去重复请求这个充值的接口,也就是说我的请求是合法的,因为所有参数都是跟合法请求一模一样的,也就是说: 服务端的 sign 验证一定能通过。如图上的例子,即使我们不知道登录账户名密码,即使接口参数被加签加密了,我们依旧能够登录并拿到登录信息,我们根本不用关心加密加签的逻辑,我们只需要简单的重放攻击即可。

6. 防重放设计
  1. 客户端在请求中添加两个参数 1.1 添加一个随机不重复的字符串参数 比如uuid 至于怎么让他不重复,可以考虑拼接时间戳,md5随机数等 1.2 添加一个请求时间的参数 如 time 值就是发送请求时的 时间戳
  2. 服务端接收到请求之后: 2.1 去缓存里中查找 uuid 这个参数对应的值是否存在 2.2 如果不存在: 就把这个uuid的值保存到缓存中, 记录这个请求 2.3 如果已存在: 存在那就证明, 已经请求过一次了, 就不处理这个请求了

这就是最简单的防重放逻辑,接口只能调用一次,即使被中间人攻击后也没法进行重放

7. 环境检测

是不是浏览器

是不是我们检测上述变量就认为是浏览器环境呢,其实不是这样的,上面的变量都是可以被串改的,所以可以作为参考,绝对不能过分的依赖!下面列举几项处理方案,可以看到当我们检测这些变量的时候,这些变量都是可以被串改的。

检测变量对抗处理方案
navigator.languagesObject.defineProperty(navigator, 'languages', { get: () => ["zh-CN", "zh", "en"] });
navigator.pluginsObject.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
是不是模拟器

一般我们检测到这些变量的时候可以无脑的认为就是模拟器,比如Puppeteer中我们启动的时候,navigator.webdriver这一属性的值等于true的,常规浏览器中由于没有这个属性navigator.webdriver的值等于undefined的。

Object.defineProperty(navigator, 'webdriver', {
	get: () => undefined,
});
复制代码

攻击者这样串改后我们是不是就没有办法知道是不是webdriver了呢?其实我们还是有办法判断的,因为这边只是返回了navigator.webdriver的值是非的,但是navigator上依旧有webdriver这个属性,我们有没有办法检测属性是否存在呢?其实我们很容易拿到navigator上所有属性的。

var attr = window.navigator, result = [];
do {
    Object.getOwnPropertyNames(attr).forEach(function(a) {
        result.push(a)
    })
} while (attr=Object.getPrototypeOf(attr));
复制代码

当我们判断navigator上有webdriver这个属性的时候,就可以简单的认为这个是模拟器环境,是不是觉得很完美的判断了是不是模拟器了,其实不是的,攻击者甚至可以删除掉webdriver属性。

delete navigator.__proto__.webdriver
复制代码

这样之后就完全抹去webdriver变量了,通过这个办法来判断是不是模拟器就没有路子了。

有没有用户行为

通常我们可以通过判断事件上的isTrusted属性来判断是不是真实的事件,大部分情况我们都能够很好的处理,但是攻击者是很可怕的,这些简单的伎俩他们能够轻轻松松的绕过,他可以重写事件啊,比如:

function clone(e) {
    const t = {};
    for (let attr in e) {
        if (typeof e[attr] === "function") {
            t[attr] = e[attr];
        } else {
            Object.defineProperty(t, attr, {
                get: () => {
                    if (attr === 'isTrusted') {
                        return true;
                    }
                    return e[attr];
                },
                set: v => {
                    e[attr] = v;
                }
            });
        }
    }
    return t;
}
const oldAEL = document.addEventListener;
window.addEventListener = document.addEventListener = function (e, func, c) {
    const newFunc = function (event) {
        const newEvent = clone(event);
        return func(newEvent);
    };
    return oldAEL.call(this, e, newFunc, c);
};
复制代码

通过上面的例子我们发现,不管我们怎么攻防,攻击者都是有办法绕过去的。其实上面还都是简单的攻防,攻击者甚至可以自己定制浏览器,当我们的页面跑在攻击者定制的浏览器中的时候,通过上面的那些方法我们真的无能为力了,那么是不是我们只能放弃了呢,其实不是的。

辨别机器行为还是得需要验证码

8. 无处不在的验证码

验证码这个名词真正被发明出来是在2003年,这比很多概念晚多了,比如神经网络70年代就已经有很多人在研究。卡内基梅隆大学的Luis von Ahn,Manuel Blum, Nicholas J.Hopper等人首次提出了“CAPTCHA”这个词。他们对验证码系统做的很深刻的研究,并且将其付诸程序化。自此大量的验证码开始被应用到网站中,有效的阻止了黄牛软件的肆虐。时至今日,每天有过亿的验证码被人们不断地输入着。

传统验证码

传统验证码易被各图像识别软件、打码平台轻易破解,人工智能飞速发展,因此扭曲的文本验证方式也不再是一个可靠的方法,据说已经能够解决99.8%的图片字符型验证码。由此诞生了很多新型的验证码类型,其中国内最具代表的就是极验,国外的就是谷歌的reCAPTCHA,他们带来了一种全新的模式。

新型验证码

新型验证码不仅很难破解,他的交互会更加的友善,甚至做到无验证码,只有在需要进行验证的时候才出来。下面是网易易盾的产品流程图,其他产品都基本类似。背后依托强大的机器学习判断行为到底是不是人。

9. 代码加密混淆

代码加密混淆大大降低了前端代码的可读性,同时一定程度上会增加代码的体积。但是对于非常核心的业务逻辑,代码加密是非常有必要的,比如:

  1. 前端加签代码,由于加签是在前端进行的,前端必须存有秘钥和加签规则,但是一旦被第三方知道加签的秘钥和规则,加签也就不公而破了,所以加签的前端代码必须得加密。
  2. 新型验证码用户行为采集代码,新型验证码涉及很多用户行为的前端采集,然后提交后端分析,如果采集规则被第三方知道,那么攻击者也就很好的进行攻击行为,所以采集代码也是需要加密的。