post: 一个制表符一个越权(8/28收工)
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
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% 的错误率就是没想清楚默认值的代价
|
||||||
|
|
||||||
|
> 安全的敌人从来不是攻击者,是「看起来已经防住了」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文已脱敏,不含真实客户名、联系人、电话或系统密码。*
|
||||||
Reference in New Issue
Block a user