罗德岛通讯与技术部

PRTS 后端全量代码审计报告

BACKEND AUDIT

项目工程完成度较高(三层分层清晰、并发隔离用 contextvars、流式/SSE、限流、遗忘曲线、四维记忆、信赖调制等设计都有巧思,文档齐全)。但存在若干会"静默失效"或"可直接被接管"的高危问题,优先级远高于性能优化:

状态:🔴 历史 · 最后核对 2026-07-14

审计日期:2026-07-14 审计范围:后端 app/(api / systems / core / utils),共 142 个 py 文件 / ~24,549 行 审计视角:资深后端工程师(架构、安全、并发、数据完整性、性能) 方法:4 个并行审计小组分模块深审 + 主审对 P0 项逐行核验(只读,未改任何代码)


0. 总体结论

项目工程完成度较高(三层分层清晰、并发隔离用 contextvars、流式/SSE、限流、遗忘曲线、四维记忆、信赖调制等设计都有巧思,文档齐全)。但存在若干会"静默失效"或"可直接被接管"的高危问题,优先级远高于性能优化:

  • 安全.env 明文密钥 + 可复用管理员种子码 + 前缀白名单鉴权模型,构成可被完整接管的攻击链(P0)。
  • 数据正确性:L3 长期记忆维度契约不一致 → 长期记忆线上实际失效;LLM 抖动即丢历史;叙事事件链卡死(P0/P1)。
  • 隐私隔离retrieve_zscore 缓存漏 username → 跨用户记忆泄漏(P0)。
  • 并发写安全:多处模块级 dict / JSON 持久化无锁或裸写,APScheduler 多线程下存在更新丢失与写损坏(P1)。

1. P0 — 必须立即修复(高危)

1.1 密钥明文入库 + JWT/FERNET 密钥可被离线破解(安全)

  • 位置.env(LLM_API_KEY / JWT_SECRET_KEY=****(已脱敏) / FERNET_KEY / SEED_INVITE=rhodes-seed-2024
  • 影响:JWT 密钥已知 → 可离线伪造任意 sub(含管理员 Doctor)的 token,完整认证绕过/提权;FERNET 密钥已知 → 可解密所有用户存储的 API Key(auth.py);种子码可无限注册管理员;LLM Key 被盗用。
  • 复核补充(2026-07-14 主审逐行核验).envJWT_SECRET_KEY 实际出现两次(第 21、23 行),属配置冗余。若两处值不一致,会导致部分 token 签名/校验错位、登录随机失败且极难排查;也加大了密钥轮换时"漏改一处"的出错面。
  • 建议:立即轮换全部密钥;JWT/FERNET 改为随机生成、仅存于密钥管理/环境变量、绝不入库;当作"已泄露"处置;并删除 .env 中重复的 JWT_SECRET_KEY

1.2 SEED_INVITE 可无限复用提权(安全)

  • 位置app/api/auth.py:141-142 _consume_invite
  • 影响:命中即 (True, True)(is_admin=True),无消费计数、无有效期、无审计日志。
  • 建议:改为一次性或删除;管理员开通走独立受控流程;任何提权动作写审计日志。

1.3 前缀白名单鉴权模型导致未鉴权端点(安全 / 架构)

  • 位置app/main.py:123 protected_paths + 路由挂载
  • 影响(已逐行确认)
    • /api/caredeep_chat.py:704)不在白名单 → 完全未鉴权send_caresomatic_manager.get(op.name) 操作全局共享躯体状态(warmth/comfort/social_availability),未登录即可篡改任意干员状态(deep_chat.py:709 默认 username='')。
    • /api/sui-fragments/*easter_egg_api.py,前缀 /api)不在白名单(白名单写的是 /api/easter-eggs)→ 实质未鉴权。
    • 模型违反 fail-closed:新增路由若未同步加白名单即默认公开,是系统性风险。
  • 建议:废除前缀白名单,改用 FastAPI 依赖注入 Depends(require_auth) 统一校验(默认拒绝);立即把 /api/care/api/sui-fragments/* 纳入鉴权。

1.4 operator_prefs 路由前缀错位 → 功能失效(功能 + 鉴权)

  • 位置app/main.py:46 app.include_router(operator_prefs.router) 漏写 prefix="/api"
  • 影响:路由实际为 /operator-avatars/{id}/avatar-pref,既不匹配任何受保护前缀(未鉴权),又因中间件不解码 token 使 username 永远为空 → 读写皆 no-op;前端按 /api/operator-avatars 访问会 404。双重破窗。
  • 建议:补 prefix="/api" 并加一条前后端路径一致性测试。

1.5 L3 长期记忆维度契约不一致 → 检索静默失效(数据正确性)

  • 位置app/systems/memory/l3_long.py:158-166(写入 512+3=515d) vs coordinator.py:152,157(查询 384+3=387d
  • 影响:ChromaDB querydimensionality mismatch,被 get_scored_candidates 的 except 吞掉返回 []L3 长期事实在线上永远不会被注入。即便去掉 +3,512 vs 384 仍冲突。
  • 建议:L3 存储与查询必须统一 output_dim(建议统一到 EMBEDDING_DIM_RETRIEVAL);时间特征要么双侧都拼、要么都不要;provider 切换时强制重建/迁移向量库。

1.6 retrieve_zscore 缓存漏 username → 跨用户记忆泄漏(隐私隔离)

  • 位置app/systems/memory/coordinator.py:116 cache_key = f"{operator_name}:{query_text[:80]}"
  • 影响:同干员 + 相同查询前缀时,用户 A 的 L2/L3 事实与原始对话会被返回给用户 B。
  • 建议:缓存键必须包含 request_username.get()(降级值也显式写入)。

1.7 LLM 摘要失败后原始对话被永久删除(数据完整性)

  • 位置app/api/chat.py summarize_and_extract_task 末尾无条件 trim_messages(keep_target=20)l1_short.py:401-431
  • 影响:若 LLM 摘要/抽取 JSON 解析失败(402/429/网络抖动/命中禁止句式),chunk 无 summary → 其原始消息被清掉且未写入 L2/L3,整段历史静默丢失,无备份。
  • 建议:仅当 chunk 成功写入 summary 后才允许 trim;或失败时对未摘要 chunk 保留原始消息。

2. P1 — 高优先级(正确性与并发)

2.1 叙事事件链状态机卡死(数据正确性)

  • 位置app/systems/narrative/engine.py:1250-12581354-1356 resolve_pending_events / _advance_chain
  • 影响:内层 _advance_chain 已把链下一阶段落盘,外层循环结束又 _save_moments(moments)(不含 new_entry)覆盖 → chain_stage 永远停在 1,事件链特性实质失效。
  • 建议_advance_chain 原地修改并返回 new_entry,由外层统一并入后单次落盘。

2.2 systems → api 反向依赖(架构违规)

  • 位置app/systems/narrative/engine.py:15 from app.api.auth import get_user_api_key, get_user_base_url
  • 影响:违反三层依赖方向,叙事引擎无法独立测试/复用。
  • 建议:将"取用户 API Key/BaseURL"下沉到 app/core(如 app/core/llm_config.py)。

2.3 并发写安全(多处模块级 dict / JSON 裸写)

  • 位置
    • app/systems/narrative/engine.py _save_moments(读-改-写不在同一锁)
    • app/systems/social/jealousy.py:295-296 _deliver_messagewrite_text(无锁/无原子),与 initiativelocked_atomic_save 混用
    • app/systems/operator/registry.py:13 / char_file.py:829 模块级 dict 缓存无锁;reload_manifest 原地清空无同步
    • app/systems/operator/char_file.py:464-468 用户覆盖层裸 write_text
    • app/systems/relation/graph.py:259-284 动态边每次全量重加载、非缓存、非线程安全
    • app/systems/memory/l3_long.py:218-230 upsert 的 get+delete 在 _write_lock 之外 → 并发 DuplicateID
  • 建议:统一封装带锁的 read-modify-write 助手(参考 file_utils.locked_atomic_save);为 L2 读写引入与 L3 同级的进程写锁;registry/char 缓存加 threading.Lock;L3 upsert 整体移入 _write_lock 或捕获 DuplicateID 降级为 upsert。

2.4 L2 请求路径写回与后台压缩互相覆盖(撕裂写)

  • 位置app/systems/memory/coordinator.py:401-403 + chat.py:731,789
  • 影响processing_lock 只挡压缩重入,挡不住请求路径回写;两段 RMW 交错会丢失对方刚写的切片/字段。
  • 建议:请求路径回写与后台写入共用同一把进程写锁,或把回写合并到后台压缩任务。

2.5 async 路径同步阻塞 embedding/IO(性能)

  • 位置app/api/chat.py:934,942 retrieve_zscore / compute_drift 直接同步调用(含网络/本地推理);群聊与 echo_detector 同样同步 get_embedding
  • 影响:作者已把 retrieve_world_context/get_embedding 用线程池卸载(920/924),却漏了这两个 → embedding 慢时阻塞整个事件循环,拖垮全站并发。
  • 建议:将 retrieve_zscore/compute_drift/check_echoing 经共享 executor(run_in_executor)执行。

2.6 SSRF 防护形同虚设(安全)

  • 位置app/core/llm_client.py:130-149 _validate_api_url
  • 影响_BLOCKED_PREFIXES 全是 http://...,但合法 URL 必为 https → 对内网 https(https://127.0.0.1https://10.xhttps://***.***.***.***(已隐去))完全不拦截;_ALLOWED_HOSTS 用子串匹配可被 api.openai.com.attacker.net 绕过。base_url 用户可控 → 可探测内网/云元数据。
  • 建议:解析 URL 后用 ipaddress 判定 IP 不在私网/环回/链路本地;主机白名单用精确相等/后缀;加 DNS 重绑定防护。

2.7 远程 LLM 调用无超时/无重试(可用性)

  • 位置app/core/llm_client.py:355(非流式远程)、:403(流式远程);本地分支 :306timeout=LOCAL_LLM_TIMEOUT_S
  • 影响:远程分支 client.chat.completions.create(...) 未传 timeout,上游卡死时连接挂起至 SDK/OpenAI 默认 ~600s;无 429/5xx 重试退避。
  • 复核补充(2026-07-14 主审逐行核验):确认远程两处 create(...) 均无 timeout= 形参,仅本地 llama 分支(:306)传入 LOCAL_LLM_TIMEOUT_S,论断属实。
  • 建议:显式 timeout;429 指数退避;5xx 有限次降级重试;区分可重试/不可重试错误。

2.8 越权信息泄露(安全)

  • 位置app/api/memory.py:286-477 /health/dashboard 仅需登录(任意普通用户)即返回跨用户聚合诊断
  • 建议:增加 _require_admin;或收窄到当前用户维度。

3. P2 — 中优先级(健壮性与一致性)

  • 3.1 embedding 维度无单一真相源.env 模型为 384d(非 Matryoshka),代码注释写 BGE-M3;zhipu(1024d) 切换时 ChromaDB 维度错配;对 256d 截断在非 Matryoshka 模型上是语义破坏。建议以模型实际输出维度为准并校验。
  • 3.2 /register 无限流app/api/auth.py:308-341,邀请码可被爆破;建议接入限流 + 失败计数锁定。
  • 3.3 限流器非原子、进程内单点app/core/rate_limit.py:12-24 计数+append 无锁、多 worker 不共享;DEFAULT_LIMIT=30 与 main.py 注释"20 次/分钟"不一致。建议加锁 + Redis 分布式;统一常量。
  • 3.4 JWT 无 aud/iss、不可吊销、密钥读取逻辑重复app/core/auth_security.pyapp/config.py 各一套;依赖 import 顺序加载 dotenv。建议合并唯一来源 + 加 aud/iss/jti + 撤销列表 + 缩短过期。
  • 3.5 runtime_config 热更新名不符实、用户缓存无锁无上限app/core/runtime_config.py:21-40。建议加 mtime 失效 + 锁 + 上限。
  • 3.6 admin 端点 username 未做格式校验 + _scan_all_conversations 无分页/上限app/api/admin.py(DoS 面)。建议白名单校验 + 分页/时间窗。
  • 3.7 /api/reload-manifest/api/agent/status 未鉴权app/main.py:108,171。建议 reload 加 admin、status 加鉴权或移调试路由。
  • 3.8 CORS 默认通配 + token 经 query 传递app/main.py:33,128-131。建议生产强制显式来源白名单;SSE 改用一次性短票据。
  • 3.9 traits 对 .char 干员静默失效app/systems/operator/traits.py:100 读已废弃的 system_prompt="" → 场景自适应人设名存实亡。建议改从 char_doc 抽取。
  • 3.10 评估器误导结论 + 标签错位evaluator.py LLM 全失败时判"全部通过";type_summary 用下标 i 查 label 错位。建议样本不足显式返回"不可用";用类型键查 label。
  • 3.11 回声检测算法不完整echo_detector.py 只比较当前轮、不用历史窗口;有 pass 死循环;字典序去重。建议对比历史窗口 + 删死代码 + 集合去重。
  • 3.12 用户名净化两套实现不一致app/core/paths.py:43-47 vs app/main.py:16-26。建议统一为一个复用函数;subdir 也做白名单净化。
  • 3.13 _require_admin 在 4 个文件重复定义:建议抽到 app/core/deps.py
  • 3.14 SSE system prompt 组装逻辑在 chat.py / deep_chat.py 大量重复:建议抽公共函数。

4. P3 — 低优先级(打磨)

  • /api/chat.py:926-928 预取 f.result() 未包裹异常 → 单点失败整轮 500(建议逐个 try/except 降级)。
  • app/utils/trust_parse.py:10 入参 None 会抛 AttributeError(建议入口加类型防护)。
  • app/systems/social/jealousy.py:172-200 每次对话 O(N) 全量扫描干员(建议缓存 + 预筛)。
  • app/systems/relation/graph.py:237-246 博士关系网仅启动全量刷新一次(基本可用,非强一致)。
  • app/systems/operator/registry.py:58 未知干员抛 KeyError(建议自定义异常由上层统一处理)。
  • app/systems/push/{bark,ntfy}.py 失败吞异常无重试(best-effort 可接受,可酌情加 1 次重试)。
  • logger 默认 DEBUG 全量落盘 + chat() 回显原始错误给用户(建议生产 INFO;不回显原始错误)。
  • app/systems/narrative/scheduler.py:33 import 时 logging.basicConfig(force=True) 全局重配(副作用)。
  • Ebbinghaus 衰减按整数天(亚日无衰减)→ 改用浮点天。
  • 模块级缓存无 LRU 上限(L1 _data_cache/_recent_user_embs_retrieve_cache)→ 长期运行内存增长。

5. 优先级修复路线图(建议)

阶段 范围 关键项
P0 立即(安全+数据) 上线前必须做 1.1 轮换密钥 / 1.2 种子码一次性化 / 1.3 依赖注入鉴权+修 /api/care / 1.4 operator_prefs 前缀 / 1.5 L3 维度统一 / 1.6 缓存键加 username / 1.7 摘要失败不丢历史
P1 本周 正确性与并发 2.1 事件链卡死 / 2.2 依赖下沉 / 2.3 并发写锁 / 2.4 L2 撕裂写 / 2.5 async 阻塞卸载 / 2.6 SSRF / 2.7 LLM 超时重试 / 2.8 admin 越权
P2 两周内 健壮性与一致性 3.1-3.14 维度真相源、限流分布式、JWT 增强、热更新、路径校验、去重代码
P3 持续 打磨 异常包裹、解析防护、扫描优化、日志级别、缓存上限

一句话总结:架构设计扎实,但"鉴权用前缀白名单而非默认拒绝""密钥明文入库""L3 维度契约不一致""跨用户缓存键缺失""LLM 失败即丢历史"这五处,会让项目在安全性与核心记忆能力上静默失效——应先于一切性能优化完成 P0/P1 修复。


6. 复核补遗(2026-07-14 主审逐行核验)

本轮对报告中的关键 P0/P1 论断做了逐行复核(只读),确认全部属实,并将复核中发现的两处新问题正式记入正文(见 1.1、2.7)。

6.1 复核新发现 A — .envJWT_SECRET_KEY 重复定义(配置冗余 / P1)

  • 位置.env 第 21、23 行均出现 JWT_SECRET_KEY=
  • 影响:同一配置项定义两次,加载时以后者为准,前者成为"死配置"。风险:① 密钥轮换时极易漏改一处,导致新旧 token 混用、登录随机失败;② 若两处值被意外改成不同,签名/校验错位极难排查。属配置治理缺陷。
  • 建议:删除重复行,仅保留一个 JWT_SECRET_KEY;在启动脚本中加"同 key 不重复"校验。

6.2 复核新发现 B — 远程 LLM 调用确无 timeout(印证 2.7 / P1)

  • 位置app/core/llm_client.py:355(非流式)、:403(流式)的 client.chat.completions.create(...)
  • 影响:与 2.7 论断一致——远程分支未传 timeout,仅本地 llama 分支(:306)有 timeout=LOCAL_LLM_TIMEOUT_S。上游无响应时连接挂起至 SDK 默认 ~600s,会拖垮 worker 连接池。
  • 建议:见 2.7。

6.3 复核结论(已逐行确认属实的论断)

  • 1.1 硬编码弱密钥(JWT/FERNET/SEED)值已核对一致;
  • 1.2 SEED_INVITE 可无限复用(auth.py:_consume_invite 命中即 (True,True));
  • 1.3 /api/care 不在 protected_paths 白名单 → 未鉴权改全局状态;
  • 1.4 operator_prefs 路由漏 prefix="/api"main.py:46);
  • 1.5 L3 维度契约错配:写入 515d(l3_long.py:166)vs 查询 387d(coordinator.py:157);
  • 1.6 retrieve_zscore 缓存键漏 usernamecoordinator.py:116);
  • 2.6 SSRF 校验仅拦 http://、白名单用子串匹配(llm_client.py:130-149);
  • 2.7 远程 LLM 无 timeout(见 6.2)。