报告人:波波 | 时间:2026-09-24 18:5x | 状态:⏸️ 已取证完毕,等待裁定,prod 数据与代码零改动
严重等级:🟠 P1(对外形象事故,无资金损失)
关系:本报告升级并取代同日 18:2x 的《C端商品位展示测试套餐》报告——原报告只发现 1 个接口,
本次实测扫描发现 9 个,原报告的根因分析与处置建议仍然有效,但影响面需按下表重估。
| 项 | 结果 |
|---|---|
| C端 游客面 GET 列表接口总数 | 94 个 |
| 🔴 泄漏测试数据的 | 9 个 |
| ✅ 游客可达且干净 | 27 个(含我本轮做的 /fleet/routes、/retail/products) |
| 🔒 需鉴权(游客不可达,非泄漏面) | 57 个 |
| ⚠️ 异常 | 1 个(/hotel/availability 需必填参数,422) |
| 资金影响 | 无(真实用户订单 0、核销 0) |
| 泄漏接口中属我代码的 | 0 个 —— 全部跨域,故我未改任何代码 |
按响应中测试标记出现次数排序(次数≈污染程度):
| # | 接口 | 标记出现次数 | 泄漏内容实例 | 代码位置 | 归属 |
|---|---|---|---|---|---|
| 1 | GET /api/miniapp/promoters | 168 | 推广员列表含测试用户 | promoters.py:8 | 渔佳新媒体 |
| 2 | GET /api/miniapp/products | 150 | 「测试套餐001」¥288 等 70 条 | miniapp_products.py:93 | 小齐 |
| 3 | GET /api/miniapp/packages | 140 | 「测试套餐001」tag_1 等 | packages.py:84 | 渔佳新媒体 |
| 4 | GET /api/miniapp/products/curated | 60 | 甄选页 = 测试套餐001~020 | miniapp_products.py:112 | 小齐 |
| 5 | GET /api/miniapp/tasks | 33 | UGC 任务含冒烟数据 | tasks_api.py:29 | 渔佳新媒体 |
| 6 | GET /api/miniapp/content/guide-articles | 30 | slug=test_seed_story_020 等 20 篇 | feed_demand.py:389 | 小冈 |
| 7 | GET /api/miniapp/merchants | 27 | 33 家商户中 26 家名称含「测试」 | miniapp_merchants.py:89 | 小齐 |
| 8 | GET /api/miniapp/products/featured | 18 | 首页推荐位 = 测试套餐001~006 | miniapp_products.py:106 | 小齐 |
| 9 | GET /api/miniapp/recommendations/combo-cards | 8 | 组合卡 pkg-7001「测试套餐001」 | feed_demand.py:108 | 小冈 |
products/featured → v1 小程序已过审上线,home.ts:137 消费它 ⇒ 线上真实游客打开首页,推荐位是「测试套餐001 ¥288」merchants → 商户列表是 C端 核心页,游客看到 26 家「测试商户0XX号」guide-articles → 内容页 slug 直接是 test_seed_story_020,一眼假动态扫描 prod 84 张含 is_test 列的表:
| 指标 | 数值 |
|---|---|
含 is_test 列的表 | 84 |
其中存有 is_test=TRUE 数据 | 39 |
| 其中「整表全是测试数据」 | 29 ← 一旦 C端 不过滤就是整表泄漏 |
29 张全测试表包括:packages orders(138) order_items(174) merchants(部分) menu_items reviews(40) stories(20) guide_articles(20) guide_profiles app_messages(52) commission_entries(50) distribution_records(50) redemption_codes(95) redemption_events(95) key_tasks(20) key_task_claims(60) merchant_bills(20) withdrawal_requests(20) travel_demands(15) distributors(10) guest_bindings(30) 等。
数据来源:backend/scripts/seed_demo_data.py(WIP-285 演示种子脚本,设计要点第 7 条明写
「全量 is_test=true,与真实业务物理隔离」)。脚本 DATABASE_URL 默认指向 staging,
prod 有这批数据推断是有人显式指向 prod 跑的(代码无法确定是谁/何时/为何)。
⚠️ 种子脚本本身没有错——它正确地打了 is_test=TRUE。
错在 C端 查询侧不认这个标记,导致"物理隔离"只停留在数据层,没有落到接口层。
is_test以 #7 为例(miniapp_merchants.py:89,作者小齐):
rows = await pool.fetch(
_BASE_SQL + " WHERE m.status = 'active' "
"AND (m.destination_id = $1 OR m.destination_id IS NULL) ORDER BY m.id", ...)
只过滤 status,没有 is_test = FALSE。
对照:我本轮做的零售/车队 C端 只读接口都写了 is_test=FALSE 并做了反证验证
(先确认 is_test=t 时查不到,再临时置 f 确认查得到且字段不含机密),
所以 /retail/products、/fleet/routes 出现在「干净」列表里。规范是可行的,只是没被普遍执行。
category 参数全线失效list_products 签名只有 limit,没有 category 形参;v2 五个分类页
(category-package/hiking/vehicle/transport/hotel)都传 ?category=X,
FastAPI 对未声明 query 参数静默丢弃 ⇒ 五页显示同一份全量套餐,分类功能形同虚设。
v2 任务书原文写明该契约 ⇒ 前端按契约写的,后端没实现。
⚠️ 附加阻塞:packages 表没有 category 列(只有 tag,取值 tag_0~tag_4 是种子占位值);
真正品类语义在 package_components.type(hotel/guide/driver/ticket/meal)
⇒ 想实现 category 过滤必须先定品类数据来源,属产品决策。
| 核查项 | 结果 |
|---|---|
orders 表 | 138 条全部 is_test=TRUE(paid 40/completed 36/in_service 27/booked 14/refunding 8/refunded 7/cancelled 6) |
| 真实用户订单 | 0 条 |
| 引用测试套餐的真实订单 | 0 条 |
verifications 核销记录 | 0 条 |
| 用户构成 | 81 个用户中 79 测试 / 2 真实 |
packages 80 条全是测试数据,零真实套餐 ⇒ 即使补上过滤,商品位也会变空白(这才是根本问题:缺真实内容)小冈今天 13:3x 刚交付「b端工作台静态演示画廊」(commit 6ac36879),明确用这批 is_test 数据给刘总演示:
「演示账号=测试商户001号(is_test),演示数据小冈自造入库:8001开通hotel+restaurant业态授权 + 3房型/12房间/4预订(挂真实orders)/3客人/2挂账/8菜品/8桌台/4餐饮订单(全is_test)」
⇒ 直接 seed_demo_data.py --purge 会连带删掉他的演示环境。故 purge 未执行。
is_test = FALSE ⭐我推荐做什么:给 9 个泄漏接口的查询补 AND is_test = FALSE(约 9 处 SQL,各加一个条件)。
packages 等 C端 直接可见表,不动 B端 演示用的房型/房间/菜品/桌台我的建议顺序:A(立即止血)→ D(根本解)→ B(category 另立任务)
| # | 问题 | 我的倾向 |
|---|---|---|
| 1 | 是否授权我跨域改这 9 处(小齐/渔佳新媒体/小冈 名下)?还是转交各归属人自行修? | 建议授权我统一改(同一类缺陷,分散改易漏、口径易不一致),改完在回执逐处列明便于复核 |
| 2 | C端 这些位置变空白是否可接受? | 需你判断。空白诚实但对外形象要考虑;若不可接受,则 D(录真实内容)必须先做 |
| 3 | category 品类数据来源怎么定?(加列 / components 推导 / tag) | 需你定产品口径 |
| 4 | 小冈的演示画廊数据是否保留? | 建议保留,我不动 purge |
| 5 | 是否知会小齐、小冈、渔佳新媒体(三人代码各有泄漏点)? | 建议知会,避免各自重复排查;我已在回执登记 |
| 6 | 是否要把「C端 查询必须过滤 is_test」立为门禁? | 建议立(见 §七) |
scripts/scan_public_test_leak.py(本次新写,已入库)
为什么不用静态分析:我先写了静态正则判据 check_istest_leak.py,反证时发现它不可信:
miniapp_merchants.list_merchants 没被抓到 —— 因其 SQL 是 _BASE_SQL + " WHERE ..." 拼接的,表名在变量里,正则扫单个字面量必然漏
POST /login 查 users(按手机号精确查单条,无害)、 fleet.py 查 merchants(按 id 查存在性)也算成泄漏
⇒ 静态判据对 SQL 拼接无能为力,判据不可信就不能当结论。
改用实测判据(可靠,直接看真实行为,不受拼接/ORM/视图影响):
/openapi.json 取全部 GET 路由(运行时真相)/api/miniapp/**,排除 admin / merchant-console)测试/冒烟/演示/seed_/test_seed/b3demo/demo_)→ 判定泄漏已反证有效:跑出的 9 个泄漏中包含我先前用 curl 单独实测确认的 /merchants(26 家测试商户)。
已声明局限(诚实标注,不夸大):
{id} 的详情接口(游客需猜 id,泄漏面小且请求数会爆炸)| 动作 | 方式 | 是否改数据/代码 |
|---|---|---|
| 查 84 表 is_test 分布 | psql DO $$ … RAISE NOTICE(只读动态扫描) | ❌ 只读 |
| 实测 94 个 C端 GET 接口 | curl / urllib GET,无 token | ❌ 只读 |
| 追溯种子来源 | grep 代码 | ❌ 只读 |
| 查真实订单/核销 | psql SELECT | ❌ 只读 |
| 查 9 处代码归属 | git blame | ❌ 只读 |
prod 数据零改动、9 处代码一行未改、purge 未执行。 全部等你裁定。
| 18:2x 报告 | 本报告(18:5x) | |
|---|---|---|
| 发现范围 | 1 个接口(products) | 9 个接口 |
| 根因分析 | 缺陷 A(无 is_test 过滤)+ 缺陷 B(category 失效) | 同上,并升级为系统性缺陷(84 表 / 29 表全测试) |
| 处置建议 | A/B/C 三选项 | 同上,新增选项 D(录真实内容)与门禁建议 |
| 归属 | 只指出 miniapp_products.py 属小齐 | 9 处归属逐一 blame 定位(小齐4/渔佳新媒体3/小冈2,我 0) |
| 工具 | 手工 curl | scan_public_test_leak.py 可复用(含反证与局限声明) |
前一份报告的结论仍然成立,本报告是范围扩大后的完整版。