init: consolidate all ephron.ren PRDs and docs
This commit is contained in:
156
prd-canvas-iframe-csp-fix.md
Normal file
156
prd-canvas-iframe-csp-fix.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# PRD: Canvas iframe 嵌入被安全策略阻止
|
||||
|
||||
## 问题描述
|
||||
|
||||
Canvas 服务的 `/view/{slug}` 页面通过 iframe 嵌入 `/raw/{slug}` 来展示 Canvas 内容。但由于共享安全头中间件设置了以下策略,浏览器会阻止 iframe 加载:
|
||||
|
||||
- `X-Frame-Options: DENY` — 禁止被任何页面 iframe 嵌入
|
||||
- `frame-ancestors 'none'` — CSP 禁止被任何页面嵌入
|
||||
|
||||
**结果:** 用户访问 `canvas.ephron.ren/view/hermes-agent-ai` 时,iframe 区域显示空白或"拒绝连接"。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 所有 Canvas 页面的预览功能完全失效
|
||||
- 首页卡片的缩略图预览也无法加载
|
||||
|
||||
## 根因分析
|
||||
|
||||
`shared/security_headers.py` 中定义了全局安全头策略:
|
||||
|
||||
```python
|
||||
_CSP_POLICY = (
|
||||
...
|
||||
"frame-ancestors 'none'; " # 第17行
|
||||
...
|
||||
)
|
||||
|
||||
# 第39行
|
||||
response.headers.setdefault("X-Frame-Options", "DENY")
|
||||
```
|
||||
|
||||
这个策略被所有 ephron.ren 服务共享(Auth、Blog、Canvas、Prompt),但 Canvas 服务的 `/raw/{slug}` 路由需要被同源 iframe 嵌入。
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 方案A:修改 `/raw/{slug}` 路由(推荐)
|
||||
|
||||
在 `canvas/src/routes/pages.py` 的 `raw_canvas` 函数中,返回响应时覆盖安全头:
|
||||
|
||||
```python
|
||||
@router.get("/raw/{slug}", response_class=HTMLResponse)
|
||||
async def raw_canvas(
|
||||
slug: str,
|
||||
ephron_auth: str | None = Cookie(default=None),
|
||||
):
|
||||
# ... 现有代码 ...
|
||||
|
||||
# Build CSP that allows same-origin iframe embedding
|
||||
raw_csp = (
|
||||
"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 'self'; " # Allow same-origin embedding
|
||||
"base-uri 'self'; "
|
||||
"form-action 'self' https://*.ephron.ren"
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=canvas.content_html,
|
||||
media_type="text/html; charset=utf-8",
|
||||
headers={
|
||||
"X-Frame-Options": "SAMEORIGIN", # Override DENY for iframe embedding
|
||||
"Content-Security-Policy": raw_csp,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 最精准,只影响需要被嵌入的路径
|
||||
- 不影响其他服务的安全策略
|
||||
- 改动范围最小
|
||||
|
||||
**缺点:**
|
||||
- 需要在路由层面重复 CSP 策略
|
||||
|
||||
### 方案B:修改共享中间件,添加路径例外
|
||||
|
||||
在 `shared/security_headers.py` 中添加例外路径:
|
||||
|
||||
```python_EXEMPT_PATHS = frozenset({
|
||||
"/raw/", # Canvas raw content needs iframe embedding
|
||||
})
|
||||
|
||||
@app.middleware("http")
|
||||
async def _security_headers(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||
|
||||
# Check if path is exempt from DENY policy
|
||||
path = request.url.path
|
||||
if any(path.startswith(p) for p in _EXEMPT_PATHS):
|
||||
response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
|
||||
# Use modified CSP with frame-ancestors 'self'
|
||||
csp = _CSP_POLICY.replace("frame-ancestors 'none'", "frame-ancestors 'self'")
|
||||
response.headers.setdefault("Content-Security-Policy", csp)
|
||||
else:
|
||||
response.headers.setdefault("X-Frame-Options", "DENY")
|
||||
response.headers.setdefault("Content-Security-Policy", _CSP_POLICY)
|
||||
|
||||
# ... 其他代码 ...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 集中管理,易于维护
|
||||
- 其他服务如果需要 iframe 嵌入也能受益
|
||||
|
||||
**缺点:**
|
||||
- 修改共享代码,影响所有服务
|
||||
- 需要更仔细的测试
|
||||
|
||||
## 推荐方案
|
||||
|
||||
**推荐方案A**,原因:
|
||||
1. 改动范围最小,只修改 Canvas 服务的一个路由
|
||||
2. 最精准,只影响 `/raw/{slug}` 路径
|
||||
3. 不影响其他服务的安全策略
|
||||
4. 风险最低
|
||||
|
||||
## 验证方法
|
||||
|
||||
修复后验证:
|
||||
|
||||
1. 访问 `https://canvas.ephron.ren/view/hermes-agent-ai`
|
||||
2. 检查 iframe 是否正常加载内容
|
||||
3. 检查浏览器控制台是否有 CSP 错误
|
||||
4. 验证其他页面(首页、管理页)是否正常
|
||||
|
||||
```bash
|
||||
# 检查响应头
|
||||
curl -sI "https://canvas.ephron.ren/raw/hermes-agent-ai" | grep -E "x-frame-options|content-security-policy"
|
||||
```
|
||||
|
||||
期望输出:
|
||||
```
|
||||
x-frame-options: SAMEORIGIN
|
||||
content-security-policy: ... frame-ancestors 'self'; ...
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `shared/security_headers.py` — 共享安全头中间件
|
||||
- `canvas/src/routes/pages.py` — Canvas 页面路由(`raw_canvas` 函数)
|
||||
- `canvas/src/main.py` — Canvas 服务入口(安装安全头中间件)
|
||||
|
||||
## 标签
|
||||
|
||||
- `bug`
|
||||
- `security`
|
||||
- `canvas`
|
||||
- `iframe`
|
||||
- `csp`
|
||||
Reference in New Issue
Block a user