feat(sso): MV-1 MyVote SSO 通道(cos72 半)— 3 轮安全审闭环#18
Merged
Conversation
新增 aastar/src/sso 模块(MV-1 的 cos72 半,KMS signTypedData 由 E-5 另行交付):
- POST /sso/authorize(JWT 保护):为当前登录用户签发一次性 code(32B hex,
TTL 60s,单次消费),绑定 { userId, aaAddress };redirect_uri 走
SSO_ALLOWED_REDIRECTS 白名单(origin 解析比对 + 前缀匹配,空=拒绝一切,
fail-closed)
- POST /sso/exchange(公开):code 换短时 SSO session token — 独立
SSO_JWT_SECRET(缺失则启动即 fail fast)+ audience "myvote" + TTL 10min,
payload { sub, aa };get+delete 同步连续操作,单线程 Node 下天然原子
- GET /sso/verify(公开):Bearer 校验 secret/audience/过期,返回
{ valid, aaAddress },坏 token 不抛 401(供 MyVote 后端/edge 轮询)
- exchange/verify 挂 per-IP 滑窗限流 20 req/min(仿 otp-rate-limit.guard,
IP 键;单实例内存实现,多副本需换 Redis——code 存储同)
- CORS 不动:main.ts 已是 origin:true 宽松配置,无需并入 SSO 白名单
- 单测 21 例:单次消费/过期/白名单 fail-closed(含 lookalike origin)/
audience/secret/过期 token/限流(IP、bucket 隔离与滑窗释放)
Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
- M1 code 绑 redirect_uri:authorize 把规范化(URL.toString)redirect_uri 存进 SsoCodeEntry;exchange DTO 新增 redirect_uri,兑换时与存值精确匹配,不匹配 401 且照样烧掉 code(OAuth code↔redirect 绑定) - M2 path 前缀绕过:白名单匹配改为 URL 解析后 origin 精确相等 + pathname 边界 匹配(等于白名单 path 或以 path+'/' 开头;白名单 path 尾斜杠规范化)—— /sso/callback 不再放过 /sso/callback-evil - M3 secret 隔离强制:SsoService 构造时校验 SSO_JWT_SECRET != JWT_SECRET,相等 即 fail fast;JwtStrategy 采用 audience 排斥方案(拒绝 aud 含 "myvote" 的 token 走 session 路径,存量无 aud 的 session token 不受影响),常量抽到 sso.constants.ts 避免 auth→sso 重依赖链循环 - L1 算法固定:SSO 签发 algorithm HS256、verify algorithms ['HS256']; JwtStrategy 同样 pin HS256 - L2 错误 oracle:exchange 的不存在/已用/过期/redirect 不匹配统一为 401 'Invalid SSO code' - 单测 21→33:redirect 不匹配拒绝+烧码、URL 规范化等价匹配、callback-evil 拒/ callback/sub 放/精确放/尾斜杠规范化、secret 相等 fail fast、JwtStrategy aud 排斥×2+存量放行、HS512 拒绝、三态错误一致性 Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
Codex 终审剩余 Medium:WHATWG URL 的 pathname 不解码 %2f/%5c/%2e,于是 /sso/callback/%2f..%2fevil 能满足 startsWith(entryPath + "/") 通过白名单; 一旦 MyVote 或其前置代理解码并归一化路径,就逃逸到 /sso/evil。 - resolveRedirect 在 origin/path 边界匹配之前,先以 /%2f|%5c|%2e/i 大小写不敏感 拒绝 path 含编码分隔符/点段的 redirect_uri(fail-closed,合法 callback 不需要) - path 比较保持大小写敏感(URL path 本就区分大小写)+ 双方同一 canonical 化 (去尾斜杠)后做 segment 边界匹配 —— 现有实现即如此,保持不变 - 注释补运营建议:生产 SSO_ALLOWED_REDIRECTS 应配精确 callback path,不要配裸 origin(裸 origin 放行该 origin 任意 path) - 单测 33→38:%2f 拒、%2F 大写拒、%5c 拒、%2e%2e 拒、正常 /sso/callback/sub 仍放行 Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
clestons
approved these changes
Jul 15, 2026
clestons
left a comment
There was a problem hiding this comment.
APPROVE — 这是我最近审到的最扎实的一段鉴权后端。45/45 测试通过(独立跑通,含 JwtStrategy 的 SSO-audience 排除)。
按攻击面逐条核对:
- Token 域隔离(双保险) — 独立
SSO_JWT_SECRET,未设即 boot 失败,== JWT_SECRET 也 boot 失败(密码学不可互换);再加JwtStrategy对aud==myvote主动排除(scalar + array 都覆盖),即使密钥配错也拦得住。verify 里algorithms:["HS256"]钉死算法 —— 不信任 token header 的 alg,堵掉 alg-confusion / none。 - 一次性 code — 32 字节随机;
get+delete之间无 await,Node 单线程下原子,并发也不可能兑换两次;失败尝试也无条件销毁 code(过期/redirect 不匹配都烧掉),符合 OAuth 语义。code 绑定{userId, aaAddress, redirectUri},exchange 必须出示完全一致的 redirect —— 偷到 code 也没法换个 redirect 兑现。 - redirect 校验(resolveRedirect) — 双方都 URL 解析、绝不裸字符串前缀:origin 全等(挡掉
myvote.example.com.evil.com)、path 按段边界匹配(挡掉/sso/callback-evil)、拒绝编码分隔符%2f/%5c/%2e(挡掉下游解码后的路径穿越)、空白名单 fail-closed。 - Oracle 抗性 — exchange 所有失败统一
INVALID_SSO_CODE,分不出「过期」还是「从没存在」;verify 设计成非抛出。 - 内存 DoS 有界 — code 10k 上限 + 驱逐,rate-limit key 50k 上限 + 驱逐,sweeper 都
unref。 - 限流接入正确 — authorize=JwtAuthGuard,exchange/verify=SsoRateLimit。且诚实标注了
trust proxy未配时所有客户端共享代理 IP(fail-closed:限太多不会限太少)。
多副本部署时那两处 in-memory store(code + 限流)要挪到 Redis —— 注释已明确写了,当前单实例正确。无阻塞。
— clestons (Opus independent review, tool-verified: 源码通读 + 45 测试跑通)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/dev/stdin