init: consolidate all ephron.ren PRDs and docs
This commit is contained in:
239
bugs/login-redirect-csp-bug.md
Normal file
239
bugs/login-redirect-csp-bug.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# 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/login`(`https://auth.ephron.ren`)→ ✅ 同源,允许
|
||||
2. 服务端返回 303 到 `https://www.ephron.ren/` → ❌ 跨源,被阻止
|
||||
|
||||
#### 登录接口响应(curl 验证)
|
||||
|
||||
```http
|
||||
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 但未执行跳转 |
|
||||
|
||||
### 3.3 Cookie 状态
|
||||
|
||||
登录后 `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 头:
|
||||
|
||||
```python
|
||||
@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` 跳转:
|
||||
|
||||
```javascript
|
||||
// 前端
|
||||
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 跳转到目标:
|
||||
|
||||
```html
|
||||
<!-- 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: 0`),CSP 对其无实际保护作用
|
||||
3. **不影响其他安全头**: `X-Content-Type-Options`、`X-Frame-Options`、`Referrer-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
|
||||
Reference in New Issue
Block a user