Files
ephron-ren-prd/2026-05-16-ephron-security-audit-round2.md

9.2 KiB
Raw Permalink Blame History

ephron.ren 2026-05-16 全面安全审计(第二轮)

审计范围

本轮针对 ephron.ren 多服务单仓架构进行源码静态审计 + 线上非破坏性动态验证,覆盖:

  • auth
  • blog
  • canvas
  • home
  • prompt
  • shared

审计目标:

  1. 识别匿名可利用的高风险接口
  2. 核查 service token / admin / public 权限边界
  3. 识别 CSRF / XSS / SSRF / 重定向 / 信息泄露类问题
  4. 为修复提供可落地建议与优先级

结论摘要

本轮确认 3 个需要处理的问题,其中:

  • 高危 1 个prompt 存在匿名可利用的 SSRF / 外连探测接口
  • 中危 1 个prompt 调试流错误链存在潜在 HTML 注入 / XSS 风险
  • 中低危 1 个:全局 CSP 仍允许 'unsafe-inline',会显著降低前端注入后的利用门槛

同时复核了一项历史上容易误报的问题:

  • 开放重定向:本轮未确认成立auth 登录真正跳转前使用了 validate_redirect() 做同域/相对路径校验。

漏洞 1prompt /api/test-connection 可匿名触发服务端外连(确认,高危)

风险等级

高危

影响

匿名用户可直接调用 https://prompt.ephron.ren/api/test-connection,让服务端向任意 base_url 发起请求,并将网络行为结果或目标返回内容片段回显给调用者。这会带来:

  1. SSRF / 内网探测:可探测 127.0.0.1、RFC1918、169.254.169.254 等地址可达性
  2. 服务端网络画像泄露根据超时、拒绝连接、HTTP 响应差异判断服务端所处网络环境
  3. 目标响应内容回显:接口会把 response.text[:200] 拼到返回 JSON 中,形成受控信息泄露
  4. 可被滥用为外连代理:尽管当前固定了路径 /chat/completions/v1/messages,仍可用于扫描和探测第三方或内部 HTTP 服务

源码证据

prompt/src/routes/api.py

@router.post("/test-connection")
async def test_connection(payload: TestConnectionRequest):

该接口没有任何登录、管理员、service token 或 CSRF 前置校验。

同时其直接使用用户提供的 base_url 发起外连:

response = await client.post(
    f"{payload.base_url}/chat/completions",

或:

response = await client.post(
    f"{payload.base_url}/v1/messages",

并把目标响应体前 200 字符回显:

return {
    "success": False,
    "error": f"HTTP {response.status_code}: {response.text[:200]}",
}

线上验证证据

匿名请求可直接成功命中该接口,且返回 200

  • base_url=https://example.com → 返回 HTTP 405 与 HTML 片段
  • base_url=http://127.0.0.1:9 → 返回 All connection attempts failed
  • base_url=http://169.254.169.254 → 返回 连接超时
  • base_url=http://10.255.255.1 → 返回 连接超时

这说明:

  1. 接口对匿名用户开放
  2. 服务端确实对调用者指定地址发起了网络连接
  3. 不同目标的网络状态可以被外部观测和区分

复现方式

curl -s https://prompt.ephron.ren/api/test-connection \
  -H 'Content-Type: application/json' \
  -d '{
    "provider":"openai",
    "base_url":"http://127.0.0.1:9",
    "api_key":"x",
    "model":"x"
  }'

修复建议

按优先级建议:

  1. 立即加鉴权:至少要求登录管理员;更稳妥的是仅限内部 admin UI 调用
  2. 禁止任意 base_url:仅允许测试已保存配置中的候选地址,不能由请求体任意指定
  3. 加入 SSRF 防护:拒绝私网、回环、链路本地、保留地址与裸 IP
  4. 移除目标响应体回显:只返回通用错误码/状态,不返回 response.text
  5. 增加审计日志与限流:记录调用者、目标 host、结果对该接口做严格 rate limit

漏洞 2prompt 调试流错误消息存在潜在 HTML 注入 / XSS 链(确认,中危)

风险等级

中危

影响

prompt 调试功能把上游 LLM/provider 返回的错误文本一路传到前端,并在浏览器中使用 innerHTML 渲染。如果攻击者能够控制:

  • provider 的错误内容
  • 或管理员配置的测试目标返回内容
  • 或某些异常消息中的 HTML 片段

则存在将 HTML/脚本片段注入到调试界面的风险。

这条链当前更像是管理/调试面 XSS 风险,而不是匿名立即拿下;但它与漏洞 1 组合后风险会放大:匿名攻击者可借 test-connection 验证回显形态,管理员再使用调试功能时可能触发注入。

源码证据

后端:prompt/src/services/llm.py

elif response.status_code != 200:
    raise LLMError(f"API error: {response.text}", "api_error")

后端 SSEprompt/src/routes/api.py

except LLMError as e:
    yield f"data: {json.dumps({'type': 'error', 'detail': e.message, 'code': e.code})}\n\n"
except Exception as e:
    yield f"data: {json.dumps({'type': 'error', 'detail': str(e), 'code': 'unknown'})}\n\n"

前端:prompt/static/js/test-prompt.js

contentDiv.innerHTML = `<div class="error">${event.detail}</div>`;

以及:

contentDiv.innerHTML = `<div class="error">${error.message}</div>`;

风险链说明

完整链路为:

provider response.text / exception textLLMError.message → SSE event.detail → 前端 innerHTML

如果 event.detail 中包含 HTML浏览器会按 HTML 解释,而不是纯文本显示。

修复建议

  1. 前端错误展示统一改为 textContent
  2. 若必须保留富文本,使用严格白名单 sanitizer而不是裸 innerHTML
  3. 后端不要原样透传上游 response.text,改为固定错误文案 + 内部日志记录详细信息
  4. 为该链路补充回归测试:确保 <img onerror=...> / <script> 等片段只按文本显示

问题 3全局 CSP 仍允许 'unsafe-inline'(确认,中低危)

风险等级

中低危

影响

shared/security_headers.py 中的全局 CSP 仍允许:

  • script-src 'unsafe-inline'
  • script-src-elem 'unsafe-inline'
  • style-src 'unsafe-inline'

这不是“单独即可利用”的漏洞,但会显著降低任何前端注入点的利用门槛,使本来可能只造成 HTML 注入的问题更容易升级为脚本执行。

源码证据

shared/security_headers.py

_CSP_POLICY = (
    "default-src 'self'; "
    "script-src 'self' 'unsafe-inline'; "
    "script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
    "style-src 'self' 'unsafe-inline'; "

修复建议

  1. 逐步移除内联脚本,迁移到静态 JS 文件
  2. 使用 nonce/hash 机制替代 'unsafe-inline'
  3. 先从高敏感页面admin / auth / prompt test开始收紧 CSP
  4. 在 CI 中加入 CSP 违规检查,防止回退

已复核但未确认为漏洞的项

1. 开放重定向

本轮没有将其确认为漏洞。

原因:

  • auth/src/routes/pages.py/login 接收 redirect/return_url/next
  • 多服务 logout 入口也可把外部 URL 编码后传给 auth
  • 但真正登录成功跳转时,auth/src/routes/api.py 调用了:
safe_redirect = validate_redirect(redirect_target)

auth/src/utils/redirect.py 仅允许:

  • 相对路径
  • *.ephron.ren
  • 开发环境的 localhost/127.0.0.1

因此目前证据表明真正落地跳转存在安全校验,不应误报为已确认开放重定向。

2. canvas raw iframe 隔离

canvas 公开页使用:

iframe.sandbox = 'allow-scripts allow-same-origin';

/raw/{slug} 又显式允许同源嵌入:

"X-Frame-Options": "SAMEORIGIN"
"Content-Security-Policy": raw_csp

这在模型上确实增加了审计复杂度,但从当前证据看它更像是产品设计选择(同源预览能力),尚未直接证明可跨出 iframe 沙箱获得父页面控制权。因此本轮只记为建议复核项,暂不记为 confirmed vulnerability。


修复优先级建议

P0立即处理

  1. 下线或加固 prompt /api/test-connection
    • 加管理员鉴权
    • 禁止任意 base_url
    • 拦截私网 / 回环 / 链路本地地址
    • 去掉响应体回显

P1本周处理

  1. 修复 prompt 调试流错误展示 XSS 风险
    • 前端改 textContent
    • 后端不透传原始错误体

P2本轮安全加固

  1. 收紧 CSP逐步移除 'unsafe-inline'

P3继续审计

  1. canvas raw 预览隔离模型做专项验证
  2. 对全部带 |safe 的 Jinja 宏做来源审计,确认调用方是否只传入受信任 HTML

建议新增测试

  1. prompt:匿名调用 /api/test-connection 应返回 401/403
  2. prompt:拒绝私网/回环 base_url
  3. prompt:错误消息渲染时 HTML 只按文本显示
  4. sharedCSP 收紧后的回归测试
  5. canvasraw 页面 sandbox / CSP / frame-ancestors 组合测试

审计结论

当前最需要优先修的是 prompt 服务:

  • 一条是已可被匿名外部直接利用的 SSRF/探测接口
  • 另一条是调试链路的潜在 XSS

其余服务(blog / canvas / auth / home)本轮未发现同等级的新匿名高危写入或未鉴权 service API 暴露问题service API 边界从静态与线上返回结果看总体是收紧的。