Skip to content

前言

聊聊前端安全SDK的设计和实战,

需求设计

当我们设计一个前端安全SDK时,我们需要这个SDK能干嘛,畅想一下场景。

  1. 能够做一些基本的安全工作,比如csrf、xss攻击
  2. 判断当前宿主环境,判断是否是机器人访问
  3. 能够对消息进行拦截,做出一些自定义行为,比如全局弹窗要求校验
  4. 采集宿主信息,生成指纹

还需要满足一下功能

  1. 对于接入方来说是无感知的,还不能侵扰业务的正常业务。
  2. 加载速度一定要快,
  3. 在宿主环境不满足时,快速降级,不能影响正常业务
  4. 效率要高,尽可能复用
  5. 防止别人调试
  6. 防止安全代码被破解

拦截系统和风险系统

拦截系统是能够对风险操作直接拦截,风险系统是在某个危险操作触发时及时上报通知相关人员,触发后续操作

技术准备工作

脚本注入

如何将安全SDK代码注入,首先安全SDK的代码必须运行在所有的脚本代码前面,包括内联代码,最好的方式无感知注入,通过openresty直接在返回的文档中插入代码。

脚本是由一个个插件的构成的,有些插件并不需要立刻执行,比如用户行为采集,浏览器静态指纹(前置运算)、验证码功能等,可以后置异步加载形式注入

不同业务可能配置的功能不同,比如有些业务方可能不需要某些插件的功能(包括同步插件),需要一种在编译时注入插件的功能,减少SDK的代码量。

浏览器指纹

浏览器指纹是设备固有的属性,和当前页面业务无关,在用户未登录的情况下能对用户进行唯一化。目前浏览器技术分为3代 第一代是基于cookie和every cookies的,是有状态的协议 第二代是采集浏览器的固有信息, 第三代是采集用户行为信息和同系统的跨浏览器的识别

目前指纹采集技术进入到2.5代技术,通过采集浏览器固有信息和部分用户行为

  1. 字体 利用的原理是如果某个某个字体不支持,那么会使用后面的字体。比如设置字体为font-family: 'somefont defaultfont'。如果字体大小等于defaultfont大小,说明somefont不支持,反之就是支持。需要注意的是,最好把字体的判断放置到ifram执行,避免宿主网页的影响
  2. 字体性能
  3. cpu cpu内核数
  4. 颜色深度(能够检测是否在虚拟桌面) window.screen.colorPath
  5. 屏幕分辨率
  6. 可用最小内存
  7. 语言 获取用户的系统所有语言和当前应用的语言 navigator
  8. navigator.hardwareConcurrency 虚拟线程既4c8t 中的T
  9. 时区 时区有两种获取方式第一个是window.Intl.DateTimeFormat.timezone, 第二个直接获取当前时间的getTimeZoneOffset
  10. 是否支持sessionStorage 或者 localStorage
  11. 是否支持indexDB
  12. 是否支持openDataBase
  13. 平台 platform.platform
  14. 插件列表,现在插件只能获取内置的列表,不能获用户所有的插件
  15. vendor navigator.vendor
  16. 某些特定的全局变量, ucweb
  17. 是否支持cookies,直接本地写一个然后再读
  18. 是否支持某些css 媒体查询
  19. 是否hdr屏 媒体查询 dynamic-range
  20. 支持的数学方法
  21. canvas指纹
  22. 检查是否支持touch事件,ontouchstart是否存在,检查createEvent('TouchEvent')、navigator.touchPoints

上面指纹又区分了基础指纹、高级指纹和硬件指纹。基础指纹的重复过高,高级指纹也可能会有部分重复项的,硬件指纹不太容易获取。

上面指纹因为可变会的量过多,怎么样设计权重是一个问题,解决方案是针对不可变的数据进行分组摘要,对于可变的数据进行端摘要, 摘要的算法简单采用md5即可。

xss

xss有三种,反射形、存储形和dom形,前面两者的区别是有没有经过后端存储,dom形是指渲染了某些的不合规的dom属性导致注入。 xss的危害是最大,相当于将整个页面的控制权完全交给了第三方,第三方脚本可以轻易将用户的cookie,界面信息,用户信息传入到第三方中。除了传统的后端的防范,前端也需要规避xss。

作为安全SDK需要具备主动防御功能,因为xss总是脚本执行,所以需要总是将安全SDK放置到所有脚本的前面

  1. 首先来解决dom形的注入。dom形注入一般是如下的形式。
html
<!-- 理论上应该是这样的 -->
<img src="xxx.png" />
<!-- 实际变成了这样 -->
<img src="xxx.png" onload="alert('xxx')" />
<!-- 注入的内容是  xxx.png" onload="alert('xxx') -->

一种常见的办法是开定时器对全局的dom进行扫描。检查是否存在可以事件,但是现在是ajax的时代,这种方式效率极低。 首先对内联的事件进行监督,在document上面对所有的事件在捕获的极端判断e.target上面的事件定义,是否会触发关键字。触发后停止后续的事件,并及时上报。对于已经判断过的的dom添加flag,后续直接跳过。比如我们的内联事件代码总是不会太长。如果某个内联事件的代码过长,黑客又不知道,可能会立马触发扫描。

  1. 如果是dom形注入的是外部的脚本,上面那种定时扫描就会生效。外部的脚本总是需要创建一个script脚本或者引入一个脚本,引入一个脚本总是需要添加src属性,从这个下手,对createElement进行重写,获取到script元素后,对script元素的src进行拦截,如果发现是外部脚本直接拦截并报警
js
const originFn = Document.prototype.createElement;
Document.prototype.createElement = function(...args) {
  const element = originFn.call(this, ...args);
  if(element.tagName === 'SCRIPT') {
    Object.defineProperty(element, 'src', {
      set(url) {
        if(checkUrl(url)) {
          return url;
        }
      }
    })
  }
}

上文中还有一个万一别人把apply重写一把,那不就失效了。为此还是需要使用自己的apply,再把apply设置为不可配置即可。

有些人说不让createElement, 那还可以cloneElement,也给干掉即可。

当然别人也能通过document.write 把你所有的代码禁用掉。我们也可以在DomContentLoaded后直接把document.write重写掉,添加监控事件。

  1. 有些页面是反射性注入,大致是通过构造一个同域iframe来实现,这时候iframe是能正常携带cookie,而且不会被主域名的事件监控,iframe可以拿到contentWindow,直接contentWindow也添加上保护程序即可,唯一麻烦是怎么知道iframe什么时候被创建,监听mutationObject 会触发domnodeinserted事件,监听这个即可 浏览器中还有直接禁掉eval和new Function的

现在的浏览器通过csp字段已经实现了部分上面的功能,能够限制网页内嵌代码等功能

csrf拦截

crsf跨站请求伪造,如同名字主要是通过伪造达到目的,目前防御的方式有两种,第一是总是验证请求来源,对于无来源或者来源不是白名单的直接忽略掉,第二个是双token,除了默认的token外总是添加自定义的toke在请求中(url、head或者body)。

安全SDK需要完成的内容是对于关键请求总是添加双token认证,将token保存在document下或者sessionStorage下,在form请求时总是携带上,对于动态表单总是在action后面添加,如果是ajax请求直接全局拦截

全局请求拦截

对于请求的拦截,跳转性都交给后端进行拦截,前端SDK需要解决的是ajax请求和fetch请求

ajax请求

ajax通过open构建请求信息,通过send构建body信息,通过response 添加响应信息,我们只需要对这3个方法进行重写即可。之前有通过Proxy实现对ajax重写,性能大概比Object.defineProperty 慢4倍左右,直接修改原型是最好的办法

fetch

fetch拦截稍微简单一些,fetch本质是一个函数,只用给函数添加一层,并且重写res.json方法即可。

验证码功能

对于一些高危的请求,在拦截后需要验证码校验当前用户的,对于跳转的页面,直接跳转到相应的页面即可,对于ajax或者fetch,就需要全局拦截后通过iframe的形式来添加验证码,并且根据登记渲染不同验证码,比如手机验证码或者图形验证码等。

验证码校验成功后如何重放请求,建议是不重放请求。让页面直接报错即可。

错误监控

前端中有两种错误需要监控,通过error(不用onError,onError不能监控资源加载失败的问题)和unhandleRejection 抓取异步的错误,抓取的内容

上报功能

数据上报,通过sendBeacon和gif上报,设计了优先级和上报队列的方式,gif上报有字节数限制,一般设置为2047个。

插件系统设计

插件的基本功能是注册,然后在core各个阶段被调用。在什么阶段被调用完全看插件本身做了哪些订阅操作。静态插件还好,如果是动态插件则还需要加载器。动态插件加载时机在core流程的最后, 首先对全部模块进行梳理,有环境检查、工具类()、加载器(加载第三方插件)、日志上传、ajax拦截器、fetch拦截器、xss拦截器、xss预警器、csrf拦截器、验证码模块、指纹模块、反调试模块、错误加载器、行为模块 每个模块都要求是可以配置控制的。

core执行流程如下

  1. 初始化事件监听器
  2. 初始化上下文,工具类
  3. 内置插件注册,包括工具类插件
  4. 执行静态插件注册
  5. 初始化加载器
  6. 加载动态插件并执行

引用文献

  1. fingerprintjs2
  2. Canvas,WebGL 以及 AudioContext 指纹原理
  3. 真正的感知:Web 客户端追踪技术
  4. 人的唯一性识别
  5. everycookie
  6. csrf