add second-round security audit for ephron.ren

This commit is contained in:
Ubuntu
2026-05-16 12:44:16 +08:00
parent 2f6209d94a
commit 45cabb79c9

View File

@@ -0,0 +1,318 @@
# 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()` 做同域/相对路径校验。
---
## 漏洞 1`prompt /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`
```python
@router.post("/test-connection")
async def test_connection(payload: TestConnectionRequest):
```
该接口没有任何登录、管理员、service token 或 CSRF 前置校验。
同时其直接使用用户提供的 `base_url` 发起外连:
```python
response = await client.post(
f"{payload.base_url}/chat/completions",
```
或:
```python
response = await client.post(
f"{payload.base_url}/v1/messages",
```
并把目标响应体前 200 字符回显:
```python
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. 不同目标的网络状态可以被外部观测和区分
### 复现方式
```bash
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
---
## 漏洞 2`prompt` 调试流错误消息存在潜在 HTML 注入 / XSS 链(确认,中危)
### 风险等级
中危
### 影响
`prompt` 调试功能把上游 LLM/provider 返回的错误文本一路传到前端,并在浏览器中使用 `innerHTML` 渲染。如果攻击者能够控制:
- provider 的错误内容
- 或管理员配置的测试目标返回内容
- 或某些异常消息中的 HTML 片段
则存在将 HTML/脚本片段注入到调试界面的风险。
这条链当前更像是**管理/调试面 XSS 风险**,而不是匿名立即拿下;但它与漏洞 1 组合后风险会放大:匿名攻击者可借 test-connection 验证回显形态,管理员再使用调试功能时可能触发注入。
### 源码证据
后端:`prompt/src/services/llm.py`
```python
elif response.status_code != 200:
raise LLMError(f"API error: {response.text}", "api_error")
```
后端 SSE`prompt/src/routes/api.py`
```python
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`
```javascript
contentDiv.innerHTML = `<div class="error">${event.detail}</div>`;
```
以及:
```javascript
contentDiv.innerHTML = `<div class="error">${error.message}</div>`;
```
### 风险链说明
完整链路为:
`provider response.text / exception text`
`LLMError.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`
```python
_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` 调用了:
```python
safe_redirect = validate_redirect(redirect_target)
```
`auth/src/utils/redirect.py` 仅允许:
- 相对路径
- `*.ephron.ren`
- 开发环境的 localhost/127.0.0.1
因此目前证据表明**真正落地跳转存在安全校验**,不应误报为已确认开放重定向。
### 2. canvas raw iframe 隔离
`canvas` 公开页使用:
```javascript
iframe.sandbox = 'allow-scripts allow-same-origin';
```
`/raw/{slug}` 又显式允许同源嵌入:
```python
"X-Frame-Options": "SAMEORIGIN"
"Content-Security-Policy": raw_csp
```
这在模型上确实增加了审计复杂度,但从当前证据看它更像是产品设计选择(同源预览能力),尚未直接证明可跨出 iframe 沙箱获得父页面控制权。因此本轮只记为**建议复核项**,暂不记为 confirmed vulnerability。
---
## 修复优先级建议
### P0立即处理
1. 下线或加固 `prompt /api/test-connection`
- 加管理员鉴权
- 禁止任意 `base_url`
- 拦截私网 / 回环 / 链路本地地址
- 去掉响应体回显
### P1本周处理
2. 修复 `prompt` 调试流错误展示 XSS 风险
- 前端改 `textContent`
- 后端不透传原始错误体
### P2本轮安全加固
3. 收紧 CSP逐步移除 `'unsafe-inline'`
### P3继续审计
4.`canvas raw` 预览隔离模型做专项验证
5. 对全部带 `|safe` 的 Jinja 宏做来源审计,确认调用方是否只传入受信任 HTML
---
## 建议新增测试
1. `prompt`:匿名调用 `/api/test-connection` 应返回 401/403
2. `prompt`:拒绝私网/回环 `base_url`
3. `prompt`:错误消息渲染时 HTML 只按文本显示
4. `shared`CSP 收紧后的回归测试
5. `canvas`raw 页面 sandbox / CSP / frame-ancestors 组合测试
---
## 审计结论
当前最需要优先修的是 `prompt` 服务:
- 一条是**已可被匿名外部直接利用**的 SSRF/探测接口
- 另一条是**调试链路的潜在 XSS**
其余服务(`blog` / `canvas` / `auth` / `home`)本轮未发现同等级的新匿名高危写入或未鉴权 service API 暴露问题service API 边界从静态与线上返回结果看总体是收紧的。