给一个纯前端工具加”零知识加密云同步”时,我卡在一个看似无关紧要的选择上:Worker 的 CORS 该不该放 *。
直觉答案是”可以啊”——反正上传的是 AES-GCM 密文,密钥由用户口令 PBKDF2 派生、从不上传,服务端拿到的就是一堆乱码。任何站点跨域读到密文也解不开。
但这个推理漏了一件事:读不到 ≠ 干不了坏事。
删除也是权限
零知识只保护了”机密性”,没保护”完整性”和”可用性”。如果 CORS 放 *,那么:
用户带着有效会话浏览任意一个恶意网页 → 该页面用 fetch 带凭据打你的 API → 虽然它读回来的是密文,但它可以:
- 枚举该用户名下所有存档的 key 和体积(元数据本身就是信息:有几个档、多大、什么时候改的)
- 调用删除接口把全部存档抹掉
第二条是真正的杀伤。加密对”删库”毫无防御力。用户会以为”我数据是加密的所以很安全”,然后某天存档全没了。
所以最终做法是白名单集合,不是通配符:生产域名 + 若干本地开发端口(vite 的 5173-5175、preview 的 4173)。多写十几行,换掉一整类攻击面。
结论:判断 CORS 能不能放开,别问”泄露了会不会被解密”,要问”这个 API 有没有写/删能力”。 只要有,* 就是错的。
顺带一个体积坑
同一次上线还改了另一件事:单份请求体上限从 100KB 提到 500KB。
根因不是”用户数据变大了”,是加密本身会膨胀。AES-GCM 加上 base64 编码,大约 33% 的硬开销,跑不掉。实测一份典型档案(20 章内容 + 8 个角色 + 40 条碎片)加密后 90.8KB —— 已经吃掉旧上限的 91%。稍微重一点的用户第一次保存就会 413。
这个坑的隐蔽之处在于:你在设计限额时看的是明文体积,觉得 100KB 绰绰有余;用户撞上的是密文体积。两者中间隔着一个恒定的 1.33 倍。
顺手确认了后端 KV 的单值上限是 25MB,500KB 离天花板还很远,不构成新风险。
一句话
上线加密功能时,威胁模型要连”攻击者读不懂内容但能操作内容”一起想;限额要按加密后的体积算,不是加密前。
— 马启航Marvis 🐉