Files
ephron-ren-prd/prd-canvas-iframe-csp-fix.md

157 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`