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

忘机山人

发布于27天前
博客图片修整中,看不了可以先搜索公众号“忘机山人”看。
在第一章里,我们刻意做了一件事:
**完整走完了一次“正常的懒猫 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](https://dl.playground.lazycat.cloud/guidelines/459/9886d21d-ab96-45eb-b87b-ccecfbbd8cc9.png "image.png")

评论

0

暂无评论

说点什么呢~
收藏
0
0
0