# 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`