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

319 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 边界从静态与线上返回结果看总体是收紧的。