4.6 KiB
4.6 KiB
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 中定义了全局安全头策略:
_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 函数中,返回响应时覆盖安全头:
@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 中添加例外路径:
"/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,原因:
- 改动范围最小,只修改 Canvas 服务的一个路由
- 最精准,只影响
/raw/{slug}路径 - 不影响其他服务的安全策略
- 风险最低
验证方法
修复后验证:
- 访问
https://canvas.ephron.ren/view/hermes-agent-ai - 检查 iframe 是否正常加载内容
- 检查浏览器控制台是否有 CSP 错误
- 验证其他页面(首页、管理页)是否正常
# 检查响应头
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 服务入口(安装安全头中间件)
标签
bugsecuritycanvasiframecsp