fix: 第12轮搜索审计修复 — _normalize_field_label 崩溃/keyset NULLSLAT/共享列表变异等21项修复
P0 (3): _normalize_field_label 函数缺失导致 (a OR b)[TI] 崩溃;
共享列表变异污染 result.groups;POST /search/advanced 缺少用户认证
P1 (8): has_not 忽略括号内 NOT;紧凑日期 YYYYMMDD 未归一化;
日期字段非日期文本 SQL 错误;keyset NULLSLAT ~50% 空值行跳过;
first_author 非 dict JSON 崩溃;MeSH 无匹配静默丢弃;
相关性排序含否定词;词数检查在 PubMed 清洗之前;
普通搜索缺错误处理;#N 引号感知不完整
P2 (6): 月越界退化;MH/MAJR FALSE 语义;field 白名单缺字段;
retracted/negative_result/tag_ids 验证器;resolveQuery 重复调用;
SearchRequestBody.page 可选;restoreFromQuery showCustomYear
This commit is contained in:
@@ -919,6 +919,147 @@
|
||||
|
||||
---
|
||||
|
||||
## 第十二轮:第 12 轮深度审计修复(21 项)
|
||||
|
||||
**日期**:2026-07-28
|
||||
**提交**:`NEXT_COMMIT`
|
||||
**数量**:21 项(2 P0 + 8 P1 + 6 P2 + 5 前端/类型)
|
||||
**触发**:用户第 7 次要求全面检查
|
||||
**测试**:1007 全部通过 + 前端 build 通过
|
||||
|
||||
### P0-1: `_normalize_field_label` 函数缺失导致 `(a OR b)[TI]` 崩溃(CRITICAL)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:619`
|
||||
- **根因**:`_parse_primary` 对括号组后带字段标签的语法 `(a OR b)[TI]` 调用 `_normalize_field_label(_raw_field)`,但此函数从未定义 → `NameError`。此语法在 PubMed 官方合法,表现为"将整个括号组的搜索结果在 TI 字段中再过滤"
|
||||
- **修复**:新增 `_normalize_field_label()` 函数,通过 `_FIELD_TAG_MAP` 和 `_SPECIAL_FIELDS` 查找标签映射,`MH:NOEXP` 特殊处理返回 `"MH"`
|
||||
- **验证**:`(lung OR breast)[TI]` 不再崩溃
|
||||
|
||||
### P0-2: 共享列表变异导致 `result.groups` 被污染(CRITICAL)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:577-587`
|
||||
- **根因**:`_parse_and_expr` 中 `left = self._parse_not_expr(result)` 返回的是 `result.groups` 中**同一个 Python list 对象**的引用。随后 `left.extend(right)` 直接修改了 `result.groups` 中存储的列表,导致后续遍历时出现重复/错乱项
|
||||
- **修复**:改为 `left = list(self._parse_not_expr(result))` — 创建副本后再 extend
|
||||
- **影响**:修复 `(A OR B) AND C` 类查询中 `result.groups` 被意外修改的 bug
|
||||
|
||||
### P0-3: `POST /search/advanced` 缺少用户认证(CRITICAL)
|
||||
|
||||
- **文件**:`features.py:278-283`
|
||||
- **根因**:高级搜索端点只声明了 `Depends(get_db)`,没有 `Depends(get_current_user)`。虽然多租户隔离在 `get_current_user` 中设置,`AdvancedSearchEngine.search` 内部不依赖 user 参数,但此端点可被未认证用户调用,且缺少统一的审计入口
|
||||
- **修复**:添加 `user: dict = Depends(get_current_user)` 参数
|
||||
- **影响**:高级搜索端点与普通搜索端点(`literature.py`)认证策略一致
|
||||
|
||||
### P1-1: `has_not` 忽略括号内 NOT 词(HIGH)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:345-347`
|
||||
- **根因**:`has_not` 属性只检查 `_ungrouped`(`group_id < 0` 的顶级词)。`NOT (A OR B)` 时 `a.is_not=True` 正确设置,但词属于 group,不在 `_ungrouped` 中 → `result.has_not = False`
|
||||
- **修复**:添加 `or any(t.is_not for g in result.groups for t in g)` 检查所有分组内的 `is_not`
|
||||
- **验证**:`NOT (cancer OR tumor)[TI]` 的 `has_not` 从 False 修正为 True
|
||||
|
||||
### P1-2: 紧凑日期 `YYYYMMDD` 未归一化(HIGH)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:736-749`
|
||||
- **根因**:`_parse_range` 中的日期格式处理只支持 `YYYY-MM-DD` 和 `YYYY/MM/DD` 等含分隔符的格式。PubMed 官方支持 8 位紧凑格式 `20240115[DP]`,原代码直接传递给 SQL → 类型不匹配错误
|
||||
- **修复**:新增正则检测 `^\d{8}$` 的紧凑日期值,自动归一化为 `YYYY-MM-DD`
|
||||
- **验证**:`20240115[DP]` 正确解析为 `2024-01-15`
|
||||
|
||||
### P1-3: 日期字段传入非日期文本导致 SQL 错误(HIGH)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:760-775`
|
||||
- **根因**:`abc:def[DP]` 被拆分为 `abc` 和 `def` 两个 Term,后续直接拼接 SQL 范围查询 → `invalid input syntax for type date` 错误。第三方 API 或其他系统误传非日期内容到日期字段时崩溃
|
||||
- **修复**:新增 `_valid_date()` 函数校验日期合法性(格式 + 月份/日范围);无效值回退为普通文本 `Term`(字段标签变为普通搜索词),不抛出异常
|
||||
- **验证**:`abc:def[DP]` 不再崩溃,退化到文本搜索
|
||||
|
||||
### P1-4: Keyset NULLSLAT 过渡导致 ~50% 空值行跳过(HIGH)
|
||||
|
||||
- **文件**:`search_engine.py:1637-1681`
|
||||
- **根因**:每个 sort 分支的第三 ORDER BY 子句 `nullslast()` 对应的 `_keyset_condition` 生成 `and_(col.is_(None), id < cursor_id)`。随机 UUID 无排序语义,`id < cursor_id` 条件会过滤掉约 50% 的 NULL 行
|
||||
- **修复**:三级 keyset 条件移除 `id` 约束,仅保留 `col.is_(None)`
|
||||
- **影响**:修复后排序列为 NULL 的行不再被随机跳过,翻页结果完整
|
||||
|
||||
### P1-5: `_cursor_from_item` first_author 对非 dict JSON 报错(HIGH)
|
||||
|
||||
- **文件**:`search_engine.py:1703`
|
||||
- **根因**:`authors[0].get("family")` 假设 `authors[0]` 是 dict。当 `authors` JSON 数组包含非 dict 值(如 `null` 或字符串)时 → `AttributeError: 'NoneType' object has no attribute 'get'`
|
||||
- **修复**:添加 `if authors and isinstance(authors[0], dict):` 保护,否则返回 `"__NULL__"`
|
||||
- **验证**:`authors: [null]` 或 `authors: ["Molnar, V"]` 不再崩溃
|
||||
|
||||
### P1-6: `_expand_mesh_tag_ids` 返回 None 时条件静默丢弃(HIGH)
|
||||
|
||||
- **文件**:`search_engine.py:856-882, 1280-1282`
|
||||
- **根因**:`_expand_mesh_tag_ids()` 未找到匹配的 MeSH 词时返回 `None`。调用方直接将 `None` 追加到 `term_conditions` 列表 → 等同于忽略此搜索条件。用户搜索一个不存在/未收录的 MeSH 词时无任何反馈,隐式返回全部文献
|
||||
- **修复**:当 `cond is None` 且 `not is_neg` 时,追加 `text("FALSE")`(无匹配 = 零结果,正确语义)。NOT 路径下 `None` 仍然合法(否定一个不存在的 MeSH = 全部通过)
|
||||
- **验证**:`nonexistent_mesh[MeSH]` 不再返回全部文献,返回零结果
|
||||
|
||||
### P1-7: `_relevance_query` 包含否定词(HIGH)
|
||||
|
||||
- **文件**:`search_engine.py:608-611`
|
||||
- **根因**:构建 `plain_parts` 时遍历所有 `terms` 未过滤 `t.is_not`。否定词 "NOT X" 中的 X 被纳入相关性排序 tsquery → 相关性分数被不应出现的否定词影响
|
||||
- **修复**:四个 `plain_parts.append` 路径全部添加 `if not t.is_not` 过滤
|
||||
|
||||
### P1-8: 普通搜索 PubMed 语法检测在词数限制之后(HIGH)
|
||||
|
||||
- **文件**:`literature.py:275-332`
|
||||
- **根因**:`len(q.split()) > 100` 的词数检查在 `is_pubmed_syntax()` 解析/清洗之前。PubMed 带字段标签的查询 `cancer[TI] OR tumor[TI] OR ...` 虽然语义上只有少数真实词,但 `split()` 将每个 `cancer[TI]` 算作一词 → 密集字段标签查询被错误拒绝
|
||||
- **修复**:先执行 `is_pubmed_syntax()` 和 field tag 清洗,再检查清洗后的文本长度
|
||||
- **验证**:`cancer[TI] AND (lung[TI] OR breast[TI] OR colon[TI])`(清洗后仅 5 词)不再被误阻止
|
||||
|
||||
### P1-9: 普通搜索缺少错误处理(HIGH)
|
||||
|
||||
- **文件**:`literature.py:275-332`
|
||||
- **根因**:整个 DB 查询块无 try/except。搜索缓存 MISS + DB 故障时返回 500 给用户,前端无降级展示
|
||||
- **修复**:`try/except Exception` 包裹,返回 `{"items":[], "total":0, "error":"搜索服务暂不可用"}`
|
||||
- **影响**:用户可见的"搜索服务暂不可用"提示,而非白页或 500
|
||||
|
||||
### P1-10: `#N` 引用解析引号感知不完整(HIGH)
|
||||
|
||||
- **文件**:`AdvancedPubSearchView.vue:227-235`、`useSearchHistory.ts:28-35`
|
||||
- **根因**:`expandQuery()` / `resolveQuery()` 中 `/#(\d+)/g` 全局匹配未排除引号内的 `#N`。历史记录中 `"PD-1 #1 biomarker"` 的 `#1` 被错误展开
|
||||
- **修复**:替换为 `"[^"]*"|'[^']*'|#(\d+)` 正则,先匹配引号内容(直接返回原文),再匹配引号外的 `#N`
|
||||
- **验证**:`"mechanism #1" 和 "review #1"` 中的 `#1` 不再被展开
|
||||
|
||||
### P2-1: `_expand_partial_date` 月越界(LOW)
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:700-730`
|
||||
- **根因**:`YYYY-13` 这类非法月份传入 `_expand_partial_date` 后直接构建 `YYYY-13-01` → SQL 日期解析报错
|
||||
- **修复**:添加月份范围检查 `1 <= int(month) <= 12`;非法月份回退为全年范围 `YYYY-01-01` 到 `YYYY-12-31`
|
||||
|
||||
### P2-2: `_single_term_condition` MH/MAJR 返回 FALSE 而非 None
|
||||
|
||||
- **文件**:`search_engine.py:1280-1282`
|
||||
- **修复**:`_expand_mesh_tag_ids` 返回 None 时,MH/MAJR 返回 `text("FALSE")` 替代原先的 `None`,保持与 P1-6 一致的语义
|
||||
|
||||
### P2-3: `field` 验证器缺少 language/volume/issue/pages/lid
|
||||
|
||||
- **文件**:`features.py:108-114`
|
||||
- **根因**:`@field_validator('field')` 的白名单只包含 `all/title/abstract/author/affiliation/journal`,实际搜索引擎支持 `language/volume/issue/pages/lid` 的全路径搜索
|
||||
- **修复**:添加 `'language', 'volume', 'issue', 'pages', 'lid'` 到允许列表
|
||||
|
||||
### P2-4: `@field_validator` 缺失 `retracted`/`negative_result`/`tag_ids`
|
||||
|
||||
- **文件**:`features.py:116-140`
|
||||
- **根因**:高级搜索接口 `AdvancedSearchRequest` 模型未对枚举值 `retracted`(yes/no/only)和 `negative_result`(yes/no/only)做验证;`tag_ids` 未做 UUID 格式验证。异常值直接传入 DB 查询
|
||||
- **修复**:新增三个 `@field_validator`:`check_retracted`、`check_negative_result`、`check_tag_ids`
|
||||
|
||||
### P2-5: 高级搜索 `resolveQuery` 重复调用
|
||||
|
||||
- **文件**:`AdvancedPubSearchView.vue:307-323`
|
||||
- **根因**:`validateQuery` 内部先调用了一次 `resolveQuery`,外层 `expanded` 又调用一次。重复解析消耗性能且可能暴露循环引用漏洞
|
||||
- **修复**:`validateQuery` 返回解引用后的结果,外层复用
|
||||
|
||||
### P2-6: `SearchRequestBody.page` 声明为 required 但运行期删除
|
||||
|
||||
- **文件**:`types/index.ts:329`
|
||||
- **根因**:TypeScript 接口声明 `page: number`(必填),但 `features.py` 的 `get_search_cache_key` 在构建缓存键时对 `page=1` 调用 `del norm["page"]`。前端类型声明与后端实际行为不一致
|
||||
- **修复**:`page: number` → `page?: number`(可选)
|
||||
|
||||
### P2-7: `restoreFromQuery` 未设置 `showCustomYear`
|
||||
|
||||
- **文件**:`SearchView.vue`
|
||||
- **根因**:从 URL query string 恢复 `year_from` 和 `year_to` 时重置了筛选面板但忘记设置 `showCustomYear.value = true`,导致年份输入框不可见
|
||||
- **修复**:在 `year_from` 和 `year_to` 恢复路径后添加 `showCustomYear.value = true`
|
||||
|
||||
---
|
||||
|
||||
截至 2026-07-28,剩余 7 项已知限制:
|
||||
|
||||
| ID | 问题 | 原因 | 影响 |
|
||||
|
||||
Reference in New Issue
Block a user