fix: 品牌下发审查修复(esc统一/语料中性化/穿越加固/反向断言动态化)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 22:52:08 +08:00
parent bbc7bc520e
commit 7697673a3b
8 changed files with 137 additions and 34 deletions
+42 -1
View File
@@ -6,8 +6,22 @@
- 响应头 Access-Control-Allow-Origin: *(挂件嵌在客户站,跨源加载);
- ?site=<slug> 站点覆盖(空域名站点如 es 靠嵌入代码 data-site 接入),
未知 site 回退租户默认门面;
- 头像按租户解析 seed_profiles/<slug>/assets/,两租户各归各。
- 头像按租户解析 seed_profiles/<slug>/assets/,两租户各归各;
- avatar 文件名解析拒绝路径穿越(PurePosixPath 不认反斜杠,Windows 下
反斜杠相对路径会真穿出资产目录 serve 任意文件)。
"""
import json
import shutil
from pathlib import Path
from tests.conftest import (
JIANCAI_HOST,
SEED_DIR,
app_test_client,
build_app,
)
REPO = Path(__file__).resolve().parents[1]
BRAND_EXPECTED = {"slug", "title", "color", "avatar", "welcome", "topics",
"placeholder", "whatsapp", "email", "privacy_url"}
@@ -44,3 +58,30 @@ def test_avatar_served_per_tenant(app_client):
r_jc = app_client.get("/api/public/avatar") # base_url = jiancai 域
assert r_jc.status_code == 200
assert r.content != r_jc.content
def test_avatar_rejects_windows_backslash_traversal(tmp_path):
"""复审加固: brand.avatar 含反斜杠/斜杠/. 或 .. 时必须回退静态兜底。
PurePosixPath 把 "..\\..\\..\\secret.txt" 当单个组件留在 .name 里,
Windows 文件系统却会把反斜杠当分隔符——不加守卫时
seed_dir/assets/../../..\\secret.txt 真解析到资产目录外(serve 任意
文件)。目标文件真实存在于穿越终点,守卫失效即红。"""
seed = tmp_path / "seed"
shutil.copytree(SEED_DIR, seed)
tenant_json = seed / "jiancai" / "tenant.json"
data = json.loads(tenant_json.read_text(encoding="utf-8"))
data["config"]["brand"]["avatar"] = "..\\..\\..\\secret.txt"
tenant_json.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
# 穿越终点: <tmp>/seed/jiancai/assets 起三个 ".." → <tmp>/secret.txt
secret = tmp_path / "secret.txt"
secret.write_bytes(b"TOPSECRET-should-never-be-served")
a = build_app(seed, tmp_path / "data")
with app_test_client(a, f"http://{JIANCAI_HOST}", tmp_path) as c:
r = c.get("/api/public/avatar")
assert r.status_code == 200
assert r.headers["content-type"].startswith("image/")
assert b"TOPSECRET" not in r.content # 穿越目标不被 serve
fallback = (REPO / "static" / "kefu-small.png").read_bytes()
assert r.content == fallback # 落静态兜底头像