Files
ephron-ren-prd/bugs/login-redirect-csp-bug.md

8.7 KiB
Raw Blame History

ephron.ren 登录重定向失败问题分析报告

严重等级: 🔴 Critical
影响范围: 所有需要登录的页面Home / Blog / Canvas / Prompt / Auth
发现日期: 2026-05-05
状态: 已确认复现


1. 问题概述

在 ephron.ren 全站所有页面中,未登录用户点击「登录」后跳转至 auth.ephron.ren/login?redirect=<base64编码的目标URL>,填写账号密码点击登录按钮后,无法重定向回原始页面,始终停留在登录页。

影响的所有入口:

来源页面 登录页 redirect 参数 结果
https://www.ephron.ren/ aHR0cHM6Ly93d3cuZXBocm9uLnJlbi8= 失败
https://blog.ephron.ren/ aHR0cHM6Ly9ibG9nLmVwaHJvbi5yZW4v 失败
https://canvas.ephron.ren/ aHR0cHM6Ly9jYW52YXMuZXBocm9uLnJlbi8= 失败
https://prompt.ephron.ren/ aHR0cHM6Ly9wcm9tcHQuZXBocm9uLnJlbi8= 失败
auth.ephron.ren/admin同源 aHR0cHM6Ly9hdXRoLmVwaHJvbi5yZW4vYWRtaW4= 成功
无 redirect 参数 N/A → /login-success 成功

关键发现: 当 redirect 目标与 auth.ephron.ren 同源时登录正常,跨源(不同子域)时失败。


2. 根因分析

2.1 问题根因

CSP form-action 'self' 策略阻止了登录接口的 303 跨源重定向。

完整流程分析:

① 用户在 www.ephron.ren 点击「登录」
   → 跳转到 https://auth.ephron.ren/login?redirect=aHR0cHM6Ly93d3cuZXBocm9uLnJlbi8=

② 用户填写账号密码,点击「登录」按钮
   → 浏览器 POST 到 https://auth.ephron.ren/api/login同源 ✅ 允许)

③ 服务端验证成功,返回 HTTP 303 重定向
   → Location: https://www.ephron.ren/(跨源 ❌)
   → 响应头包含 Content-Security-Policy: ... form-action 'self'

④ 浏览器检查 CSP form-action 策略
   → 重定向目标 https://www.ephron.ren/ ≠ 'self'https://auth.ephron.ren
   → 浏览器阻止重定向,停留在登录页

⑤ 结果Cookie 已设置ephron_auth但页面未跳转

2.2 技术细节

CSP 配置auth.ephron.ren 所有响应)

Content-Security-Policy: default-src 'self'; 
  script-src 'self' 'unsafe-inline'; 
  script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net; 
  style-src 'self' 'unsafe-inline'; 
  style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://maxcdn.bootstrapcdn.com; 
  img-src 'self' data: https:; 
  font-src 'self' data: https://fonts.gstatic.com https:; 
  connect-src 'self'; 
  frame-ancestors 'none'; 
  base-uri 'self'; 
  form-action 'self'

关键指令: form-action 'self'

CSP form-action 行为说明

根据 CSP Level 2/3 规范,form-action 指令限制的是「表单提交的目标 URL」。在 Chromium 的实现中,表单提交触发的 整个重定向链 都受此策略约束:

  1. 表单 POST 到 /api/loginhttps://auth.ephron.ren)→ 同源,允许
  2. 服务端返回 303 到 https://www.ephron.ren/ 跨源,被阻止

登录接口响应curl 验证)

HTTP/2 303
location: https://www.ephron.ren/
set-cookie: ephron_auth=eyJ2...; Domain=.ephron.ren; HttpOnly; Max-Age=604800; Path=/; SameSite=lax; Secure
content-security-policy: ... form-action 'self'

服务端逻辑完全正确:验证凭证 → 签发 token → 设置 Cookie → 303 重定向。但浏览器因 CSP 策略拒绝执行重定向。

源码定位

  • CSP 配置: shared/security_headers.py 第 8-20 行,_CSP_POLICY 常量
  • 登录接口: auth/src/routes/api.py 第 174-241 行,POST /api/login
  • 重定向校验: auth/src/utils/redirect.py 第 16-75 行,validate_redirect()
  • 登录页面模板: auth/templates/login.html 第 140 行,表单 action

3. 浏览器复现证据

3.1 控制台错误

[ERROR] Sending form data to 'https://auth.ephron.ren/api/login' violates 
the following Content Security Policy directive: "form-action 'self'".

3.2 网络请求分析

请求 状态 说明
GET /login?redirect=... 200 登录页正常加载
POST /api/login 已发送 浏览器确实发出了 POST 请求
303 重定向响应 未跟随 浏览器收到 303 但未执行跳转

登录后 ephron_auth Cookie 已正确设置(Domain=.ephron.ren; HttpOnly; Secure; SameSite=Lax),说明服务端逻辑完全正常。问题纯粹在客户端 CSP 策略。

3.4 对照实验

测试场景 结果 CSP 错误
无 redirect 参数登录 跳转到 /login-success
redirect = auth.ephron.ren/admin(同源) 跳转到 admin
redirect = www.ephron.ren(跨源) 停留登录页 form-action 'self'
redirect = blog.ephron.ren(跨源) 停留 API URL form-action 'self'
redirect = canvas.ephron.ren(跨源) 停留 API URL form-action 'self'
redirect = prompt.ephron.ren(跨源) 停留 API URL form-action 'self'

4. 修复方案

方案 A推荐: 从 303 响应中移除 CSP 头

shared/security_headers.py 的中间件中,对 303 重定向响应不添加 CSP 头:

@app.middleware("http")
async def _security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers.setdefault("X-Content-Type-Options", "nosniff")
    response.headers.setdefault("X-Frame-Options", "DENY")
    response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
    
    # 仅对非重定向响应添加 CSP避免 form-action 阻止跨源重定向)
    if response.status_code not in (301, 302, 303, 307, 308):
        response.headers.setdefault("Content-Security-Policy", _CSP_POLICY)
    
    # ... Cache-Control 逻辑不变

优点: 最小改动,不影响其他页面的 CSP 保护
缺点: 重定向响应失去 CSP 保护(但重定向响应通常无 HTML 内容CSP 保护意义不大)

方案 B: 修改 form-action 策略

form-action 'self' 改为允许所有 ephron.ren 子域:

form-action 'self' https://*.ephron.ren

或更精确地列出所有子域:

form-action 'self' https://www.ephron.ren https://blog.ephron.ren https://canvas.ephron.ren https://prompt.ephron.ren

优点: 明确允许列表,安全性可审计
缺点: 新增子域需同步更新 CSP通配符 * 可能过于宽松

方案 C: 改用 JavaScript 重定向

修改登录接口,返回 200 + JSON由前端 JS 执行 window.location.href 跳转:

// 前端
const res = await fetch('/api/login', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
    window.location.href = data.redirect_url;
}

优点: 完全绕开 CSP form-action 限制
缺点: 需要修改前后端逻辑JS 禁用时无法登录(渐进增强降级)

方案 D: 使用中间页面中转

登录成功后先重定向到 auth.ephron.ren/redirect?url=<target>(同源),再由该页面通过 meta refresh 或 JS 跳转到目标:

<!-- auth.ephron.ren/redirect 页面 -->
<meta http-equiv="refresh" content="0;url=https://www.ephron.ren/">
<script>window.location.href = decodeURIComponent(params.get('url'));</script>

优点: 保持 form-action 'self' 不变
缺点: 多一次跳转,增加延迟;需要额外的路由和页面


5. 推荐修复路径

推荐方案 A,理由:

  1. 改动最小: 仅修改 security_headers.py 中间件的 1 行逻辑
  2. 安全性可接受: 303 重定向响应体为空(content-length: 0CSP 对其无实际保护作用
  3. 不影响其他安全头: X-Content-Type-OptionsX-Frame-OptionsReferrer-Policy 仍正常添加
  4. 无需修改前端: 登录流程保持 HTML 表单提交,不依赖 JavaScript

6. 附录

6.1 受影响的完整页面列表

所有跳转到 auth.ephron.ren/login?redirect=... 的页面均受影响:

  • www.ephron.ren — 首页
  • blog.ephron.ren — 博客(含文章详情页、管理后台)
  • canvas.ephron.ren — 画布
  • prompt.ephron.ren — 提示词(含详情页、管理后台)
  • auth.ephron.ren — 注册成功后跳转、管理后台

6.2 注册表单同样受影响

注册页面 (auth.ephron.ren/register) 使用相同的 CSP 策略和表单提交模式,注册成功后的 303 重定向也可能受 form-action 'self' 影响。需一并验证和修复。

6.3 测试环境

  • 浏览器: Chromium (Playwright headless)
  • 测试账号: Elaina_user / Elaina_owner
  • 测试时间: 2026-05-05 22:00-22:30 CST