Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端安全系列 - CSRF 实战 #29

Open
HXWfromDJTU opened this issue Oct 26, 2020 · 0 comments
Open

前端安全系列 - CSRF 实战 #29

HXWfromDJTU opened this issue Oct 26, 2020 · 0 comments

Comments

@HXWfromDJTU
Copy link
Owner

前言

无论是BS时代还是CS时代,计算机诞生之初安全问题就一直是重中之重。安全也从来都是们大课题,是你出招我接招的武功比拼。本文仅结合当前见识过的一些招数,管中窥豹地去记录一些日常。

安全是体系

每一次成功的攻击,看似都是多个低概率事件结果。但墨菲定律告诉我们,会发生的事迟早会发生。

面向安全问题,自己问问自己:

  • 后端程序运行的环境一般是linux环境,那前端代码的宿主浏览器node本身你又了解多少呢?
  • 知道什么样的行为容易成为XSS的温床吗?
  • 知道浏览器cookie可以根据domain字段判断是否携带。那Same Sitesecure第三方cookie又了解多少呢?
  • 除了我们平时熟知的,XSSCSRF之外,网络劫持非法调用又该如何防范呢?

安全是一个体系,在恶意攻击面前,每一端每一环都是至关重要。程序从前端到后端,需要把安防这套组合拳打好配合,每个环节都至关重要。

CSRF

CSRF (Cross-Site-Request-Forgery),中文称之为跨站点伪造攻击。主要流程如下

  1. 受害者正常网站登录,并保存登录态到cookie中。
  2. 攻击者诱导受害者进入事先准备好的第三方网站。
  3. 攻击者准备好的第三方网站的恶意脚本中,会向被攻击网站发送跨站请求。
  4. 并且利用受害者原本就在被攻击网站中留下的cookie注册凭证,通过受攻击网站的后端用户检验。
  5. 冒充用户进行各种操作以到达攻击目的(转移资产查询敏感信息等)。

原理与特点

  • 不需要盗取和修改你的账户,而是利用你的身份。被攻击后,用户难以发现,网站开发者也难以发现。
  • 没有防备的情况下,十分容易中招。因为受害者可能就不小心点击了一个恶意连接,打开的页面一闪而过,攻击可能就完成了。

各色各样的CSRF

  • Get请求CSRF攻击,通常会埋藏在恶意网站中的一个图片链接中,或者给出一个超链接引诱用户点击。
    1. 是因为图片请求可已默认发起
    2. 是因为img请求可以绕过浏览器同源策略限制
    <!--  自动发起请求 -->
    <img src="https://safebank.com/assets/change-verify-phone?new_number=1345678901">
    <!--  引诱用户点击 -->
    <a href="https://safebank.com/assets/change-verify-phone?new_number=1345678901"></a>
    上面👆的请求可以要求银行更换用户绑定的安全手机号。没错,利用的是你当前浏览器中cookie中的身份token做的身份验证。
  • POST类型的CSRF
     <form action="https://safebank.com/assets/change-verify-phone">
       <input type="hidden" name="new_phone" value="1345678901">
       <input type="hidden" name="other_param" value="xxxxx">
     </form>
     <script>
       document.forms[0].submit() // auto submit
     </script>
    通过以上示例,我们得知:
    1. 通过表单自提交请求仍然能够轻松模拟POST请求,并且不阻碍浏览器携带用户token
    2. 了解http协议的同学,也知道GETPOST在请求上其实是相同的,只是两端的读取方式上不一致罢了。
  • CORS类型的CSRF
    大家会也许好奇,为什么要把个类型拎出来?CORS不也是归类到POST或者GET吗?
    • 在后端设置允许跨域请求Response Header中的 Access-Controll-Allow-Origin的时候,为什么不建议设置为*的原因。
    • 设置为*意味着来自所有域的脚本,都可以对这个借口进行跨域访问,多增加了一层风险。

防范措施


知己知彼,从上面的总结可以看出CSRF的特点

  • 攻击的发起大多数都发起自第三方网站,除非你的网站已经被XSS攻击了。
  • 攻击者并无法读取你的cookie信息,只是借用你的cookie而已

针对CSRF特点,我们针对性可以做出防范:

  1. 直接阻止外域访问

    • 判断请求来源。
    • 防止外域借用cookie中的token
  2. 添加外域获取不到的信息到请求中

    • 知名的 CSRF-Token 方案。

判断请求来源

都知道http请求是无状态、无连接的,每一次请求的信息都携带在了请求与相应头中,大家是否还记得OriginReferer这两个Request Header呢?

Origin 与 Referer

The Origin request header indicates where a fetch originates from. It doesn't include any path information, but only the server name. It is sent with CORS requests, as well as with POST requests. It is similar to the Referer header, but, unlike this header, it doesn't disclose the whole path.

The Referer request header contains the address of the page making the request. When following a link, this would be the url of the page containing the link. When making AJAX requests to another domain, this would be your page's url. The Referer header allows servers to identify where people are visiting them from and may use that data for analytics, logging, or optimized caching

以上分别是是MDNOriginReferer 的描述,都可以标明当前请求的来源,而Referer更详细。但我们更需要关心的是这二者发送与不发送的情况。

  • Origin
    1. 仅在发生跨站点请求、或者同域的POST请求下携带
    2. IE 11 中CORS请求也不会写带Origin请求头
    3. 302重定向之后的请求中,不包含Origin请求头
  • Referer
    1. 协议为表示本地文件的 "file" 或者 "data" URI 时不发送。
    2. 当前请求页面采用的是非安全协议,而来源页面采用的是安全协议(HTTPS)。
    3. IE 6IE 7下,使用location.href或者window.open进行跳转都不携带referer

防范措施

Origin 或者 Referer 属于自己的白名单中,则直接拒绝访问。简单粗暴地处理,会有以下问题:

  • 从以上可以知道OriginReferer请求头是不完全可靠,只能够借助这两个请求头进行初步防御
  • 用户通过搜索引擎跳转访问页面的时候,referer一定会是搜索引擎的域名。会拒绝掉正常流量。
  • 若你的网站显示被XSS攻击后,那么攻击方就会很粗暴地成为你自己的Origin了。

使用 CSRF Token

要说读取Origin/Referer Header信息像是检查你的车票,那么CSRF Token则像是则是检查你的身份证了。区别就在于你能够拿出一些,别人拿不出来的东西,以证明该请求的合法性。

实现步骤

  1. 下发 CSRF Token 给页面
    • 用户首次打开页面的时候,服务器需要给用户生成一个CSRF Token(一般为随机字符串和时间戳的哈希),服务器存储一份,下发给客户端一份。
    • 为防止 CSRF 攻击者利用,不再使用Cookie存储和提交CSRF Token
  2. 前端开发者使用其他位置存储,并在后续的每一个请求参数中携带这个CSRF Token
    • 一般可以在beforeSendintercepter中统一添加
  3. 服务器根据Token的合法性,决定是否要处理此请求
    • 包括验证随机字符串合法性
    • 和验证时间戳是否超过有效期

存在的难度

  1. 下发的Token是页面级别的,在大型网站中session存储大量的CSRF Token压力是巨大的。
  2. 在多台服务器分布式机器的环境下,需要使用Redis作为多台服务器的公共存储空间
  3. 后端需要以页面为维度去处理Token,工作量巨大。
  4. 前端来看,有些不能使用统一拦截器的请求。比如<a>标签跳转、<form>提交,则需要手动添加CSRF Token

验证码 与 支付密码

回想我们使用金融类的App时候,就算你已经登录了,在支付的时候也需要再次输入支付密码呢?这里面的支付密码验证码,其实就相当于支付这个请求的CSRF Token。这个方法适用于少数关键的几个接口。

{
  email_verify_code: "1213"
  password: "250f78769f22a8c43a2b767fde4b093fbbcdc28bd7ecac4bad883a4b0fcf30e3"
  timestamp: 1603684913776
  value: "2"
}

简化版(反向) CSRF Token

在业务处理中去验证token的有效性极大地添加了开发工作量。那么有没有更轻便的形式完成简单的CSRF Token工作呢?尝试一下流程:

  1. 前端项目配置文件中,动态地生成一个signKey,作为后续加密使用。
// app.config.ts
const CSRFTokenInfo = {
  appName: 'bank',
  signKey: 'ajhsbdkjasu123123bjsbkd'
}
  1. 前端开发者,在请求服务器接口时,使用signKey请求参数,生成请求的signature:
    • 获取该请接口的path、当前时间timestamp、本应用的idappName作为、签名参数signKey作为准备参数
    • 将以上四者,按照一定规则拼接在一起。(规则不固定,但要服务器便于还原读取)
    • 使用md5进行加密生成signature
    // request.interceptor.ts
    export function signRequest (config: RequestConfig) {
        const key = CSRFTokenInfo.signKey
        const timestamp = Date.now()
        const fullPath = [(config.baseURL || '').replace(/\/$/, ''), (config.url || '').replace(/^\//, '')].join('/')
        const path = new URL(fullPath).pathname
    
        // 明文签名
        const text = `app=${CSRFTokenInfo.appName}&timestamp=${timestamp}&path=${path}&signature=${key}`
        // 加密签名
        const signMessage = md5(text)
    
        // 绑定到请求头中
        config.headers['x-service-app'] = CSRFTokenInfo.appName
        config.headers['x-service-timestamp'] = timestamp
        config.headers['x-service-signature'] = signMessage
    
        return config
      }
  2. 将生成签名过程的input(app、timestamp)与output(signature)通过请求头传递到后端
    // other request header...
    x-service-app: bank
    x-service-signature: 12dsf222ssdf312d334sddzxczxcz
    x-service-timestamp: 1403691292039
  3. 服务器准备:
    • 为了减少业务方的开发,签名校验服务推荐在nginx + 本地脚本校验的形式完成
    • 获取到前端请求中携带的apptimestampsignature参数,结合nginx中拦截到的请求path,和前端一样重新计算一次signature,并且比对结果
    • timestamp字段则另外再加一个防重放时间的检测
    • 通过校验则放行请求到业务处理方层,否则直接拒绝
  4. 那么signKey两端该如何同步呢?
    • 与服务端下发CSRF Token不同,这次我们反向去考虑问题
    • 项目开发时通过本地脚本,将app.config.ts中配置的signKey加入到发布脚本中
    • 项目发布时,ci机器一般拥有ssh到正式环境的权限。
    • 通过发布脚本在ci发布流程完成后。执行特定的script以启动服务器上更新signKey为前端本地的值。
      # build 项目 .....
      # reload 项目 ....
      # 更新signKey
      python xxx/xxx/update-sign-key.py -- bank ajhsbdkjasu123123bjsbkd

优缺点

  • 可以看出signKey的更新是由前端驱动的,只有在发版的时候才会更新,实时性并不如第一种方案。
  • 统一在nginx层处理,可以大量减少后端同学的开发工作量。

SameSite Cookie

所谓成也Cookie,败也CookieCSRF则是利用浏览器的这个规则漏洞建立起来的攻击方式。前面说到,安全每一个环节都有责任,那么作为前端主要运行环境的浏览器,是否也可以在Cookie规则上进行优化呢?

时间来到了2020年秋天SameSite Cookie已经被大多数浏览器所支持了,甚至于有“浏览器全面禁止第三方Cookie”的传闻(连接),这里推荐一个小工具给大家测试自己浏览器的Same-Site Cookie支持情况,传送门==>

cookie中授权 tokensame-site属性设置为Strict则可以直接防止CSRF的发生。

发现CSRF

根据上面的总结,CSRF攻击,通常有以下的特点

  • 第三方发起: 说明我们要关注非白名单中的域,发起的跨域请求
  • 使用img作为CSRF请求时,Request Header中的MIME Type为图片,而想要窃取的数据返回的MIME Tpye一般为plain/textjson等文本信息
  • 后端童鞋在review 日志的时候,需要多关注以上类型的请求

总结

  • 应用安全每一个环节都有责任,勿以事小而不为。被攻击时,每一个环节都不是无辜的。
  • 拒绝绝非同源访问
    • Origin 与 Referer 请求头识别
    • CSRF Token
      • 服务器下发式
      • 验证码二次验证式
      • 客户端主动同步式
  • SameSite Cookie的使用
  • 要善于观察发现服务上的CSRF请求

参考文章

[1] HTTP headers 之 host, referer, origin
[2] 浏览器的沙箱模式
[3] 前端优化 - by Alex 百度
[4] 前端安全系列(二):如何防止CSRF攻击? - 美团技术团队
[5] CSRF 漏洞的末日?关于 Cookie SameSite 那些你不得不知道的事

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant