前后端安全通信,WebAssembly直呼666

1,248 阅读8分钟

前后端安全通信,WebAssembly直呼666

WebAssembly 这个名词出来也挺长时间了,老页面仔除了日常搬砖,也没机会一睹芳容,正好最近所谓的安全团队挑事,给了一些机会近距离接触下。

:本文来自一位好友F的亲身经历,后文为方便叙述,使用第一人称来展开。

本文大纲:

  • 一个公司的故事,关于前后端通信安全的大讨论
  • ChatGPT:WebAssembly 介绍
  • 基于 WebAssembly 的前后端 ”安全“ 通信方案

故事的开头

公司有专门的所谓安全团队,会不定时的来挑一些系统扫描,看看有没有什么漏洞,比如前后端通信有没有额外加密(是的,额外加密,就是让秋儿在应用层实现一个简单的 https 机制)、你的webpacksource-map 有没有正常关闭(说的就是测试环境,想不到吧,就找你测试环境来挑刺,坚决不能让你在测试环境方便测试&定位bug)、JS代码有没有混淆(是的,只要他们看不懂,那就是棒棒哒)。

可能最近帝都的天气不太好,影响到了该团队心情,决定挑几个系统,搞点 所谓 的安全漏洞祭天。嗯,刚好我负责的系统中奖了。

想来去年也中奖过几次了,这次应该没有什么问题,被他们拿来说事了吧。事实证明,还是低估了他们。所谓安全团队的大佬,直接在自己浏览器里,打开代码调试界面,打上断点,哟西,居然打断点能看到前端的加密秘钥!马上立刻整改!

看着是AES秘钥,但其实我们是模仿了 https 的那套加解密流程:

  • 前端代码保存后端提供的 RSA 公钥A
  • 前端JS在页面加载时,会生成随机的 16 字节长度的字符串,作为 AES 秘钥B
  • 前后端通信的消息体,都使用 AES 秘钥B来加解密
  • 前端在请求后端接口时,会在请求 header 里带上(使用RSA对AES秘钥B进行加密之后的)秘钥C
  • 后端使用 RSA 私钥解密C,得到 AES 的秘钥B

这套流程里,其实 AES 秘钥不管能不能 在JavaScript里打断点看到 ,都对系统安全不会有任何影响。(严格来说,这套机制也是没个卵用,对系统安全的惟一影响就是,增加了前后端联调、定位问题的复杂度,棒棒哒)。

下面就是我和所谓安全大佬(下面简称“安全”)的对话:

安全:你这AES秘钥我打断点能看到啊,秘钥泄漏了,很危险,属于高危漏洞,必须整改

我:我们其实是用RSA来加密了AES秘钥的,而且这个秘钥每个人不一样,即使攻击者看到自己的,也没什么用

安全:那不行,打断点能看到,就会被攻击者利用

我:那我秘钥总得有个地方存吧,会JavaScript的都能打断点看到

安全:你能不能不用变量来存储密钥吗?

我:感觉有点难度(不用变量,我存你手机上么,每次要的时候,给你打电话?)

安全:试试 web worker 或者 web crypto api ,直接在浏览器内部处理,不暴露给JavaScript。或者试试禁止调试

我:web worker 也只是一个JavaScript文件,也可以打断点看到。web crypto api 就是一套加密的API,也需要地方存储秘钥啊(这里我说错了,后来尝试了下这个 web crypto api ,确实可以不暴露秘钥,但是由于我们还需要把AES传给后端,所以最终还是不能采用)。

我:其实这些安全措施都没用,我在自己浏览器上,都能拿到你的JS代码了,什么不能做呢。即使你用禁止调试,我也一样可以用本地代理,把你的JS内容换一下,去掉禁止调试的逻辑。而且禁止调试这种 伤敌800自损1000 的做法,实在没必要。你看看阿里腾讯百度,他们的接口消息体都是明文的,根本没有额外加密。

安全:明文是肯定不行的,在我们这儿肯定过不了。

我:那你给我一些,通过你们扫描的系统,我看看他们怎么做的。

然后安全给我发了一个系统,大概2分钟,JS打个断点,就找到了他们的秘钥。然后安全又给我发了其他系统,时间关系就不去找了,要找也是很轻松的找到。最后,安全给的指示是,不管怎么做,只要他们找不到我们秘钥就行。嗯,围绕这个目标,想到了今天的主角: WebAssembly

WebAssembly

🚀 什么是 WebAssembly?

WebAssembly(缩写:Wasm) 是一种运行在现代浏览器中的二进制指令格式,它设计用于:

  • 提供接近原生性能的执行速度
  • 与 JavaScript 共存,共享内存、调用函数
  • 支持多语言编译(如 Rust、C、C++、Go 等)

WebAssembly 并不是要替代 JavaScript,而是作为它的 高性能补充,用于处理计算密集型、性能敏感、需要安全沙箱隔离的任务(如加密、图像处理、音视频编解码、游戏、模拟器等)。

🧠 通俗解释

如果把浏览器比作一台计算机,那么:

  • JavaScript 就像浏览器的“脚本语言”,灵活但速度较慢;
  • WebAssembly 就像浏览器的“汇编语言”,执行效率极高;
  • 你可以用 Rust、C++ 等高级语言 写核心算法,然后编译成 WebAssembly,直接在浏览器中运行,而无需服务器参与。

WebAssembly的消息加解密方案

再次 reminding,我们的目标,并不是提供一个安全的系统,而是为了让 所谓安全团队,他们找不到我们的秘钥。

基于这个目标,我的方案是,把 RSA 加密、AES加解密的逻辑,都放到 WebAssembly 内实现,以我对他们的了解,他们应该打断点也找不到我的秘钥了。

不会 rust 语言?没关系,作为尊为的 cursor pro 会员,这些小事都让cursor去解决吧。

整天实现方案如下:

  • RSA 的公钥,放在 JavaScript 侧来维护,方便后续各个系统对接这套 wasm 加解密方案
  • WebAssembly 内部,维护一份 RSA 公钥(也就是上面的JS里维护的,会暴露函数给JS调用传进来)、AES随机秘钥
  • WebAssembly 暴露函数:
    • setRsaPublicKey:设置RSA公钥
    • getSecureAESKey:获取经过RSA加密之后的AES秘钥,用来传给后端
    • encrypt:使用AES加密
    • decrypt:使用AES解密

逻辑简单,没几行代码。但是在开发过程中,遇到了如下2个恶心的问题:

  • 之前RSA公钥的格式不对,原来JavaScript代码里的RSA公钥,没有适当的换行,导致 rust 里解析RSA公钥报错。经过 cursor 和 gemini 反复修改,终于找到这个问题了
  • 刚开始cursor给我的AES秘钥,是真的随机生成的,导致java端在解密的时候,遇到了很多问题。
    • 首先是AES秘钥长度不对,我在 rust 里生成的是 16 字节,但是在java端,经过一系列骚操作之后拿到的,变成了26个字节(某一次遇到的数据是这样,不是每次都是这些长度)。原来,java端在解密出来AES秘钥之后,又转换成了 UTF-8 的 string,然后后在使用的时候,又从这个string获取对应的字节数组。在这两次抓换中,导致了字节数据失真!根据cursor的解释,原始随机字节里,又的不是合法的UTF-8字符,在转换成UTF-8字符时,会被别的默认占位符替代,再从UTF-8字符转换字节时,默认的占位字符会变成3个字节,导致出问题。那为什么以前JavaScript版本的加密,没这个问题呢?翻了翻JavaScript的秘钥生成代码,原来是从固定的 [a-zA-Z0-9] 这个集合里随机出来的16个字符,这些都是合法的UTF-8字符,难怪java骚操作之后还是没问题。
    • 第二个问题,AES加解密的模式对不上,这个只能扣了扣java代码,发现用的是 ECB_MODE 这个模式,改了改 rust 代码,终于跑起来了!
    • 刚高兴了一半,又遇到一个问题。有的请求没有参数,传的是 undefined ,之前JavaScript版本的加密使用的 CryptoJS 看来能处理这种问题。在我的版本, JSON.stringify(undefined) 之后得到的还是 undefined (我还以为会是字符串的 "undefined"……),导致在传递给 rust 时获取字符串长度报错,只能继续加上默认值了

最后,感谢LD、感谢CCAV、感谢懂王,当然,更要感谢舍得买cursor pro的我,终于解决了这个问题,使得我们的系统安全得到了极大提升,足以应对任何内部外部的安全挑战!

哦,差点忘了,最应该感谢的是公司的安全团队,鞭策着我们不断进步。您辛苦了!鞠躬!

相关文档