feat(mv7): cos72 SSO 授权发起页 + 治理入口 — 开放重定向 3 轮修复(叠加 #18)#19
Merged
Conversation
clestons
approved these changes
Jul 15, 2026
clestons
left a comment
There was a problem hiding this comment.
APPROVE — safeReturnPath 是教科书级的开放重定向防御,我对着它试了一圈没能绕过。5/5 测试通过(node:test 跑通)。
叠在 #18 之上。核心是 ?redirect= 回跳路径的同源守卫,分层防御:
- 拒绝一切 C0 控制符/空格/反斜杠 —— 正是 URL parser 会 strip/fold 成 authority 的走私字母表;
- 必须单个前导
/(拒//协议相对),且拒绝编码分隔符%2f/%5c; - 权威判定:用 WHATWG URL parser 解析后
url.origin === origin,返回的是 parser 归一化后的pathname+search+hash,原始输入永远不到 sink; - (3b) 对输出再判一次 —— 这一步最见功力:
/%2e%2e//evil.com(%2e%2e=..)经 dot-segment 折叠后 pathname 变成//evil.com,而url.origin仍等于 base(host 藏在 path 里,step 3 会漏),靠对归一化 OUTPUT 复查startsWith("//")兜住。绝大多数实现都会漏这一层。
接入也对:login 页 safeReturnPath(get("redirect")) → returnTo(不安全则回落 DEFAULT_LANDING)→ router.push sink;sso/start 页把 code 拼到后端回显的、已按白名单归一化的 redirectUri 上,绝不用原始 query 值,还留了 badRedirect 兜「不该发生」的分支。前后端在编码分隔符上采取同一套 fail-closed 立场。无阻塞。
— clestons (Opus independent review, tool-verified)
MyVote SSO 真机跑通的 cos72 侧最后一块: - app/sso/start:过场页。读 ?redirect_uri → 未登录则跳登录并带回跳参数 → 已登录用 cos72 JWT 调 POST /sso/authorize → 成功后 location.replace 到 「后端回显的 redirectUri」+ code(用 URL/searchParams 拼接,自动处理 ?/&)。 防开放重定向:前端不做白名单,只跳后端回显值(后端已 fail-closed 校验)。 失败不静默:400 白名单拒绝 / 400 无 AirAccount / 401 / 网络 各自独立错误页, 白名单文案直接点名 SSO_ALLOWED_REDIRECTS 并给运维处置指引。 - auth/login:新增 ?redirect= 登录后回跳(仅允许同源路径,拒绝 //host 与 /\host, 防开放重定向),passkey 与邮箱验证码两条路径均生效。 - lib/api:ssoAPI.authorize + skipAuthRedirect 请求标记 —— SSO 授权的 401 不再被 全局拦截器吞成裸 /auth/login 跳转,由页面自行处理以保住 redirect_uri 上下文。 - CommunityNav:NEXT_PUBLIC_MYVOTE_URL 配置后,MyVote tab 变为「MyVote · 治理」 外链入口(同标签页,便于 SSO 回跳);未配置则维持既有「即将上线」占位。 - i18n:新增 ssoStart 命名空间,EN/ZH 双语,i18n:check 878 keys 对齐。 Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
Codex 审出 Critical:safeReturnPath 旧实现仅查 startsWith("/") + 第二字符,
`?redirect=/%09//evil.com`(URLSearchParams 解码为 /\t//evil.com)与
`/%0d%0a//evil.com` 可绕过——WHATWG URL 解析剥掉 tab/CR/LF 后 //evil.com 被当作
protocol-relative authority 归一为 https://evil.com/,登录成功 router.push 跳站外钓鱼。
修复(抽为 lib/safe-return-path.ts 纯函数,分层防御):
1. 拒绝所有 C0 控制字符/空格(codepoint<=0x20)与反斜杠——正是 URL 解析器会剥离/
折叠成 authority 的字符集;
2. 必须单个 / 开头(拒 //),并拒绝百分号编码的路径分隔符 %2f/%5c(对齐后端
resolveRedirect 的 ENCODED_PATH_SEPARATOR,防下游解码器把 /%2f%2fevil 折成 //evil);
3. 权威判定:new URL(raw, origin) 强制 url.origin===origin,返回解析器规范化的
pathname+search+hash(绝不回传原始输入)。
测试(lib/safe-return-path.test.ts,node:test 内建 runner + ts-node,零新依赖;
新增 npm script test:unit;测试文件从 tsconfig 排除以免 bundler 解析 .ts 扩展名):
拒 //evil.com、/\evil.com、/%2f%2fevil.com、%2F%2Fevil.com、/%09//evil.com(→/\t//)、
/%0d%0a//evil.com(→/\r\n//)、https://evil.com、look-alike host、前导空格;
放行 /dashboard、/sso/start?redirect_uri=x、/proposal/0xabc#frag。4 组全绿。
另:
- sso/start 注明 SSO code referrer 清理责任边界(cos72 侧 location.replace 已正确,
清 callback URL 的 code 属 MyVote #4 的 history.replaceState 职责)。
- CommunityNav 外链注明「同页跳转 + rel=noopener 是刻意安全例外」。
Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
new URL() 折叠 `..` 后 pathname 可变成 //evil.com(同源检查仍过), 对规范化后的最终结果二次校验单 / 开头。补 dot-segment 测试; 双重编码 %252e 经核验非真绕过(解析器不解 %25、router 不二次解码),放行为同源路径。 Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
23b2d64 to
07885db
Compare
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