对Matataki架构的一些思考

安全

  1. https://github.com/Matataki-io/Matataki-FE/issues/997 的回应

accessToken 如果放在非HttpOnly的Cookie里,脚本注入成功的话基本=拿到Token的值了。
API Server 和 Matataki-FE 并不在同一个domain下,Matataki-FE需要通过Header传递accessToken。如果是Android或是iOS这样的应用,那么找个相对安全的地方放置Token还是相对容易的,但是浏览器端都不好办,有的单页应用是直接放在被闭包保护的state里的,浏览器重新加载页面的话就重新登入。而要在有页面切换的应用使用,解决方案基本只会是Cookie,换成LocalStorage之类的地方只会更不保险。

大部分只是在开发层面上前端后端分离的,最后部署的时候还是会放在一个域下,站点前端控制器和API前端控制器分开来管理,前者做CSRF Token配合HttpOnly Secure Cookie传递SESSIONID或者JWT,后者用OAuth2,也更倾向和有后端的应用对接(不泄露ClientSecret),走authorization-code模式,安全性相对更高。

现在Matataki主站使用的基本是把两者当作一体对待的一般login,不过API Server Domain不同导致CSRF Token+HttpOnly Secure Cookie模式没法使用了。

而Developer平台提供的对接形式应该算是implicit模式的变种,应该是考虑到对接的APP不少是无后端的。

一般的APP用短期的JWT放在可被读到的Cookie里也还好,理由也是攻击的时间窗口并没有那么长,影响多少可控,而当前API涉及范围还挺大的,除了平台文章相关功能还包括Fan票部分,这里关系到用户的核心资产,如果授权了Fan票操作权限看来没有额度授权之类的,非常敏感了。

目前想到的主要改造思路:

  1. 权限进一步细化,授予Fan票相关权限的时候加上额度限制,并控制JWT的有效时长
  2. 涉及到转账、支付相关的API需要二次认证,可以有小额免密类的提升体验

这两个不调整架构也能做,要注意的细节会比较多,有API列表的话应该是针对特定API加强认证

对Matataki主站的改造可以考虑在matataki.io域名上搭一层薄的API转发网关,主站走这个API,accessToken放在主站的HttpOnly Secure Cookie里,在网关转发的时候把Token放到Header里提交给API Server

Developer那边也建议支持authorization-code,让第三方服务的后端也能用上HttpOnly Secure Cookie,现在的API Server模式在Server to Server的交互中使用。
现在的模式在静态Blog这种无后端的场景还是很有用,不过一个不注意是真的有可能从脚本泄露的。


如果Login Success在API Server的Domain下写HttpOnly Secure Cookie,API的调用根据条件检查Cookie和Header会如何呢?
没有其他后端的纯Client App/DAPP也没必要取这个Cookie里的JWT传给其他服务端,不能用JS读出来不是问题,有必要和其他后端交互的应用可以改造成authorization-code模式,在使用上似乎有优势。
问题在于一次登入就能在多个使用API的Client App应该是通用,会造成CSRF。即使根据申请的App来维护Cross-Origin清单,恶意站点如果去事先申请App,恐怕是没有那个精力去一个个审查的。这类跨站授权本来也是靠用户自己判断Client App是否值得信任,毕竟用户自己点授权的么。但是用户信任A站点生成的Token如果能被拿到B去使用性质就不一样了

如果是靠API返回值记录一个accessToken(和现在一样),再额外加一个HttpOnly Secure Cookie呢?
即使accessToken通过脚本泄露了,没有Cookie请求也无法通过;
即使在网站A请求到了Cookie,用户被某种方式引导带着Cookie访问危险站点B,没有accessToken也不行;
这样攻击就要再网站A构造脚本注入,读到accessToken后引导到危险站点B,两个都全了。那么签发accessToken的时候限定使用域,在B用这个accessToken请求会发现这个accessToken只能在站点A使用,也能拦截下来;

直接在Cookie里带上授权域信息也行,如果用同一个KEY,体验上会变成在其中一个App登入,另一个会被下线,从安全来说倒也不是问题;
如果不同的clientId写进不同的KEY里,accessToken里带上clientId,根据不同的clientId去检查不同的Cookie,应该能做到并存

这种模式下,API Server检查accessToken的逻辑是这样的:

  1. 判断API Endpoint是否需要accessToken才能访问,如果是没带accessToken,accessToken签名无法验证,accessToken过期等,拒绝
  2. 判断这个请求是不是带的accessToken是不是签发给谁的
    2-A. 是签发给某个服务端(可以包括matataki.io)的,如果是泄露的也说明服务端没尽到保管好accessToken的责任;不过还是可以通过检查请求头,请求IP等形式做进一步的安全判断,比如明显是通过浏览器发起的请求可以直接拒绝掉,不符合预设的使用场景;如果没有其他问题就判定通过
    2-B. 不是,是签发给浏览器前端使用的,判断是否有带上HttpOnly Secure Cookie(可以根据clientId检查不同的Cookie),如果有而且有效(格式也是一种JWT,不过内容和accessToken不同),那么通过,如果没有,拒绝
    2-C. 是签发给某个Mobile App的,按最普通的accessToken处理就可以了

PKCE解决的应该是clientSecret暴露在外的问题,本来也是Mobile App没有合适的放clientSecret位置的解决方案

https://www.oauth.com/playground/authorization-code-with-pkce.html

对SPA来说每重开一次都重新登录一次也不算不可接受了,涉及到币的话说不定反而是安全的证明
如果要更长时间保持,还是需要参考上面那种带些定制的手段了

有一些参考文章,可能有帮助:

https://wso2.com/library/articles/a-primer-on-oauth-2-0-for-client-side-applications-part-3/

https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead

  1. MatatakiAuth的JWT验证问题

当前是直接取JWT的payload解析回json使用的,没有验证签名。
通过userId+eth address等可以构造出这样的报文,有可能伪造身份。邮箱或者钱包地址在文章列表的author里是明文
userId在首页的url里就可以确认。
应该用签发JWT的KEY验证签名,为了减少KEY的扩散可以是生成JWT签名部分用privateKey,各个APP用publicKey验签