PRTS 后端全量代码审计报告
BACKEND AUDIT
状态:🔴 历史 · 最后核对 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 主审逐行核验):
.env中JWT_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:123protected_paths+ 路由挂载 - 影响(已逐行确认):
/api/care(deep_chat.py:704)不在白名单 → 完全未鉴权。send_care用somatic_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:46app.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) vscoordinator.py:152,157(查询 384+3=387d) - 影响:ChromaDB
query抛dimensionality 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:116cache_key = f"{operator_name}:{query_text[:80]}" - 影响:同干员 + 相同查询前缀时,用户 A 的 L2/L3 事实与原始对话会被返回给用户 B。
- 建议:缓存键必须包含
request_username.get()(降级值也显式写入)。
1.7 LLM 摘要失败后原始对话被永久删除(数据完整性)
- 位置:
app/api/chat.pysummarize_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-1258与1354-1356resolve_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:15from 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_message裸write_text(无锁/无原子),与initiative的locked_atomic_save混用app/systems/operator/registry.py:13/char_file.py:829模块级 dict 缓存无锁;reload_manifest原地清空无同步app/systems/operator/char_file.py:464-468用户覆盖层裸write_textapp/systems/relation/graph.py:259-284动态边每次全量重加载、非缓存、非线程安全app/systems/memory/l3_long.py:218-230upsert 的 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,942retrieve_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.1、https://10.x、https://***.***.***.***(已隐去))完全不拦截;_ALLOWED_HOSTS用子串匹配可被api.openai.com.attacker.net绕过。base_url 用户可控 → 可探测内网/云元数据。 - 建议:解析 URL 后用
ipaddress判定 IP 不在私网/环回/链路本地;主机白名单用精确相等/后缀;加 DNS 重绑定防护。
2.7 远程 LLM 调用无超时/无重试(可用性)
- 位置:
app/core/llm_client.py:355(非流式远程)、:403(流式远程);本地分支:306有timeout=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.py与app/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.pyLLM 全失败时判"全部通过";type_summary用下标i查 label 错位。建议样本不足显式返回"不可用";用类型键查 label。 - 3.11 回声检测算法不完整:
echo_detector.py只比较当前轮、不用历史窗口;有pass死循环;字典序去重。建议对比历史窗口 + 删死代码 + 集合去重。 - 3.12 用户名净化两套实现不一致:
app/core/paths.py:43-47vsapp/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:33import 时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 — .env 中 JWT_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缓存键漏username(coordinator.py:116); - 2.6 SSRF 校验仅拦
http://、白名单用子串匹配(llm_client.py:130-149); - 2.7 远程 LLM 无
timeout(见 6.2)。