--- title: "一个制表符,一个越权" description: "周五收工复盘:KAP 平台两个同名客户差一个 \t,派单挂错联系人;行级隔离上线当天,复盘揪出 6 个详情类越权端点。安全这件事,永远栽在'看起来一样'和'只做了一半'上。" date: 2026-08-28T19:35:00+08:00 draft: false tags: - 安全 - 权限 - 排障 - 复盘 --- ## 背景 周五,两件大事收尾:给自建运维平台上线了**工号绑定 + 行级数据隔离**(12 人企微用户导入),以及修复了一起**三方派单挂错联系人**的生产事故。 一件是新建安全机制,一件是老安全机制失效。巧的是,它们的教训互为镜像。 ## 一、一个制表符引发的事故 派单后,第三方平台的工程师端显示的是**历史联系人**,不是工单上的当前联系人。 排查下来,链条很优雅地坏着: ``` KAP 平台存在两个"同名"客户: "\t上海汉得信息技术股份有限公司" ← 带一个制表符前缀,主联系人=陈红秀(历史) "上海汉得信息技术股份有限公司" ← 正常,主联系人=金甜甜(正确) ``` - **strip 之后名字完全相同**,列表接口返回的还是脱敏名(`上海****`),精确匹配在生产环境永不命中 - 代码逻辑是「找到就 break」——**盲取第一条**,命中了带制表符那位 - 更糟的是,一次 update 把正确客户的地址写进了历史客户档案(**地址污染**) ### 修复:盲选改裁决 候选 >1 时全部拉详情,按四级裁决: 1. detail.name 精确匹配 2. **主联系人电话归一化对比**(实测唯一可靠的裁决键) 3. 地址对比 4. 全部失败 → 报 `ambiguous` 转人工,**绝不盲选** 顺手把另外两家平台排查了一遍:修吧有同类问题(原来传的是公司名,联系人姓名整个丢失)已补传;大鱼链路本来就传联系人,无需改。 **教训**:`matched = c; break` 这种「找到第一条就用」的写法,在数据源不可控(存在脏档案、脱敏名、同名客户)时就是定时炸弹。**候选不唯一时,宁可报错转人工,也不要猜。** ## 二、隔离上线当天,复盘揪出 6 个越权 另一条线是给平台加**行级数据隔离**:每个工程师只能看自己工号的数据,SN 脱敏(`4CE2***Y2`),管理员看全量。 上线过程很顺利——5 个数据面接入隔离,端到端验证:普通用户看 10 条,admin 看 47 条,全对。 **如果故事到这里结束,这篇文章就不存在了。** 晚上复盘时换了个方法:不查「已接入的端点」,而是 grep 全仓所有带 `gh_id` 的模型,反查它们**每一个**查询方法是否都过了隔离。结果揪出 6 个漏网的——全是**详情类端点**: | 端点 | 越权方式 | |------|---------| | getTrend | 传任意 SN 查别人设备的健康趋势 | | getDetail | 传任意 SN 看客户名、工单号 | | getRepairs | 传任意 SN 查维修记录 | | errorStats | 全局统计里返回完整 SN | | warrantyExpiring | 保修列表未按工号过滤 | | findById | 传任意 id 查诊断报告详情 | **为什么会漏?** 列表类端点的查询条件里天然带 `gh_id`(WHERE 一加就隔离了);而详情类端点按主键/SN 查单条,**查询条件本身不含工号**——需要额外反查这条数据的归属人。接入清单是按「页面/功能」列的,详情接口藏在「页面能正常打开」的假象里。 修复后:6 项越权全部拦截,admin 回归正常。 ## 镜像:两件事互为对方的教训 | | 制表符事故 | 越权补漏 | |---|---|---| | 出事的层 | **数据层**(脏数据 + 盲选代码) | **权限层**(只防了列表没防详情) | | 根因模式 | 「strip 后一样」→ 误判唯一 | 「列表能看对」→ 误判全防住 | | 正确姿势 | 候选不唯一 → 四级裁决,绝不盲选 | 隔离要覆盖**所有**查询路径,不只列表 | 共同点就一句话:**验证的时候,你测的路径和攻击者走的路径,往往不是同一条。** - 验证「客户匹配」用的是干净数据,生产里是脱敏名 + 制表符脏档案 - 验证「隔离生效」点的是列表页面,攻击者直接构造详情请求 ## 今日其他(速记) - **edit_wo 维修站歧义**:用户传「武汉长租」连败 5 次,根因是子串匹配没过滤排除词——候选里只剩「美邦武汉长租」「极算武汉长租」,正确的「总仓(武汉)长租」因含括号反而匹配不上。create_wo 早有排除词机制,edit_wo 漏了(同源机制没同步的又一例) - **巡检单 46% 建错站**:rent_type=未知时默认长租没生效,③级按 API 顺序被短租站稳定截胡。教训:**默认值必须覆盖"未说"的分支,取第一个匹配必须显式优先级** - **AI 网关 504**:商汤长 prompt 挂起撞 nginx 120s,fallback 没来得及跑——把 curl 超时降到 90s 让降级生效,顺手接入 MiniMax 做第三梯队 - **eff_stats 每日双跑**:调度器只触发 1 次,是 agent 模型在同一会话里执行了 2 遍脚本。cron prompt 加「严格只执行一次」,明晨验证 ## 总结 今天的关键词是「**补全**」: 1. **数据不可信时,代码要能裁决**——同名客户、脱敏名、脏档案,盲选第一条就是赌博 2. **权限不能只防"正门"**——列表是正门,详情是侧门,越权往往从侧门走 3. **复盘方法比复盘本身重要**——「grep 全部 gh_id 模型反查接入面」这个动作,比发现这 6 个漏洞更有复用价值 4. **默认值也是一种决策**——「用户没说」不等于「随便选」,46% 的错误率就是没想清楚默认值的代价 > 安全的敌人从来不是攻击者,是「看起来已经防住了」。 --- *本文已脱敏,不含真实客户名、联系人、电话或系统密码。*