Skip to content

Commit 02cf26d

Browse files
committed
fix(security): 消除 CodeQL 全部 10 个开放告警
- font_path 先归一 \ 与 / 分隔符再用 os.path.basename 剥目录成分,跨平台拒绝 ..\x.ttf 这类名字,修 3 处 py/path-injection - _LINE_RE 与学年/年度清理改写为 possessive/原子组等价形式,超长行最坏耗时降到线性,修 2 处 py/polynomial-redos - 数据集导入的边缘失败提示只回异常类型名,原文留在服务端日志,修 py/stack-trace-exposure - _SCRIPT_RE 兼容 </script > 结束标签,修 py/bad-tag-filter - 前端守卫测试 ../ 前缀剥离改全局替换、RegExp 转义补齐反斜杠,修 3 处 js/incomplete-sanitization
1 parent 9f5019e commit 02cf26d

9 files changed

Lines changed: 28 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@
433433

434434
### Changed
435435

436+
### Security
437+
438+
- **消除 CodeQL 静态扫描的全部 10 个开放告警。** 字体文件名校验改用 `os.path.basename` 并先归一 `\` / `/` 两种分隔符再剥目录成分(`..\\x.ttf` 这类跨平台写法现在一律拒收,合法字体名行为不变);网申资料粘贴识别与经历头解析的两条正则改写为无多项式回溯的等价形式(超长行的最坏耗时从平方级降到线性,正常输入的解析结果逐字不变);数据集导入时「全局模型配置 / 字体未能导入」的提示只回异常类型名,异常原文(可能含本机路径)只进服务端日志;导出一致性测试的 `<script>` 过滤正则兼容 `</script >` 这类带空白的结束标签;前端守卫测试里两处只替换首次出现的 `../` 改为全局替换、一处构造 `RegExp` 前的转义补齐反斜杠。
439+
436440
## 1.0.2 - 2026-10-18
437441

438442
### Added

‎backend/app/api/datasets.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async def import_archive(request: Request) -> dict:
180180
created["global_llm"] = merge_exported_profiles(global_payload)
181181
except Exception as exc: # noqa: BLE001 - 边上信息,不能扩大失败面
182182
logger.warning("备份包里的全局模型配置未能导入:%s", exc)
183-
created["global_llm"] = {"error": str(exc)}
183+
created["global_llm"] = {"error": f"全局模型配置未能导入({type(exc).__name__})"}
184184
# 字体(格式 6)同理:属于"数据集之外"的全局资产,恢复失败**不回滚**已经落盘
185185
# 的数据集,只把计数与原因如实附在返回里——用户至少能看出字体没完全回来。
186186
try:
@@ -196,7 +196,7 @@ async def import_archive(request: Request) -> dict:
196196
created["font_issues"] = fonts["issues"]
197197
except Exception as exc: # noqa: BLE001 - 字体是附加物,不能扩大失败面
198198
logger.warning("备份包里的字体未能导入:%s", exc)
199-
created["font_issues"] = [f"字体未能导入({exc})"]
199+
created["font_issues"] = [f"字体未能导入({type(exc).__name__})"]
200200
except HTTPException:
201201
staging.unlink(missing_ok=True)
202202
raise

‎backend/app/services/profile_parser/entry_blocks.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _parse_header(line: str) -> tuple[list[str], str, str]:
187187
# 只摘**独立成词**的学年/年度(后面跟空白或行尾),避免误伤以这两个字
188188
# 开头的组织名。
189189
clean = re.sub(r"^(?:学年|年度)(?=\s|$)\s*+", "", clean)
190-
clean = re.sub(r"\s+(?:学年|年度)$", "", clean)
190+
clean = re.sub(r"\s++(?:学年|年度)$", "", clean)
191191
# 日期已先移除,因此此处可以兼容未加空格的 ``公司-岗位-时间`` 格式,
192192
# 又不会把年月中的连字符切开。宽空白/制表符也是列分隔符(截图抄录的
193193
# 对齐排版),每个部件再按宽空白拆开。

‎backend/app/services/user_fonts.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import json
19+
import os
1920
from datetime import datetime
2021
from pathlib import Path
2122
from typing import Any
@@ -75,15 +76,19 @@ def _read_names() -> dict[str, str]:
7576
def _write_names(names: dict[str, str]) -> None:
7677
directory = fonts_dir()
7778
directory.mkdir(parents=True, exist_ok=True)
78-
_names_file().write_text(
79-
json.dumps(names, ensure_ascii=False, indent=2), encoding="utf-8"
80-
)
79+
_names_file().write_text(json.dumps(names, ensure_ascii=False, indent=2), encoding="utf-8")
8180

8281

8382
def font_path(name: str) -> Path:
84-
"""按文件名取字体路径(拒绝任何目录成分,调用方不必再拼路径)。"""
85-
safe = Path(name).name
86-
if not safe or safe != name or safe == ACTIVE_FILENAME:
83+
"""按文件名取字体路径(拒绝任何目录成分,调用方不必再拼路径)。
84+
85+
分隔符先归一再取末段(``os.path.basename`` 是 CodeQL 认可的目录成分剥离):
86+
Windows 上 ``\\`` 也是分隔符,只认 ``/`` 会把 ``..\\x.ttf`` 这类名字在 POSIX
87+
上当普通文件名放过去。``safe != name`` 一道闸把任何带目录成分的名字全拒掉。
88+
"""
89+
normalized = name.replace("\\", "/")
90+
safe = os.path.basename(normalized)
91+
if not safe or safe in {".", ".."} or safe != normalized or safe == ACTIVE_FILENAME:
8792
raise FontError("字体文件名不合法")
8893
path = fonts_dir() / safe
8994
if not path.is_file():

‎backend/app/services/webform/extra_profile_text.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,12 @@
5858
# 「有没有可用的文字」的判据:归一化后还剩多少字母/数字/汉字。
5959
# 纯标点、「***」、「:」这类没有名字的"栏目"在界面上没法显示,也就没法勾选。
6060
_MEANINGFUL_RE = re.compile(r"[0-9a-z\u4e00-\u9fff]")
61-
# 一行一条 `标签:值`(中文冒号、英文冒号、制表符都认)。
62-
_LINE_RE = re.compile(r"^\s*([^::\t]{1,80})\s*[::\t]\s*(.+?)\s*$")
61+
# 一行一条 `标签:值`(中文冒号、英文冒号、制表符都认)。标签段用 possessive
62+
# 量词、分隔符前后空白原子化,压掉 group×`\s*` 的多项式回溯;分隔符前的原子组
63+
# 必须排除 `\t`(``[^\S\t]`` = 空白但非制表符)——否则制表符作分隔符时会被原子
64+
# 组先吞掉且不回退,整行匹配失败。尾部 ``(.*\S)`` 从右往左定位最后一个非空白,
65+
# 替代惰性 ``.+?`` 逐位重扫尾随空白的二次回溯。
66+
_LINE_RE = re.compile(r"^\s*([^::\t]{1,80}+)(?>[^\S\t]*)[::\t](?>\s*)(.*\S)\s*$")
6367

6468
# 目录 label → (key, 是否属于网申资料)。**整个表单目录**都进索引,不只是 extra 那 63 项:
6569
# 「姓名」「手机号」这类属于**简历资料**的栏目必须被认出来并**丢掉**——它们不是"网申的新栏目",

‎backend/tests/test_rich_export_consistency.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _pdf_text(content: bytes) -> str:
7777
# ===== HTML =====
7878

7979

80-
_SCRIPT_RE = re.compile(r"<script[\s\S]*?</script>", re.IGNORECASE)
80+
_SCRIPT_RE = re.compile(r"<script[\s\S]*?</script\s*>", re.IGNORECASE)
8181

8282

8383
@pytest.mark.parametrize("template", sorted(RESUME_TEMPLATES))

‎frontend/src/features/assistant/components/AssistantEmptyState.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function renderEmptyState() {
3333

3434
/** 从样式表源码里取某个类选择器规则的颜色声明(可能是 var() 引用)。 */
3535
function declaredColor(selector: string) {
36-
const escaped = selector.replace(/\./g, "\\.");
36+
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3737
const block = accessibilityStyles.match(new RegExp(`${escaped}[^{]*\\{([^}]*)\\}`));
3838
const color = block?.[1].match(/color:\s*([^;]+);/);
3939
return color ? color[1].trim() : "";

‎frontend/src/theme/colorGuard.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe("颜色守卫", () => {
5959
// 唯一的白字例外:`color: #fff` / `color: #ffffff`。
6060
if (property === "color" && /^#(fff|ffffff)$/i.test(match[2].trim())) return;
6161
offenders.push({
62-
file: file.replace("../", ""),
62+
file: file.replace(/\.\.\//g, ""),
6363
line: index + 1,
6464
text: text.trim(),
6565
});

‎frontend/src/theme/inlineColorGuard.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe("内联样式颜色守卫", () => {
132132
const offenders: Offender[] = [];
133133

134134
for (const [key, source] of Object.entries(SOURCE_FILES)) {
135-
const file = key.replace("../", "");
135+
const file = key.replace(/\.\.\//g, "");
136136
if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(file))) continue;
137137

138138
for (const region of styleRegions(source)) {

0 commit comments

Comments
 (0)