Files
ai-gateway-go/docs/security-review-0.10.1.md
T
superidou 9501751792 0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
  与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
  撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
  全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
  >4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
  后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
  nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
2026-08-13 10:50:51 +08:00

365 lines
22 KiB
Markdown
Raw 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.
# 安全审查与修复报告(0.10.1 修订)
审查范围:`internal/*`29k 行 Go)、`migrations/``deploy/``api/openapi/`
审查方式:4 路并行深度代码审查 + 关键发现人工复核 + 全量单元测试/`go vet`
日期:2026-08-12。以下条目均已在代码中修复并验证;未修复项作为已知限制列出。
## 高危(已修复)
### 1. 数据平面 SSRFDNS rebinding 可把代理流量引向内网
`internal/gateway/proxy.go``http.Transport` 原先使用普通 `net.Dialer`
Provider 主机名被 DNS 重绑到 169.254.169.254 / 10.x 后,网关会把带完整
Prompt 的请求转发到内网并回传响应。控制面有 `safeDialContext`,数据面没有。
**修复**`provider.SafeDialContext`(拨号前对所有解析地址做公网校验、直连
已校验 IP 防止二次解析)统一供控制面与数据面使用;`Proxy` 新增
`SetAllowPrivateProviderURLs`,由 `ALLOW_PRIVATE_PROVIDER_URLS` 控制。
### 2. 网关 API Key 泄露给上游
`internal/provider/openai/adapter.go``Prepare` 只在配置了上游凭据时
`Set Authorization`,从不剥离客户端头——Provider 无 API Key 时,客户端
用于认证网关的完整 Bearer Key 被原样转发给上游。
**修复**:无条件 `Header.Del("Authorization")` 后再按需注入。
### 3. SSRF 判定网段不完整(CGNAT/6to4/NAT64 可绕过)
`isPublicAddress` 只用了 Go 内建分类,漏掉 100.64.0.0/10、198.18.0.0/15、
192.0.2.0/24、192.88.99.0/24、240.0.0.0/4、2002::/166to4 内嵌 IPv4)、
64:ff9b::/96NAT64)等;且存在两份重复实现(provider 与 controlplane)。
**修复**:导出 `provider.IsPublicAddress` 单点实现,补齐全部特殊用途网段,
v4-mapped v6 先 `Unmap()` 再判定;两处调用统一。
### 4. 应用/数字员工运行把所有网关错误伪装成 200
`internal/workbench/runtime_http.go``boundedRecorder` 初始 `code=200`
`WriteHeader(4xx/5xx)` 变成 no-op:鉴权 401、限流 429、上游 502 全部被
记录为成功,`callGateway` 的错误分支是死代码,审计/trace/事实核验数据被
污染。
**修复**`code` 初始化为 0`Write` 兜底 200;错误分支恢复生效。
### 5. 登录枚举 + 锁定状态泄露(响应差异 + 计时侧信道)
`internal/identity`:未知账号 401 / 停用 403 / 锁定 429(精确倒计时)三态
可枚举全部管理员/门户账号及其安全状态;停用/锁定账号不执行 dummy 哈希,
响应时间还泄露"账号是否存在"。
**修复**:登录路径四种失败(未知/停用/锁定/口令错)统一返回 401
"账号或口令错误";停用/锁定账号同样执行 600k 次 dummy PBKDF2,计时一致。
### 6. 登录限流可被伪造 X-Forwarded-For 绕过
`ClientIP` 无条件采信 XFF 首值;dev compose 把网关端口暴露在 0.0.0.0
直连时每请求换一个 XFF 值即可旋转 `gateway:login-limit:<ip>` 键。
**修复**:新增 `TRUSTED_PROXIES`(默认环回+RFC1918+ULA,覆盖 compose 的
nginx 同网段场景),仅可信对端携带的 XFF 被采信;dev compose 网关端口
改绑 `127.0.0.1:8080`
### 7. 凭据变更后旧会话继续有效
会话是独立 Redis 键,无账号级版本:改密/停用 2FA 后,被盗会话在 TTL
(12h)内一直有效,强制改密形同虚设。
**修复**`Principal.AuthVersion` + Redis 计数键(TTL 7 天 > 会话 12h
保证版本键过期时旧会话已自然消亡)。改密、TOTP 启用/停用、备用码重生成
`BumpAuthVersion`;会话与 TOTP 挑战校验版本。
### 8. Webhook 投递失败被吞掉(无重试、无死信)
`notifications.go` `deliver` 失败只记日志,事件照常 ACK——与注释声称的
"reclaim 重试"相悖,通知实际是 at-most-once。
**修复**:投递失败让事件留在 pending 由 reclaim 重试;单条投递记录
`attempts >= 10` 后放弃自动重试(保留 failed 状态供管理端人工重试),
防止毒 Webhook 永久卡死事件队列。
## 中危(已修复)
### 9. API Key 认证热路径全表扫描
`api_keys` 只有 `(key_prefix, key_hash)` 复合索引,`WHERE key_hash=$1`
无法使用 → 每个代理请求全表扫描。
**修复**:迁移 `000029` 新增 `UNIQUE (key_hash)` 索引。
### 10. 撤销/改限流的缓存传播窗口与误报
缓存 TTL 30s + `Invalidate` 失败时 DB 已生效却向管理员报错(重试会造成
混乱)。
**修复**`Invalidate` 失败降级为 Warn 日志(DB 是权威源);缓存 TTL 降至
10s 作纵深防御。
### 11. Token 配额提交脚本可重建无 TTL 残留键
`SET ... KEEPTTL` 在键已过期(跨月)时创建永不过期的新键。
**修复**`EXISTS` 为 0 时放弃回写。
### 12. 熔断器忽略 HTTP 500
只有 502/503/504 计入熔断失败,持续 500 的上游永远不熔断且 `success()`
不断清零失败计数。
**修复**`failureResult` 将所有 5xx 计入熔断;重试仍只针对 502/503/504
与可重放请求。
### 13. Outbox 退避在 attempt≥35 溢出为负延迟
`2^(attempt-1)` 秒转 `time.Duration` 溢出 → `MarkFailed` 把 available_at
设到过去 → 事件被立即重新认领,形成热循环烧掉重试预算(`MAX_ATTEMPTS`
允许到 100)。
**修复**`attempt > 30` 直接返回上限。
### 14. 调度器停机期间漏掉所有中间执行
`scheduleDue` 只补最新一次,`* * * * *` 任务宕 30 分钟丢 29 次执行。
**修复**:从旧 `next_run_at` 起逐个 occurrence 补跑(单任务单轮上限
`catchUpLimit=100`,防长期停机瞬间灌入海量记录)。
### 15. 多字节截断产生无效 UTF-8,事件永久卡死
scheduler/trace/notifications 按字节截断 `[:1000]`,切半多字节 rune 后
PostgreSQL `text` 列拒绝写入 → 重试循环永远失败;inbox 的 Go `len()`
(字节)与 DB `length()`(字符)口径不一致,4000 字节中文正文超 4000
字符 CHECK。
**修复**:全部改为 rune 边界安全截断;inbox 落库前按 rune 数截断
title 256 / body 4000 / link 512)。
### 16. 管理端部分更新静默重置角色/状态
只改显示名的 PUT 把 superadmin 降为 operator、把停用部门重新激活(绕过
停用保护)。
**修复**`role` 留空 = 不修改(仅创建时默认 operator/member);`active`
省略时保留当前值(部门与账号一致)。
### 17. Agent 节点心跳可枚举 + 令牌非恒定时间比较
`WHERE code AND token_hash` 单条 UPDATE404 vs 401 区分有效 code。
**修复**:先按 code 取哈希、`subtle.ConstantTimeCompare`,未知 code 与
错误令牌统一 `ErrInvalidToken``Endpoint` 增加 http(s) 绝对 URL 格式校验。
### 18. MCP 服务器内部端点泄露给门户用户
`marketplace.Detail` 原样返回 `endpoint_url`/`has_secret_headers`,任何
登录门户用户可读取内网服务拓扑。
**修复**:门户详情脱敏这两个字段(管理端与运行时列表不受影响)。
### 19. usage_daily 多币种成本混算
价格支持任意货币,但日聚合无币种维度,USD 与其它币种相加成单一数字。
**修复**:迁移 `000030``currency char(3)` 并入 PKrecorder 按币种
聚合,查询视图返回 `currency`
### 20. Content policy 脱敏整包重编码
`json.Marshal` 重编码改变键序/数字格式/HTML 转义,破坏上游签名与字节
级契约。
**修复**`redactBytes` 状态机在原始字节上仅对 textual 字段值字符串做
替换,其余字节零改动(语义与 walkText 一致,含嵌套/数组上下文传播)。
### 21. 事实核验不作用于数字员工
`runApplication``applyFactCheck``runDigitalEmployee` 没有——`block`
策略可被数字员工入口绕过。
**修复**:数字员工响应同样执行事实核验管线。
### 22. 其它(批量)
- `shadow` 中间件:handler panic(如 `http.ErrAbortHandler`)泄漏并发槽位
→ 新增 completed 标志在 panic 路径归还。
- 审计关闭 drain 只试一次 → 3 次有限重试 + 失败明确日志。
- Trace 表无保留 → `TRACE_RETENTION`(默认 90 天)由 maintenance worker
清理(spans 靠 ON DELETE CASCADE)。
- 广播(inbox)非事务逐行插入 → 单事务包裹,中途失败不留半套消息。
- `visible()` 空 APIKeyID fail-open → fail-closed。
- 文件下载缺 `X-Content-Type-Options: nosniff`(用户可控 Content-Type)。
- Provider 管理错误原样返回内网解析地址 → `ErrBlockedAddress` 哨兵,
客户端只收通用提示,细节进服务端日志。
- 备用码生成 `%31` 取模偏差 → 拒绝采样(丢弃 ≥248)。
- OIDC 每次登录拉 discovery+JWKS → JWKS 按 provider 缓存 5 分钟。
- MCP 客户端缓存不随服务器编辑失效 → 缓存键含 `revision`
- 凭据轮换单条损坏阻塞全部 → 跳过并计数。
- `auditSpan.capture` 无锁写 → 纳入锁。
- `proxyFor` 并发 cache-miss 重复构建 → `LoadOrStore` 单飞。
- `max_tool_rounds=0`(未配置)导致应用首个工具调用即失败 → 默认 5。
## 部署配置(已修复)
- **`CREDENTIAL_MASTER_KEY` 全零默认值**(compose):任何拿到仓库的人可
解密全部 Provider 凭据/TOTP 密钥/Webhook 签名。compose 改为
`${CREDENTIAL_MASTER_KEY:?...}` 强制设置;`ValidateRuntime` 拒绝已知弱
密钥(所有环境)。
- 网关端口 8080 暴露 0.0.0.0 → 绑定 `127.0.0.1`nginx 是唯一外部入口)。
- nginx`server_tokens off` + `X-Content-Type-Options`/`X-Frame-Options`/
`Referrer-Policy` 安全头。
- worker 服务加内存限制(`deploy.resources.limits`)。
- 本地 compose 的 PostgreSQL/MinIO 弱口令为本地开发默认,README 已注明
生产必须覆盖(生产 compose 已强制要求)。
## 已知限制(未修复,建议后续处理)
1. **审计维护大锁**`audit/maintenance.go`):分区 drop/move 全程持
`ACCESS EXCLUSIVE`,flush 会阻塞。建议把空分区创建移出锁区间、仅
detach/move/attach 留在锁内,并分批删除。
2. **内容策略单条规则失败冻结全部策略**`Reload` 时任一规则编译失败会
中止整个快照刷新(保存时 `Validate` 已缓解)。建议 per-policy skip。
3. **无定价模型静默按 0 计费**`pricing.Calculate` 未命中返回零成本且无
信号。建议未定价模型加日志/指标或可配置默认价。
4. **outbox 事件丢失预算**Redis Stream `MAXLEN ~100k` 裁剪 + worker 宕机
超预算时事件永久丢失(无回放)。建议 worker 启动时按
`published_stream_id` 从 outbox 表补发。
5. **audit 幂等**`audit_events` 无唯一键,响应丢失重试会重复落审计行。
可用 `request_id + recorded_at` 建唯一约束。
6. **Outbox marker 30 天 Redis 内存税**:高吞吐下 `published:*` 键累积。
7. **多实例 JWKS/会话版本键无 TTL 清理**auth-version 键 7 天 TTL 已缓解。
8. **`UPSTREAM_FALLBACK_ENABLED` 是空转**fallback adapter 与主 adapter
相同,没有真正的第二个上游。建议支持第二上游 URL/凭据或移除该开关。
9. **OpenAPI 漂移**`scripts/check_openapi_routes.py` 报 52 条已实现路由
未收录 OpenAPIadmin files/inbox/marketplace/skills、portal marketplace、
`/v1/mcp-servers` 等)。建议把检查脚本接入 CI 并补齐文档。
10. **`/metrics` 未认证**:仅靠网络位置保护(dev compose 已回环绑定),
生产建议放内网或加访问控制。
11. **scheduled_task_runs.response 无保留**:每次执行持久化最多 2 MiB
响应。建议只存截断预览或加清理任务。
12. **agent_traces 会话聚合无预聚合表**:大表上 GROUP BY 查询慢。
## 验证
- `go build ./...``go vet ./...` 通过。
- `go test ./...` 全量单元测试通过(含 gateway/identity/scheduler/
workbench/contentpolicy/audit/outbox/provider/shadow)。
- 新增测试:XFF 可信代理/不可信对端、无可信代理时的回退行为。
- 迁移 `000029`key_hash 索引)、`000030`usage_daily currency)在
真实 PostgreSQL 上执行通过。
- Docker Compose 全栈(API + 5 workers + admin/portal 前端 + 依赖)启动
健康,`/healthz` `/readyz` 正常。
---
# 追加:业务逻辑正确性专项审查(第二轮,2026-08-13)
以功能正确性视角(算法边界/算术/并发语义/协议语义/事务一致性)对全模块复核,
在安全审查基础上新增修复:
## 已修复
1. **[高] 裸 body 上传 nil reader panic**`files_admin_http.go`/`portal_files_http.go`):
非 multipart Content-Type 上传时 body 保持 nil`io.LimitReader(nil,...)`
`Upload` 中 panicnet/http 恢复后每个此类请求 500 + panic 日志)。
修复:非 multipart 时回退 `r.Body`
2. **[高] Bootstrap key 流量永久卡死审计管线**(`audit/recorder.go`):
bootstrap 身份 `APIKeyID="bootstrap"` 非 UUID,写入
`usage_daily.api_key_id uuid NOT NULL REFERENCES api_keys` 使整批事务失败、
pending 永不清理、审计队列填满后所有事件被丢弃。
修复:`uuidPointer` 对非 UUID 返回 NULL;日聚合跳过非 UUID 身份(审计
事件本身仍落 audit_events)。
3. **[高] 定价通配符优先级错误**`pricing/service.go`):通配符只按生效日期
排序,`gpt-4o-mini` 可能命中更宽泛的 `gpt-4*` 价格。
修复:通配符按前缀长度降序(更具体优先)再按生效日期。
4. **[高] 全局工具对所有人生效不可见**(`runtime_http.go visible()`):
`len(departments)==0` 对 secure 资源返回 false——无部门限定的工具/MCP
全部消失,绑定它们的应用运行时失败且无配置期报错。
修复:无部门限定=全局资源,所有已认证主体可见;部门匹配只约束有部门
限定的资源。
5. **[高] 调度任务可配置永远无法执行的目标**(`scheduler/service.go`):
管理员创建的 API Key 无 tenant,对部门限定应用/员工运行时一律 404/403,
但保存时无任何校验。
修复:`validateTarget` 增加 `requireKeyTenant` 预检——目标部门限定时,
任务 API Key 必须存在、启用且 tenant 匹配,否则保存即报错。
6. **[中] 路由权重合计 0 → 整数除零 panic**(`runtime/resolver.go`):weight≤0
的脏数据行(绕过管理端校验)使 `Sum64()%0` panic。
修复:权重池过滤非正权重;全非正时视为无匹配。
7. **[中] 重试路径 nil `GetBody()` panic**`gateway/resilience.go`):GET/HEAD
携带未设置 GetBody 的 body 时重试调用 nil 方法。
修复:GetBody 缺失时放弃重试(避免空体重发)。
8. **[中] TOTP 挑战令牌双重使用竞态**(`identity/session.go`):两个并发请求
用同一 pending 令牌 + 各自有效的不同因子可铸出两个会话。
修复:`AuthenticatePending``GetDel` 原子消费。
9. **[中] 事实核验策略作用域失效**`factcheck/engine.go` + workbench):
`ORDER BY scope LIMIT 1` 只执行字典序第一条策略,其余部门策略被静默忽略
或张冠李戴。
修复:`Check` 增加 scope 参数,`enabledPolicy` 优先精确匹配
`department:<uuid>`、回退 globalworkbench 从应用/员工部门推导 scope。
10. **[低] 熔断器陈旧成功关闭刚打开的电路**(`gateway/resilience.go`):
打开前通过 allow() 的在途请求成功会清零失败计数并清除 openUntil。
修复:仅半开探针成功可关闭电路;关闭态成功只清零计数。
11. **[低] MCP 工具缓存读竞态**`mcp_client.go`):tools/toolsAt 无锁读。
修复:读写统一持锁。
12. **[低] 心跳 UPDATE 不复查 token_hash**`agentnode/store.go`):轮换瞬间
旧令牌仍可更新一次节点元数据。
修复:UPDATE 条件恢复 `token_hash=$2`
13. **[低] 卸载已停用/归档资源失败**`marketplace.go`):publishedResourceID
要求 enabled+published,管理员停用后用户无法卸载,安装行永久卡死。
修复:Uninstall 改用不限制状态的 `resourceIDByCode`
## 复核为正确的关键逻辑(节选)
- 限流 Lua:无 off-by-one(限额 N 精确放行 N 次);月配额键含月份自动翻转。
- Token 配额 reserve/commit/refund 对账数学正确、并发安全;跨月提交安全跳过。
- `usageSession.finish` sync.Once 保证恰好执行一次;SSE/JSON usage 提取路径
无双重计数。
- 审计 recorder 队列/pending 无丢失无重复(事务内 CopyFrom+聚合原子提交)。
- 分区维护 SQL 边界([start,end)、drop 判定)正确。
- PBKDF2/TOTP/备用码密码学实现正确(±1 窗口、原子防重放、拒绝采样)。
- 调度器补跑循环无 off-by-oneoutbox claim/发布/标记与去重正确。
- 会话 AuthVersion 语义正确:bump 后新登录签发新版本、旧会话 fail-closed。
---
# 追加:网关接入报错修复与部署密钥持久化(2026-08-13)
## 用户报告的问题
管理端「网关接入」页面提示"服务器内部错误,请稍后重试"。
## 根因
`GET /api/v1/admin/providers` 对每条记录解密凭据,**任何一条解密失败
AES-GCM tag 校验失败)都会让整个列表返回 400**。本环境每次重建部署都
重新生成随机 `CREDENTIAL_MASTER_KEY`,旧 key 加密的 Provider 记录全部
无法解密 → 列表接口 400 → 前端显示"服务器内部错误"。
## 修复
1. **[bug] Provider 列表降级展示**`provider/admin_http.go view()`):
单条凭据解密失败不再使整个列表报错,改为 `credential_error` 警示字段
返回 200,管理端仍可编辑/删除该记录恢复。
2. **[功能缺口] Provider 缺少删除接口**:新增
`DELETE /api/v1/admin/providers/{id}``repository.Delete` 事务 + outbox
`provider.deleted` 事件;provider_models/model_routes 由 FK 级联清理)。
3. **[部署] master key 持久化**:本地 compose 首次启动生成
`deploy/.env`(compose 自动读取)并复用,重启/重建不再换 key;
`.gitignore` 排除 `deploy/.env``deploy/PRODUCTION.md` 增加持久化与
轮换说明。
## 验证
- 列表接口对损坏记录返回 200 + `credential_error` 警示(旧代码返回 400)。
- DELETE 接口删除成功,级联清理正常。
- 用持久 key 重建 Provider 后**容器重启凭据仍可解密**
`key_configured: true, credential_error: ''`),resolver 快照刷新无错误。
- 全量单测 24 包通过。
---
# 追加:第三轮审查(前端 + 回归,2026-08-13
## 前端审查发现并修复(web/apps/admin + portal + api-client
1. **[高] 凭据异常不可见**:后端新增的 `credential_error` 字段前端未展示,
运维无法发现 KEK 失配/损坏的 Provider。修复:`ProviderRecord` 增加字段,
凭据列对异常记录显示红色警示 Tag(tooltip 展示原因)。
2. **[高] localStorage 持久化敏感数据**Pinia persist 全量落盘
accessToken/refreshToken/账户信息/锁屏密码密文),任何 XSS 直接窃取。
修复:改为 sessionStorage + `pick` 白名单(仅 isLogin/token/language),
账户信息与锁屏密码不再持久化。
3. **[中] 后端错误消息被吞**`handleError` 优先用状态码通用文案,400/429
显示"服务器内部错误",校验失败原因不可见。修复:后端 `msg` 优先;
补齐 400/429 文案与枚举。
4. **[中] 门户用量页显示原始微单位**`cost_microunits` 原样输出。修复:
按 /1e6 转换显示 USD 金额(与管理端一致)。
5. **[功能] Provider 删除按钮**:前端补齐删除入口(确认框 + 级联提示)。
6. 记录未修项:refreshToken 双端死代码、锁屏密码加密占位 key(已不落盘,
建议后续改服务端校验)、注册/找回密码页为模板 mock、B7 约 21 处
ElMessageBox 取消未捕获等。
## Go 回归审查发现并修复
1. **[高] >4MB 非流式响应 token 计量丢失**:`consumeUsageObject` 提取的
usage 对象以 `inUsage=false` 解析,顶层 *_tokens 永不计数;压缩触发后
计量静默回退为预留值。修复:`consumeJSONAsUsage`inUsage=true),
并加固 "usage" 键位置判定(要求前一非空白字符为 { 或 ,)。
2. **[中] 脱敏检测与改写不一致**(转义内容):`\u002d` 等 JSON 转义使
解码后匹配成功但字节改写失败,请求原样上送却标记已脱敏。修复:
`applyRules` 增加解码文本回退路径(替换后重新 JSON 转义);替换文本
本身做 JSON 转义防破坏结构;`Redacted` 以字节改写结果为准,不谎报。
3. **[中] 脱敏规则跨策略泄漏**:字节改写使用所有 redact 策略的规则(含
不适用于本请求的端点/模型作用域)。修复:仅收集本请求命中的 redact
策略规则。
4. **[中] 熔断探针语义**:陈旧成功/取消探针会错误关闭电路。修复:
`allow` 返回探针标记,`success(probe)` 仅探针成功可关闭;取消探针
调用 `abortProbe` 保持打开。
5. **[中] TOTP 挑战令牌先消费后验证**:验证码输错即烧令牌、强制重新登录
且叠加锁定计数。修复:`AuthenticatePending` 改只读,验证通过后
`ConsumePending`(GetDel)原子消费——重试友好且防双会话。
6. **[中] 管理员重置密码不作废会话**management update 补
`BumpAuthVersion`,与自助改密语义一致。
7. **[低] 会话 TTL 校验**`AUTH_SESSION_TTL` 上限 7 天,`authVersionTTL`
提高至 14 天,杜绝版本键先过期导致旧会话复活。
8. **[低] provider delete 补充 `tenant_id IS NULL` 过滤**(与其它查询一致)。
## 新 logoLLMGuardX语枢)
- 设计:AI 网关路由主题——深蓝圆角方块 + 青色枢纽节点 + 三向分支连接,
中心"语枢"字母 A 标记;沿用品牌色(#071F4D/#00E4E5/#006EFF)。
- 替换:侧边栏/顶栏 logo(SVG,Vite 内联)、登录页图标、favicon16-256
多尺寸 ICO)。程序化像素验证渲染正确。