9.2 KiB
ephron.ren 2026-05-16 全面安全审计(第二轮)
审计范围
本轮针对 ephron.ren 多服务单仓架构进行源码静态审计 + 线上非破坏性动态验证,覆盖:
authblogcanvashomepromptshared
审计目标:
- 识别匿名可利用的高风险接口
- 核查 service token / admin / public 权限边界
- 识别 CSRF / XSS / SSRF / 重定向 / 信息泄露类问题
- 为修复提供可落地建议与优先级
结论摘要
本轮确认 3 个需要处理的问题,其中:
- 高危 1 个:
prompt存在匿名可利用的 SSRF / 外连探测接口 - 中危 1 个:
prompt调试流错误链存在潜在 HTML 注入 / XSS 风险 - 中低危 1 个:全局 CSP 仍允许
'unsafe-inline',会显著降低前端注入后的利用门槛
同时复核了一项历史上容易误报的问题:
- 开放重定向:本轮未确认成立。
auth登录真正跳转前使用了validate_redirect()做同域/相对路径校验。
漏洞 1:prompt /api/test-connection 可匿名触发服务端外连(确认,高危)
风险等级
高危
影响
匿名用户可直接调用 https://prompt.ephron.ren/api/test-connection,让服务端向任意 base_url 发起请求,并将网络行为结果或目标返回内容片段回显给调用者。这会带来:
- SSRF / 内网探测:可探测
127.0.0.1、RFC1918、169.254.169.254 等地址可达性 - 服务端网络画像泄露:根据超时、拒绝连接、HTTP 响应差异判断服务端所处网络环境
- 目标响应内容回显:接口会把
response.text[:200]拼到返回 JSON 中,形成受控信息泄露 - 可被滥用为外连代理:尽管当前固定了路径
/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 failedbase_url=http://169.254.169.254→ 返回连接超时base_url=http://10.255.255.1→ 返回连接超时
这说明:
- 接口对匿名用户开放
- 服务端确实对调用者指定地址发起了网络连接
- 不同目标的网络状态可以被外部观测和区分
复现方式
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"
}'
修复建议
按优先级建议:
- 立即加鉴权:至少要求登录管理员;更稳妥的是仅限内部 admin UI 调用
- 禁止任意 base_url:仅允许测试已保存配置中的候选地址,不能由请求体任意指定
- 加入 SSRF 防护:拒绝私网、回环、链路本地、保留地址与裸 IP
- 移除目标响应体回显:只返回通用错误码/状态,不返回
response.text - 增加审计日志与限流:记录调用者、目标 host、结果;对该接口做严格 rate limit
漏洞 2:prompt 调试流错误消息存在潜在 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")
后端 SSE:prompt/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 text
→ LLMError.message
→ SSE event.detail
→ 前端 innerHTML
如果 event.detail 中包含 HTML,浏览器会按 HTML 解释,而不是纯文本显示。
修复建议
- 前端错误展示统一改为
textContent - 若必须保留富文本,使用严格白名单 sanitizer,而不是裸
innerHTML - 后端不要原样透传上游
response.text,改为固定错误文案 + 内部日志记录详细信息 - 为该链路补充回归测试:确保
<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'; "
修复建议
- 逐步移除内联脚本,迁移到静态 JS 文件
- 使用 nonce/hash 机制替代
'unsafe-inline' - 先从高敏感页面(admin / auth / prompt test)开始收紧 CSP
- 在 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(立即处理)
- 下线或加固
prompt /api/test-connection- 加管理员鉴权
- 禁止任意
base_url - 拦截私网 / 回环 / 链路本地地址
- 去掉响应体回显
P1(本周处理)
- 修复
prompt调试流错误展示 XSS 风险- 前端改
textContent - 后端不透传原始错误体
- 前端改
P2(本轮安全加固)
- 收紧 CSP,逐步移除
'unsafe-inline'
P3(继续审计)
- 对
canvas raw预览隔离模型做专项验证 - 对全部带
|safe的 Jinja 宏做来源审计,确认调用方是否只传入受信任 HTML
建议新增测试
prompt:匿名调用/api/test-connection应返回 401/403prompt:拒绝私网/回环base_urlprompt:错误消息渲染时 HTML 只按文本显示shared:CSP 收紧后的回归测试canvas:raw 页面 sandbox / CSP / frame-ancestors 组合测试
审计结论
当前最需要优先修的是 prompt 服务:
- 一条是已可被匿名外部直接利用的 SSRF/探测接口
- 另一条是调试链路的潜在 XSS
其余服务(blog / canvas / auth / home)本轮未发现同等级的新匿名高危写入或未鉴权 service API 暴露问题;service API 边界从静态与线上返回结果看总体是收紧的。