Skip to content

feat(mv7): cos72 SSO 授权发起页 + 治理入口 — 开放重定向 3 轮修复(叠加 #18)#19

Merged
jhfnetboy merged 3 commits into
masterfrom
feat/mv7-sso-start-page
Jul 15, 2026
Merged

feat(mv7): cos72 SSO 授权发起页 + 治理入口 — 开放重定向 3 轮修复(叠加 #18)#19
jhfnetboy merged 3 commits into
masterfrom
feat/mv7-sso-start-page

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

/dev/stdin

@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner July 14, 2026 11:31

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVEsafeReturnPath 是教科书级的开放重定向防御,我对着它试了一圈没能绕过。5/5 测试通过(node:test 跑通)。

叠在 #18 之上。核心是 ?redirect= 回跳路径的同源守卫,分层防御:

  1. 拒绝一切 C0 控制符/空格/反斜杠 —— 正是 URL parser 会 strip/fold 成 authority 的走私字母表;
  2. 必须单个前导 /(拒 // 协议相对),且拒绝编码分隔符 %2f/%5c;
  3. 权威判定:用 WHATWG URL parser 解析后 url.origin === origin,返回的是 parser 归一化后的 pathname+search+hash,原始输入永远不到 sink;
  4. (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)

@jhfnetboy jhfnetboy changed the base branch from feat/mv1-sso-endpoints to master July 15, 2026 03:54
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
@jhfnetboy jhfnetboy force-pushed the feat/mv7-sso-start-page branch from 23b2d64 to 07885db Compare July 15, 2026 03:55
@jhfnetboy jhfnetboy merged commit 8bd79e0 into master Jul 15, 2026
1 check passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants