忘机山人
https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env 在第二章的结尾,我们刻意停在了一个被大多数流程图忽略的位置: ``` https://env.alice.heiyu.space/callback?code=abc123 ``` 这是懒猫 Authorization Code Flow 中一个看起来非常不起眼的瞬间。 浏览器回跳,URL 里多了一个参数。 ENV 查看器继续往下走,拿 `code` 换 token。 但如果你站在 ENV 查看器的立场仔细想一想,就会发现一个问题几乎是 unavoidable 的: > **这个 code 是暴露在浏览器环境里的。 > 那它为什么还敢被当作“凭证”使用?** 这一章,我们只做一件事: > **证明:即使在“懒猫自动注入 CLIENT_SECRET”这种看似有密的场景下,没有 PKCE,Authorization Code Flow 在家庭服务器环境里也并不安全。** --- ## 3.1 Authorization Code 到底是什么 在开始谈 PKCE 之前,我们必须先明确一件事: > **Authorization Code 本身不是 token,也不是身份声明。** 它只是一个**中间凭证**,用来完成两件事之间的衔接: * 浏览器参与的授权阶段(`/sys/oauth/auth`) * 容器与懒猫 SSO 直接通信的 token 交换阶段(`/sys/oauth/token`) 在理想设计中,它应该满足三个特性: 1. 短期有效(懒猫 SSO 默认就是几十秒) 2. 只能使用一次(用过即作废,重试会拿到 `invalid_grant`) 3. **只能被特定 Client 使用** 前两个特性,单靠懒猫 SSO 就能保证。 第三个特性,问题就来了。 --- ## 3.2 一个经常被低估的事实:家里的浏览器也是不可信的 在 Authorization Code Flow 中,**code 一定会经过浏览器**。 这不是实现问题,而是协议设计的必然结果。 而浏览器环境在懒猫这种场景下意味着什么? * URL 会被浏览器历史记录、同步到云端 * 如果你在盒子前挂了自建 nginx / openresty,**访问日志里会留下 code** * JavaScript 可以读地址栏(跨 App 的第三方脚本如果被加载进来,能读到) * 浏览器插件可以读取页面内容 * Referer 可能被发送到第三方 * 家人用的浏览器你不可能审计 * 盒子开了公网访问后,任何经过的中间设备都有可能留下痕迹 换句话说: > **只要 code 出现在 URL 里,就默认处在“可能被窃取”的环境中。** 家庭内网不是纯净室,懒猫官方做了很多努力(证书、加密、子域隔离), 但**不能**替你解决“code 被读到之后怎么办”。 如果你此刻的直觉是: > “那 code 被偷了是不是就完了?” 恭喜你,你已经走到 PKCE 之前所有安全设计者走到的那个位置。 --- ## 3.3 没有 PKCE 会发生什么(不是假设,是攻击模型) 我们来构造一个完全现实的攻击场景,不引入任何极端条件。 ### 场景 * 盒子主人 Alice 装了一个稍微不那么干净的浏览器扩展(它请求了 `` 权限) * ENV 查看器使用 Authorization Code Flow * 假设**没有 PKCE** ### 正常流程 1. Alice 在 Grant Access 页点同意 2. 浏览器回跳: ``` https://env.alice.heiyu.space/callback?code=abc123&state=xyz ``` 3. ENV 查看器后端用 `abc123` 换 token ### 攻击者(那个扩展)只需要做一件事 > **在第 2 步,读到这个 code** 扩展不需要: * 窃取密码 * 破解加密 * 控制懒猫 SSO 它只需要获得: ``` abc123 ``` 然后把这个 code 连同它偷到的 `CLIENT_SECRET`(如果 App 是纯 SPA,secret 也在前端代码里), 自己向懒猫 SSO 发起: ``` POST /sys/oauth/token code=abc123 client_id=xu.deploy.env client_secret=... ``` 如果没有 PKCE,**懒猫 SSO 没有任何技术手段区分**: * 这是 ENV 查看器的后端 * 还是一个拿着同样 client 凭证的攻击者 于是,一个结论非常残酷: > **在没有 PKCE 的情况下,懒猫 Authorization Code 本质上是“谁拿到 code + client 凭证,谁能用”。** --- ## 3.4 为什么 `LAZYCAT_AUTH_OIDC_CLIENT_SECRET` 解决不了这个问题 到这里,很多懒猫 App 开发者会下意识想到: > “我在 manifest 里读 `LAZYCAT_AUTH_OIDC_CLIENT_SECRET` 了啊,应该算 Confidential Client 吧?” 这是一个**只在后端场景成立的直觉**。 ### 对有后端容器的懒猫 App * `CLIENT_SECRET` 通过环境变量注入到容器 * 换 token 的 POST 发生在容器内,不经过浏览器 * 攻击者拿不到 secret 👉 这种情况下,secret 确实有意义。 ### 对纯前端 SPA 型懒猫 App * `CLIENT_SECRET` 要么在构建期打进前端 bundle * 要么在运行期从某个 `/config` 接口传给前端 * 等价于公开 * 攻击者同样可以使用 于是,对于纯前端 App: > **`CLIENT_SECRET` 在安全意义上等于不存在。** 即使是有后端的 App,只要你的 `CLIENT_SECRET` 不小心被日志打出来(懒猫玩家经常开 debug)、或者写到了可被别的容器读到的文件里,它的保密性就已经被打穿。 这正是 PKCE 存在的历史背景,也是它在懒猫 SSO 里**不能省略**的直接原因。 --- ## 3.5 PKCE 的核心思想(不是参数,而是绑定) PKCE(Proof Key for Code Exchange)不是“多加一个校验字段”。 它解决的是一个非常明确的问题: > **如何证明: > “来换 token 的这个请求, > 就是当初发起授权的那个 Client 实例?”** 注意这里的关键词是: * 不是“哪个用户”(那是 Alice 的事) * 不是“哪个 IdP”(那是懒猫 SSO 的事) * 也不是“哪个应用包名”(那是 `client_id` 的事) * 而是:**哪个正在运行的 Client 实例** 即使两个人都在用 ENV 查看器,他们是**两个独立的 Client 实例**,PKCE 让他们彼此的 code 也不可互换。 --- ## 3.6 PKCE 在懒猫上的完整流程(一步都不能省) 现在我们重新走一遍懒猫 Authorization Code Flow,但这次加上 PKCE。 懒猫 SSO discovery 里明确支持: ```json "code_challenge_methods_supported": ["S256", "plain"] ``` **必须用 `S256`**,`plain` 只是兼容古董 App,新项目不要用。 --- ### Step 1:Client 本地生成一个秘密 在发起授权之前,ENV 查看器先在本地生成一个高熵随机值: ``` code_verifier = random(43~128 字符, [A-Z a-z 0-9 - . _ ~]) ``` 这个值有两个重要特性: 1. 从不发送给懒猫 SSO(此时) 2. 从不出现在浏览器 URL 中 它是真正的“秘密”,存在服务端 session 或安全的客户端存储里。 --- ### Step 2:Client 派生一个公开承诺 ENV 查看器对这个 secret 做一次变换: ``` code_challenge = BASE64URL(SHA256(code_verifier)) ``` 这个 challenge: * 可以公开 * 不能反推 verifier * 与 verifier 有确定性数学关系 这一步,本质上是一个 **commitment**。 --- ### Step 3:授权请求中只发送 challenge 浏览器跳转到懒猫 SSO 时,参数变成了: ``` GET https://alice.heiyu.space/sys/oauth/auth? response_type=code &client_id=xu.deploy.env &redirect_uri=https%3A%2F%2Fenv.alice.heiyu.space%2Fcallback &scope=openid%20email%20profile &state=... &nonce=... &code_challenge=yyy &code_challenge_method=S256 ``` 注意: * 浏览器里只有 `code_challenge` * `code_verifier` 仍然只存在于 ENV 查看器本地(session / 后端内存) --- ### Step 4:懒猫 SSO 将 code 与 challenge 绑定懒猫 SSO 在生成 Authorization Code 时,会记录: ``` authorization_code: value = abc123 client_id = xu.deploy.env bound_challenge = yyy bound_method = S256 ``` 从这一刻起,这个 code 就不再是“自由凭证”。 --- ### Step 5:换 token 时提交 verifier(关键验证) ENV 查看器请求 token 时,除了原本的参数,再带一个 `code_verifier`: ```http POST https://alice.heiyu.space/sys/oauth/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(xu.deploy.env:) grant_type=authorization_code &code=abc123 &redirect_uri=https%3A%2F%2Fenv.alice.heiyu.space%2Fcallback &code_verifier=zzz ``` 懒猫 SSO 执行验证: ``` SHA256(code_verifier) == bound_challenge ? ``` * ❌ 不相等 → 直接 `invalid_grant` * ✅ 相等 → 继续发 token --- ## 3.7 PKCE 实际验证的是什么 我们用一句话把 PKCE 的验证逻辑说清楚: > **PKCE 验证的不是“你是谁”, > 而是“你是不是当初那个发起授权的人”。** 它解决的是: * code 被偷 * code 被重放 * code 被跨 Client 使用 * 两个同名 App 实例之间的 code 串用 而它**不解决**: * token 是否伪造(那是 JWKS + 签名的活) * token 是否过期 * token 是否发给正确的 Client(那是 `aud` 的活) 这些问题,会在后面的章节里由 JWT 签名和 ID Token Validation 解决。 --- ## 3.8 为什么 PKCE 对懒猫 App 是“必须”,不是“增强” 在懒猫 SSO 的实践中,可以明确给出一个结论: > **在懒猫场景下, > 没有 PKCE 的 Authorization Code Flow 是不安全的。** 这不是建议,而是逻辑必然。 因为: * 盒子可能暴露公网,`heiyu.space` 子域对全世界可见 * 同一个盒子上会跑很多 App,共享同一个懒猫 SSO * 家人的设备、家里的浏览器你不可能都信得过 * 部分 App 是纯前端 SPA,`CLIENT_SECRET` 等于裸奔 * Authorization Code 一定会暴露在 URL 里 PKCE 是唯一能够在这些前提下**恢复 Client 绑定能力**的机制。 懒猫 SSO 支持 `S256`,你只要用就对了。**所有主流 OIDC 客户端库默认都打开 PKCE,不要主动关掉。** --- ## 3.9 PKCE 在验证链中的位置 现在我们可以把 PKCE 放回到整条懒猫 SSO 验证链中: | 阶段 | 验证目标 | | ----------- | --------------------------------------- | | PKCE | Authorization Code 是否被原始 Client 使用 | | JWT 验签 | token 是否由这台盒子的懒猫 SSO 签发 | | iss / aud | token 是否适用于当前 App(`xu.deploy.env`) | | exp / nonce | token 是否仍然有效、是否对应这次登录 | | scope / groups | token 能做什么、用户是不是盒子主人 | 你会发现一个重要事实: > **PKCE 是懒猫 SSO 验证链中最早的一道闸门。** 如果这一关被绕过,后面所有校验都失去意义。 ---  ## 3.10 本章结论(明确且不可妥协) 我们可以用几句话结束这一章: 1. Authorization Code 一定会暴露在浏览器环境,懒猫也不例外 2. 家庭网络 + NAS暴露公网,是现实中的常见组合 3. 没有 PKCE,code 等价于“谁拿到谁能用” 4. 懒猫注入的 `CLIENT_SECRET` 无法单独替代 PKCE 5. PKCE 把 code 与 Client 实例进行了数学绑定 6. 因此,PKCE 不是增强,而是懒猫 SSO 的 **成立条件** --- ### 本章小结(一句话) > **PKCE 的存在,说明懒猫 SSO 从一开始就假设: > 浏览器和家庭网络,永远不值得无条件信任。**
评论
0暂无评论