@@ -0,0 +1,143 @@
|
|||||||
|
# 审计报告:中控 Bridge + 实例全方位(2026-10-07)
|
||||||
|
|
||||||
|
> 基线提交:`a23fb7c`(含数据统计页与实盘说明同步)
|
||||||
|
> 对照:`docs/审计报告-高危修复-2026-10-07.md`(币本位高危闭环)
|
||||||
|
|
||||||
|
## 1. 范围
|
||||||
|
|
||||||
|
| 范围 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 本仓库实例侧 | `app.py`、`lib/options/**`、`lib/instance/**`、`lib/hub/**`(Bridge)、`lib/sim/**`、`lib/env/**`、备份/远程更新 |
|
||||||
|
| 本仓库中控侧 | 仅有 **实例 Bridge**(`/api/hub/*`、`/hub_sso`);**中控前端/常驻进程**在独立仓库(如 `crypto_okx_hub`),本轮未审其 SSRF/SSE 实现 |
|
||||||
|
| 方法 | 三路并行静态审阅(Bridge 权限、资金路径回归、Web 鉴权/XSS/CSRF)+ 人工复核关键代码 + 对照 10-07 高危修复项 |
|
||||||
|
|
||||||
|
## 2. 总览
|
||||||
|
|
||||||
|
| 级别 | 数量 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Critical | 1 | Bridge Token 明文进 env 设置页 → 等价远程 root |
|
||||||
|
| High | 6 | 平仓后卖币回归;settings 认 Bridge Token;7 天 embed_sso;备份 tar slip;CSRF/破坏性 GET;`/options/guide` 缺模块 500 |
|
||||||
|
| Medium | 9 | 监控/条件单失败语义;close_preview 时延;Webhook;登录无限速;复盘图;journal 删路径;frame-ancestors=*;version 泄路径 |
|
||||||
|
| Low | 若干 | 密码策略、进程内 SECRET 回退、局部 XSS、SSO 可重放至过期、settings Token 用 `==` |
|
||||||
|
|
||||||
|
**币本位 10-07 Critical/High 主路径**:开仓增量、卖回封顶、粉尘桥、`sweep_spot_after_options_flat`、中控条件单持仓上限等 **仍在**;本地相关单测此前已通过。
|
||||||
|
**回归**:`maybe_sell_spot_after_close` 未与 sweep 对齐。
|
||||||
|
|
||||||
|
## 3. Critical
|
||||||
|
|
||||||
|
### C1 · `HUB_BRIDGE_TOKEN` 对已登录会话明文可见
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `lib/env/env_schema.py`(`SENSITIVE_EXACT` 未含 `HUB_BRIDGE_TOKEN`);`/api/settings/env`;设置页 JS 明文输入框 |
|
||||||
|
| 证据 | `_is_sensitive` 仅匹配 `APP_PASSWORD` / `*_SECRET` / `*_API_KEY` 等;`HUB_BRIDGE_TOKEN` 走明文 `current` |
|
||||||
|
| 影响 | 任意已登录或持有 7 天 `embed_sso` 的会话可复制 Token,进而无 Cookie 调用 `/api/hub/update`(git pull+重启)、`/api/hub/backup/download`(含 `.env`)、平仓/条件单等 |
|
||||||
|
| 建议 | 将 `HUB_BRIDGE_TOKEN`(及 `WECHAT_WEBHOOK`)列入 `SENSITIVE_EXACT`;UI 仅显示掩码;复制走一次性确认;拆分 read/trade/admin Token |
|
||||||
|
|
||||||
|
## 4. High
|
||||||
|
|
||||||
|
### H1 · 平仓后卖币:持仓查询失败/`None` 当空仓(回归)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `lib/options/options_coin_open_lib.py` · `maybe_sell_spot_after_close` |
|
||||||
|
| 证据 | `fetch_option_positions(ex) or []`;`except: pass` 后仍调用 `sell_residual_after_option_flat` |
|
||||||
|
| 对照 | 同文件 `sweep_spot_after_options_flat` 已正确拒绝 `None`/异常 |
|
||||||
|
| 调用链 | 买一平仓 / close_exec / monitor 同步卖币 |
|
||||||
|
| 影响 | API 抖动时可能市价卖掉桥/抵押币,期权仓仍在 |
|
||||||
|
| 建议 | 与 sweep 对齐;加单测 |
|
||||||
|
|
||||||
|
### H2 · Bridge Token 可认证大部分 `/api/settings/*`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `lib/instance/instance_settings_register.py` · `_api_login_required` |
|
||||||
|
| 证据 | `X-Hub-Token == HUB_BRIDGE_TOKEN` → `allowed=True`;仅拦截 env **写入**,不拦 restart / backup restore / download |
|
||||||
|
| 影响 | Token 即可恢复备份(含 `.env`)、PM2 重启,无需实例密码 |
|
||||||
|
| 建议 | Bridge 只走 `/api/hub/**`;设置类 API 仅 session/embed_sso |
|
||||||
|
|
||||||
|
### H3 · `embed_sso` 默认 7 天且可进 URL
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `hub_bridge_register.py` `/hub_sso`;`app.py` `login_required` |
|
||||||
|
| 影响 | Referer/历史/日志泄露 → 长期全权 UI(含开仓、改密、恢复备份) |
|
||||||
|
| 建议 | TTL 缩至数小时;禁止 query 传参(仅 header/sessionStorage);绑定 Origin |
|
||||||
|
|
||||||
|
### H4 · 备份恢复 `tar.extractall` 无成员路径校验
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `lib/instance/backup_lib.py` · `restore_backup` |
|
||||||
|
| 影响 | 已认证用户恢复恶意包时可写穿临时目录(tar slip) |
|
||||||
|
| 建议 | Python 3.12+ `filter="data"`;或校验成员路径不越出 `tmp` |
|
||||||
|
|
||||||
|
### H5 · CSRF:`SameSite=None` + 无 CSRF Token;破坏性 GET
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `app.py` Cookie SameSite 默认 None;`/del_key/<id>` GET 删除 |
|
||||||
|
| 影响 | 跨站可借会话触发恢复/改密等;`<img src=/del_key/…>` 可删关键位 |
|
||||||
|
| 建议 | 状态变更一律 POST + CSRF/Origin 校验;删除改 POST |
|
||||||
|
|
||||||
|
### H6 · `/options/guide` 引用不存在的 `lib.hub.hub_strategy_lib`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 位置 | `lib/options/options_register.py`(约 441 行) |
|
||||||
|
| 证据 | 仓库内无 `hub_strategy_lib.py`;同能力已在 `lib/common/markdown_html_lib.py` |
|
||||||
|
| 影响 | 打开期权说明页直接 ImportError/500(可用性 High,非资金) |
|
||||||
|
| 建议 | 改为 `from lib.common.markdown_html_lib import render_markdown_html` |
|
||||||
|
|
||||||
|
## 5. Medium
|
||||||
|
|
||||||
|
| ID | 主题 | 位置 | 要点 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| M1 | 监控持仓拉取失败 → `[]` | `options_monitor_lib.py` | 目标监控可能被标 expired,自动化出场中断 |
|
||||||
|
| M2 | 条件单 exit `or []` / `machine_has_option_position` 视 `None` 为空 | `options_cond_order_lib.py` | 查询失败可能误结束条件单 |
|
||||||
|
| M3 | 中控快照仍算 `close_preview` | `options_hub_lib.py` | 利于流动性展示,加重超时风险(相对 10-07「关闭 preview」有回退) |
|
||||||
|
| M4 | `WECHAT_WEBHOOK` 未脱敏 | env schema | 登录用户可读完整 webhook |
|
||||||
|
| M5 | `/login` 无限速 | `app.py` | 密码爆破 |
|
||||||
|
| M6 | 复盘静态图无 login | `options_review_register.py` | 依赖不可猜测文件名 |
|
||||||
|
| M7 | journal 删除路径未限制在 upload 根下 | `journal_images_lib.py` | DB 被投毒时可删库外文件 |
|
||||||
|
| M8 | `HUB_EMBED_FRAME_ANCESTORS=*` | `hub_bridge_auth.py` | 任意父页可嵌实例(点击劫持面) |
|
||||||
|
| M9 | `/api/hub/version` 返回本机 `repo` 绝对路径 | `instance_update_lib.py` | 信息泄露(需 Bridge Token) |
|
||||||
|
|
||||||
|
## 6. Low / 运维注意
|
||||||
|
|
||||||
|
- Bootstrap 空密码时写入 `admin123`(`scripts/bootstrap_deploy_secrets.py`)— 上线后必须改密
|
||||||
|
- `APP_AUTH_DISABLED=true` 关闭全部鉴权 — 仅本地调试
|
||||||
|
- `FLASK_SECRET_KEY` 未配置时进程随机 — 重启会话失效
|
||||||
|
- 改密最短 6 位、明文比对 `.env`
|
||||||
|
- 复盘/统计主路径有 `escapeHtml`;`options_panel.js` 等对交易所字段拼接仍有局部 XSS 面
|
||||||
|
- 远程更新依赖固定脚本 + `git pull`:Token 泄露即供应链风险(与 C1/H2 叠加)
|
||||||
|
- SSO 签名令牌在过期前可重放(无一次性消费);settings 侧 Token 比对用 `==` 而非 `hmac.compare_digest`
|
||||||
|
- `lib.hub.hub_auth` 在本仓不存在,settings 鉴权依赖 ImportError 回落(行为易混淆)
|
||||||
|
|
||||||
|
## 7. 已验证仍稳健(摘要)
|
||||||
|
|
||||||
|
- Bridge 写路径:禁市价平、中控仅全平、条件单创建有持仓上限与本机有仓拒绝
|
||||||
|
- SSO `next` 防开放重定向(`_safe_next_path`)
|
||||||
|
- 备份文件名白名单 + 路径限制在 backup 目录内
|
||||||
|
- Bridge Token 校验用 `hmac.compare_digest`
|
||||||
|
- 币本位:买币增量、卖回 `coin_amount` 封顶、粉尘桥 `positions_ok`、sweep 失败不卖
|
||||||
|
- SQLite WAL + busy_timeout;条件单/目标出场多数已「先关连接再网络」
|
||||||
|
- env 对 API Key / Passphrase / App 密码有脱敏
|
||||||
|
|
||||||
|
## 8. 建议修复优先级
|
||||||
|
|
||||||
|
1. **立刻**:C1 脱敏 `HUB_BRIDGE_TOKEN`;H1 对齐 `maybe_sell_spot_after_close`;H6 guide 导入修正
|
||||||
|
2. **本周**:H2 收紧 settings 对 Bridge Token;H3 缩短 embed_sso;H4 tar filter;H5 CSRF + `/del_key` POST
|
||||||
|
3. **随后**:M1–M3 失败语义统一;M4–M9 加固;评估中控侧独立仓库 SSRF/实例 URL 白名单
|
||||||
|
|
||||||
|
## 9. 结论
|
||||||
|
|
||||||
|
- **交易资金主路径**:10-07 高危大多仍成立,但 **平仓后卖币入口存在明确 High 回归**,应优先修。
|
||||||
|
- **中控/实例权限模型**:单一长效 `HUB_BRIDGE_TOKEN` + 明文进设置页 + 设置 API 认 Token,是本轮 **最大系统性风险**(Critical)。
|
||||||
|
- **中控 UI 仓库**未纳入本轮;实例 Bridge 与中控控制面需分开再审一轮。
|
||||||
|
|
||||||
|
修复后建议:再跑一轮 Bugbot(未提交变更)+ `tests.test_spot_dust_and_sell_cap` 等币本位单测,并人工测「持仓 API 失败时不卖币」。
|
||||||
|
|
||||||
|
## 10. 审阅来源
|
||||||
|
|
||||||
|
并行静态审阅(结果已并入上文):[Bridge 权限](f31789fc-cb2a-4838-ba28-777fbdc07de0)、[资金路径回归](b0c41cf8-fa1c-4a7c-8840-b2ba60fceb15)、[Web 鉴权/CSRF](6c69483e-2340-47c0-bdb8-0d02473dbfbf)。
|
||||||
@@ -574,22 +574,27 @@ def maybe_sell_spot_after_close(
|
|||||||
inst_id: str,
|
inst_id: str,
|
||||||
close_result: dict[str, Any] | None = None,
|
close_result: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""期权平仓后若该合约为币本位且已空仓,卖回本桥残留币."""
|
"""期权平仓后若该合约为币本位且已空仓,卖回本桥残留币.
|
||||||
|
|
||||||
|
持仓查询失败/空响应时不卖,等下次监控再查(与 sweep_spot_after_options_flat 一致).
|
||||||
|
"""
|
||||||
|
del close_result # 预留字段;卖回只认交易所持仓实况
|
||||||
if margin_mode_from_inst_id(inst_id) != "coin":
|
if margin_mode_from_inst_id(inst_id) != "coin":
|
||||||
return None
|
return None
|
||||||
# 仍有仓则不卖
|
|
||||||
try:
|
try:
|
||||||
rows = cfg["fetch_option_positions"](ex) or []
|
rows = cfg["fetch_option_positions"](ex)
|
||||||
for p in rows:
|
except Exception as e:
|
||||||
if str(p.get("instId") or p.get("inst_id") or "") != inst_id:
|
return {"ok": False, "msg": f"持仓查询失败,暂不卖币: {e}", "skipped": True}
|
||||||
continue
|
if rows is None:
|
||||||
try:
|
return {"ok": False, "msg": "持仓查询失败: 空响应,暂不卖币", "skipped": True}
|
||||||
if abs(float(p.get("pos") or 0)) > 1e-12:
|
for p in rows or []:
|
||||||
return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"}
|
if str(p.get("instId") or p.get("inst_id") or "") != inst_id:
|
||||||
except (TypeError, ValueError):
|
continue
|
||||||
pass
|
try:
|
||||||
except Exception:
|
if abs(float(p.get("pos") or 0)) > 1e-12:
|
||||||
pass
|
return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"}
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
parts = inst_id.split("-")
|
parts = inst_id.split("-")
|
||||||
underlying = (parts[0] if parts else "ETH").upper()
|
underlying = (parts[0] if parts else "ETH").upper()
|
||||||
return _spot_bridge.sell_residual_after_option_flat(
|
return _spot_bridge.sell_residual_after_option_flat(
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
from flask import render_template_string
|
from flask import render_template_string
|
||||||
|
|
||||||
from lib.hub.hub_strategy_lib import render_markdown_html
|
from lib.common.markdown_html_lib import render_markdown_html
|
||||||
|
|
||||||
md_path = Path(__file__).resolve().parents[2] / "docs" / "期权开平仓与监控说明.md"
|
md_path = Path(__file__).resolve().parents[2] / "docs" / "期权开平仓与监控说明.md"
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -35,5 +35,65 @@ class TestSellCapLogic(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(sell_amt, 0.006)
|
self.assertAlmostEqual(sell_amt, 0.006)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMaybeSellAfterCloseGuard(unittest.TestCase):
|
||||||
|
"""平仓后卖回:持仓查询失败/空响应绝不卖币."""
|
||||||
|
|
||||||
|
def test_none_positions_no_sell(self):
|
||||||
|
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
|
||||||
|
|
||||||
|
called = {"sell": 0}
|
||||||
|
|
||||||
|
def _fetch(_ex):
|
||||||
|
return None
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
"fetch_option_positions": _fetch,
|
||||||
|
"get_db": lambda: (_ for _ in ()).throw(AssertionError("不应开库卖币")),
|
||||||
|
}
|
||||||
|
out = maybe_sell_spot_after_close(
|
||||||
|
cfg, object(), inst_id="ETH-USD-260701-2500-C"
|
||||||
|
)
|
||||||
|
self.assertIsInstance(out, dict)
|
||||||
|
self.assertFalse(out.get("ok"))
|
||||||
|
self.assertTrue(out.get("skipped"))
|
||||||
|
self.assertIn("空响应", str(out.get("msg") or ""))
|
||||||
|
self.assertEqual(called["sell"], 0)
|
||||||
|
|
||||||
|
def test_fetch_raises_no_sell(self):
|
||||||
|
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
|
||||||
|
|
||||||
|
def _fetch(_ex):
|
||||||
|
raise RuntimeError("network down")
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
"fetch_option_positions": _fetch,
|
||||||
|
"get_db": lambda: (_ for _ in ()).throw(AssertionError("不应开库卖币")),
|
||||||
|
}
|
||||||
|
out = maybe_sell_spot_after_close(
|
||||||
|
cfg, object(), inst_id="ETH-USD-260701-2500-C"
|
||||||
|
)
|
||||||
|
self.assertIsInstance(out, dict)
|
||||||
|
self.assertFalse(out.get("ok"))
|
||||||
|
self.assertTrue(out.get("skipped"))
|
||||||
|
self.assertIn("持仓查询失败", str(out.get("msg") or ""))
|
||||||
|
|
||||||
|
def test_still_open_skips_sell(self):
|
||||||
|
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
|
||||||
|
|
||||||
|
inst = "ETH-USD-260701-2500-C"
|
||||||
|
|
||||||
|
def _fetch(_ex):
|
||||||
|
return [{"instId": inst, "pos": "1"}]
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
"fetch_option_positions": _fetch,
|
||||||
|
"get_db": lambda: (_ for _ in ()).throw(AssertionError("有仓不应卖币")),
|
||||||
|
}
|
||||||
|
out = maybe_sell_spot_after_close(cfg, object(), inst_id=inst)
|
||||||
|
self.assertTrue(out.get("ok"))
|
||||||
|
self.assertTrue(out.get("skipped"))
|
||||||
|
self.assertIn("仍有持仓", str(out.get("msg") or ""))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user