# ephron.ren 2026-05-16 第三轮完整收口版安全审计 ## 结论摘要 本轮在前两轮基础上继续完成: - 路由与攻击面清单化 - 认证 / 权限 / CSRF 逐路由审计 - 前端注入与渲染链回溯 - 上传 / iframe / raw 渲染专项 - 运行时 / 配置风险复核 ### 当前总体判断 `ephron.ren` **不是“裸奔”状态**,具备以下明确安全基线: - 各服务统一安全响应头在线上已生效 - admin 页面普遍具备服务端鉴权与权限检查 - 大部分 admin 写操作具备 CSRF 防护 - blog 图片上传链具有权限、CSRF、大小限制、Pillow 重编码 WebP - auth 登录跳转链带 `validate_redirect()`,本轮**未确认开放重定向成立** - canvas `/raw/{slug}` 具备专用 `CSP + SAMEORIGIN`,当前**不作为确认漏洞** 但本轮也确认了数个真实安全问题,其中以 `prompt` 与 `home admin` 最值得优先修复。 --- ## 一、确认问题 ## 1. 高危:`prompt /api/test-connection` 可匿名触发服务端外连(SSRF / 网络探测面) ### 风险等级 高危 ### 证据 #### 源码证据 `prompt/src/routes/api.py` - 存在公开 `POST /api/test-connection` - 该接口直接接受客户端传入的 `provider/base_url/api_key/model` - 服务端随后执行: - `POST {payload.base_url}/chat/completions` - 并带上 `Authorization: Bearer {payload.api_key}` - 未见登录校验、权限校验、CSRF 约束 #### 线上验证 非破坏性 POST 验证: - `POST https://prompt.ephron.ren/api/test-connection` - 使用 `base_url=http://127.0.0.1:9` - 线上返回 `200`,正文为:`{"success":false,"error":"All connection attempts failed"}` 这说明: - 接口可匿名访问 - 服务端实际尝试了到指定地址的连接 - 攻击者可以利用该接口探测不同目标的网络可达性/响应差异 ### 影响 - 可被匿名用于对内网/本机/特定外部地址做探测 - 若未来错误回显更详细,可能进一步暴露内部网络结构或上游服务信息 - 若某些环境允许访问元数据地址/内部控制面,风险会进一步升级 ### 修复建议 1. 该接口至少改为 **管理员鉴权**,不要匿名暴露 2. 严格限制 `base_url`: - 白名单域名/白名单 provider - 禁止 `localhost` / `127.0.0.0/8` / RFC1918 内网 / link-local / metadata 地址 3. 后端不要把原始网络错误细节直接返回给前端 4. 增加审计日志与更严格限流 --- ## 2. 中危:`prompt` 调试测试链存在潜在 HTML 注入 / XSS ### 风险等级 中危 ### 证据 #### 源码链路 `prompt/static/js/test-prompt.js` - `event.type === "error"` 时: - `contentDiv.innerHTML =
${event.detail}
` - 普通异常分支也有: - `contentDiv.innerHTML =
${error.message}
` - 流式内容渲染: - `contentDiv.innerHTML = this.renderMarkdown(fullContent)` - `renderMarkdown()` 调用 `marked.parse(text)` `prompt/src/services/llm.py` / `prompt/src/routes/api.py` - 上游错误文本、连接失败信息、响应文本会被包装进错误链并送回前端 ### 风险判断 目前已确认: - 前端把错误详情直接写入 `innerHTML` - 调试链中存在“外部返回内容 / 错误文本 → 前端 DOM”的直接通路 尚未在本轮做破坏性利用,但这已经足够构成**潜在 XSS 风险**,尤其在: - 上游 provider 返回可控 HTML 错误页 - 配置中允许用户指定自定义 `base_url` - 管理员在调试页里测试恶意配置 ### 影响 - 可能形成管理员面自触发 / 存储后触发的 XSS - 结合当前 CSP 中保留 `'unsafe-inline'`,利用门槛被进一步降低 ### 修复建议 1. 错误内容改用 `textContent` 输出,禁止 `innerHTML` 拼接错误文本 2. 若要渲染 markdown,先做服务端/前端 HTML sanitize 3. 后端不要透传原始 `response.text` 或上游 HTML 错误页 4. 若确需展示富文本,使用可信白名单 sanitizer --- ## 3. 中危:`prompt` 登录态写接口存在缺 CSRF 的静态确认问题 ### 风险等级 中危 ### 证据 #### 源码证据 在 `prompt/src/routes/api.py` 中,以下接口使用 `ephron_auth` 登录态校验,但未见 `verify_csrf_token(...)`: - `/api/prompts/{key}/test` - `/api/prompts/{key}/test/save-example` 其中: - `/test` 属于登录态调试触发接口 - `/test/save-example` 明显属于**状态变更接口**,会保存测试样例 ### 线上验证 匿名访问两者均返回 `401 需要登录`,说明它们依赖 cookie 登录态。由于本轮未使用真实登录态做有状态利用,因此把“缺 CSRF”认定为**静态确认缺陷**,而不是已完成线上利用。 ### 影响 - 若管理员/登录用户处于活跃登录态,第三方站点可诱导浏览器跨站发起状态变更请求 - `/test/save-example` 的风险高于纯测试接口,因为它会修改数据 ### 修复建议 1. 对所有 cookie 登录态写接口统一加入 CSRF 校验 2. 建议抽象通用依赖,避免后续再漏 3. 将“登录态 + POST + 改数据”作为测试基线补自动化测试 --- ## 4. 中危:`home` admin 编辑器存在存储型 / 自触发型 DOM XSS ### 风险等级 中危 ### 证据 #### 服务端链路 `home/src/routes/admin.py` - 后端把数据库中的内容直接: - `json.dumps(content, ensure_ascii=False)` - 传入模板变量 `content_json` `home/src/services/content.py` - `save_draft(content, uid)` / `publish_content(content, uid)` - 将传入 JSON 内容**原样存库**,未做字段级 HTML 清洗或输出编码 #### 前端链路 `home/templates/admin/index.html` - `const initialContent = JSON.parse({{ content_json | tojson }});` - 随后大量使用: - `list.innerHTML += ... value="${exp.title}" ...` - `value="${proj.title}"` - `textarea ...>${proj.bullets.join('\n')}` - `value="${skill.category}"` - 即:数据库中的内容被重新拼进 HTML/属性值/textarea 内容 - 未见对引号、标签、事件属性进行转义 ### 风险判断 这意味着: - 只要后台内容中包含特制字符串(如引号、闭合标签、事件属性片段) - 管理员再次打开编辑页时,就可能在其浏览器中落地为 DOM XSS 这更偏向: - **管理员面存储型 XSS** - 或 **自触发型 XSS**(管理员自己保存恶意内容,再次进入编辑器触发) ### 影响 - 劫持管理员会话上下文 - 发起后台操作、窃取页面可见数据 - 若存在其他高权限操作页面,可进一步横向利用 ### 修复建议 1. 禁止把不可信数据直接拼进 `innerHTML` 2. 改为: - `document.createElement(...)` - `input.value = ...` - `textarea.value = ...` - `textContent = ...` 3. 若必须模板化拼接,先做严格 HTML attribute escaping 4. 为 `home admin` 补一组带引号/标签 payload 的自动化回归测试 --- ## 二、已复核但不应误报的点 ## 1. 统一安全响应头:线上已确认存在 线上验证确认以下响应头存在于公开页与 API: - `Content-Security-Policy` - `X-Frame-Options` - `X-Content-Type-Options: nosniff` - `Referrer-Policy: strict-origin-when-cross-origin` 因此“整站缺安全头”**不成立**。 ## 2. `canvas /raw/{slug}` 当前不作为确认漏洞 源码显示: - `X-Frame-Options: SAMEORIGIN` - raw 专用 CSP:`frame-ancestors 'self'` - 用于受限 iframe 预览 虽然它允许 same-origin 预览,理论上仍值得长期关注,但**本轮证据不足以确认越界利用**,不应草率报漏洞。 ## 3. blog Markdown 正文链当前不报直接存储型 XSS `blog/src/utils/markdown.py` 中,Markdown 渲染后经过 `bleach.clean(...)` 白名单净化,因此“博客正文可直接任意 HTML 执行”这一说法在当前证据下**不成立**。 ## 4. blog 图片上传链当前未见明显任意文件上传 已确认: - 权限检查 - CSRF 校验 - 大小限制 - Pillow 解码并重编码为 WebP - 写入固定上传目录 本轮未发现明显的任意脚本上传路径。 ## 5. auth 开放重定向本轮未确认成立 auth 登录成功跳转链调用了 `validate_redirect()`,因此不能把入口上可传 `redirect` 参数直接等同于“已存在开放重定向漏洞”。 --- ## 三、配置风险 / 需持续关注 ## 1. 开发环境默认值过于宽松(配置陷阱) 多个服务存在类似模式: - `IS_DEVELOPMENT = os.getenv("ENVIRONMENT", "development") == "development"` - `docs_url="/docs" if IS_DEVELOPMENT else None` - `error_message = str(exc) if IS_DEVELOPMENT else None` 这意味着: - **如果部署时漏设 `ENVIRONMENT`**,服务会默认按 development 行为启动 - 可能意外暴露 `/docs` - 500 页面可能泄露异常细节 ### 判断 当前这是**配置风险**,不是已确认线上漏洞;但优先级应高,因为它属于“部署一失误就暴露”的类型。 ### 建议 - 默认值改为 production - 生产环境必须显式开启 development,而不是反过来 - 启动时对 `ENVIRONMENT` 做严格枚举校验 --- ## 四、优先修复顺序 ### P0 1. 下线或严格鉴权 `prompt /api/test-connection` 2. 阻断 `home admin` 的 `innerHTML +=` 数据拼接 ### P1 3. 修 `prompt` 调试链的错误输出 / markdown 渲染 XSS 面 4. 给 `prompt` 登录态写接口补 CSRF ### P2 5. 调整所有服务的环境默认值为 production 6. 为以上问题补回归测试 --- ## 五、本轮结论 如果问“现在是否可以说这个项目已经完整安全了”,答案仍然是:**不能**。但和前两轮相比,现在可以更明确地说: - 项目具备基本安全基线 - 不是全面失守 - 但确实存在若干**已确认真实问题**,尤其集中在 `prompt` 和 `home admin` - 这些问题已足以支持继续进入“修复 PR / 回归验证”阶段,而不是只停留在观察层