懒猫SSO 第二章:为什么 Client 不能信懒猫“返回结果”

忘机山人

发布于61天前
博客图片修整中,看不了可以先搜索公众号“忘机山人”看。

在第一章里,我们刻意做了一件事:
完整走完了一次“正常的懒猫 SSO 登录流程”,但没有解释任何安全细节。

https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env

如果你回看那个流程,会发现一个非常危险的现象:

整个过程中,ENV 查看器几乎没有“主动验证”任何东西。

它只是:

  • 把浏览器重定向到 /sys/oauth/auth
  • 等待浏览器跳回 /callback
  • 拿到一个 code
  • 换回一组 token

然后,很多懒猫 App 就直接宣布:

“登录成功。”

这件事之所以看起来合理,是因为我们在脑海中默认了一整套“看不见的前提”。
而这一章的目的,就是把这些前提全部显性化


2.1 一个被默认的信任模型

当 ENV 查看器收到一个 id_token 时,通常会发生如下对话(虽然没有写在任何代码里):

  • 懒猫 SSO 说:

    “这个 token 代表用户 alice。”

  • ENV 查看器心里想:

    “好,那她就是 alice,还是盒子主人,那我给她管理员视图。”

这个逻辑的问题不在于“不合理”,而在于:

它跳过了所有“为什么我应该相信你”的问题。

在安全系统里,任何跳过“为什么”的设计,都会在后面以事故的形式补回来。

而在家庭服务器这种主人和被邀请家人共用一个盒子的场景下,
“我该不该把 Alice 当成管理员”这个判断,一旦出错,后果不可逆。


2.2 把隐含假设全部摊开

我们先不谈攻击者。
只从逻辑完整性出发,把 ENV 查看器在“信任登录结果”时,隐含接受的假设列出来

  1. 返回的 token 真的是这台盒子上的 懒猫 SSO 发的
  2. token 在传输过程中没有被修改
  3. token 不是别人伪造的
  4. token 是发给 xu.deploy.env 这个 Client 用的,而不是发给盒子上另一个应用
  5. token 还在有效期内
  6. token 对应的是刚才那次登录,而不是别的历史请求
  7. 浏览器没有被篡改
  8. 家庭内网没有被劫持

你会发现一个事实:

流程中,并没有任何一步“天然保证”这些假设成立。

换句话说:

流程跑通 ≠ 假设成立


2.3 为什么“看起来没问题”的系统依然不安全

很多懒猫 App 开发者第一次接触这个问题时,会产生一个非常自然的反应:

“这些假设在家庭内网里几乎总是成立的吧?我家又没有 APT。”

这正是问题的核心。

安全系统设计的第一原则是:

不要基于“通常如此”来建立信任。

尤其在懒猫这种场景下,“通常”其实比你想的更脆弱:

  • 盒子可能被暴露公网(很多人开了 heiyu.space 的公网访问)
  • 盒子上跑着多个应用,它们共享同一个懒猫 SSO
  • 被邀请的家人带进来的设备你无法控制
  • 一些“炫技”级玩法(自签 SSL、自建反代、第三方 OIDC 对接)会破坏默认安全假设

OIDC 的设计,建立在一个非常保守的前提之上:

假设浏览器、网络、中间环境,全部是不可信的。

懒猫 SSO 继承了这个前提,你作为 App 开发者也必须继承。


2.4 Client 真的“什么都没验证”吗?

到这里,你可能会反问一句:

“那 HTTPS 呢?我的 alice.heiyu.space 是懒猫官方给我签的证书,
不是已经很安全了吗?”

这是一个非常关键的问题。

HTTPS 确实解决了很多问题,但它解决的是:

  • 浏览器 ↔ 懒猫 SSO 之间的传输安全
  • 容器 ↔ 懒猫 SSO 之间的传输安全
  • 防止明文被窃听
  • 防止简单的中间人篡改

但 HTTPS 不解决下面这些问题:

  • 返回的 token 是不是给 xu.deploy.env 的,还是给盒子上别的 App 的
  • token 是否来自这台盒子的懒猫 SSO,而不是攻击者在另一台盒子上部署的懒猫 SSO
  • token 是否是旧的、被重放的
  • 浏览器环境是否泄露了中间 code

也就是说:

HTTPS 保护的是“通道”,
懒猫 SSO 必须解决的是“内容”。


2.5 为什么 Client 不能“信懒猫就好了”

另一个常见的直觉是:

“我信任懒猫这个盒子,那懒猫返回的结果我就直接信。”

这句话在逻辑上是不完整的

因为它混淆了两个不同层面的“信任”:

  1. 信任懒猫 SSO 这个身份源
  2. 信任某个具体结果来自这个懒猫 SSO、且适用于当前 App

即使你 100% 信任懒猫 SSO 本身,也不能跳过第二层。

原因很简单:

同一个懒猫 SSO 同时为盒子上所有 App 服务。

如果 ENV 查看器不明确验证:

  • “这是不是发给我(xu.deploy.env)的?”
  • “这是不是 Alice 刚才那次登录的结果?”

那么一个合法但不相关的结果,就可能被错误使用。

比如:另一个 App(哪怕是善意的)拿到了懒猫 SSO 发给它自己的 token,
阴差阳错传给了 ENV 查看器,
ENV 查看器如果不检查 aud,就会用一个不属于自己的 token 完成登录

这不是科幻。在懒猫这种“应用彼此能互联”的架构下,aud 检查是基础操作


2.6 一个非常重要的转折点:

信任不是一个状态,而是一个过程

到目前为止,我们其实已经得到一个非常重要的结论:

“登录成功”不是一个瞬间事件,
而是一个逐步建立的信任过程。

这个过程不是靠“感觉安全”,而是靠一连串明确的验证步骤。

我们可以把它抽象成一句话:

懒猫 SSO 的核心不是“身份声明”,
而是“身份声明的验证链”。

懒猫只是把这条链的起点和工具替你准备好了(注入环境变量、生成 client_secret、暴露 discovery),
链上的每一环都还是你自己负责执行的。


2.7 验证链从哪里开始?

如果你回到第一章的流程,会发现一个关键事实:

在 token 出现之前,
ENV 查看器已经接收过一个“关键输入”。

那就是:

https://env.alice.heiyu.space/callback?code=abc123&state=xyz

这个 code,决定了 ENV 查看器能否拿到后续的一切。

于是,一个新的问题浮现出来:

如果 code 本身被滥用,
后面再严格的 token 校验,还有意义吗?

这不是一个修辞问题。

这是 PKCE 出现的直接原因,也是懒猫 SSO 为什么在 discovery 里明确声明
code_challenge_methods_supported: ["S256", "plain"] 的原因。


2.8 本章的结论(明确而不留余地)

我们可以用几句话,把这一章的核心结论说清楚:

  1. ENV 查看器不能因为“流程跑通”而信任登录结果
  2. 所有信任都必须来自显式验证
  3. 验证必须覆盖流程的每一个关键输入
  4. 验证链必须从 token 出现之前 就开始
  5. “懒猫帮我注入了 CLIENT_SECRET”不是跳过验证的理由

换句话说:

如果你只验证 token,
那你已经晚了一步。


2.9 接下来我们要做什么

在下一章中,我们将回到那个被忽略的问题:

Authorization Code 暴露在浏览器里,
为什么在懒猫场景下还敢用?

我们会看到:

  • code 被拦截在家庭网络里不罕见(家里装了奇怪插件、内网多设备、公网暴露)
  • 纯前端 SPA 型懒猫 App 无法保密任何 secret
  • 懒猫虽然给你注入了 CLIENT_SECRET,但在某些场景下它实际上是 public 的
  • PKCE 如何把一个“谁拿到都能用”的 code,变成“只能被最初那个 Client 使用”

这是 懒猫 SSO 验证链的第一道真正闸门


本章小结(一句话)

懒猫 SSO 的安全性,不来自“我信任懒猫”,
而来自“我验证过这次结果”。

image.png

评论

0

暂无评论

说点什么呢~
收藏
0
0
0