后台菜单权限我按角色拆了三层,还是被一个接口参数越权查了全站订单

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 65 浏览 2 回复

上周有个老项目做安全复查,我信心满满地跟同事说后台权限早就按角色-部门-数据范围拆了三层,结果安全同事甩过来一个请求:/api/order/detail?id=12345,用普通业务员的 Cookie 直接访问,返回了另一条业务线的订单详情。

当场社死。不是没做鉴权,是鉴权漏了一个最蠢的口子——我只校验了"你能不能进这个菜单",没校验"这条数据是不是你的"。

一、菜单权限 ≠ 数据权限,我混了两年才分清

最早我的后台权限模型很简单:用户 → 角色 → 菜单列表。登录后根据角色查出能点的菜单,前端藏掉没权限的按钮,完事。

问题就出在这。前端藏按钮只是防误触,接口层面如果直接 Order::find($id),任何人拿到 URL 都能遍历全站数据。我那个三层权限,说白了只是"功能权限",跟数据能不能看半毛钱关系没有。

后来补的方案是:每个涉及数据的接口,必须带 where(['user_id' => $currentUser->id]) 或者走数据范围过滤器。ThinkPHP 里我封装了一个 DataScope trait,模型查询自动注入当前用户的数据范围,避免每个控制器手写。

二、CSRF 防护我关过一次,因为"上传图片报错"

有段时间后台富文本上传图片总报 419,查了半天发现是 CSRF token 没带上。当时图省事,在路由中间件里把 verifyCsrfToken 给这个上传接口开了白名单。

白名单开完就忘了。三个月后安全扫描,这个接口裸奔在外,配合 XSS 能直接构造表单提交。修复的时候我没再关 CSRF,而是换了方案:上传走独立的无状态接口,用签名验签(hash_hmac 拼时间戳),前端传 signtimestamp,后端验有效期和签名,既解决 419 又不丢防护。

三、SQL 注入最隐蔽的一回:orderBy 参数

见过 ?sort=price&order=desc 这种参数吧?我早期直接拼进查询:

$query->order($request->get('sort'), $request->get('order'));

ThinkPHP 的 order 方法如果第一个参数是字符串,第二个参数会被直接拼接。有人传 ?sort=1&order=(select sleep(5)),直接触发延时注入。

现在我的规矩是:所有用户可控的排序字段,先白名单校验,再用数组方式传:->order([$sort => $order]),TP 会对数组键做字段名转义。或者更保险点,排序字段写死映射表,前端传 sort=1 对应后端 price,绝不直接透传。

四、几个现在还在执行的笨办法

1. 后台接口统一加 admin 前缀,单独域名或端口,跟前端站点物理隔离,减少暴露面。

2. 敏感操作(改配置、删数据、发公告)强制二次验密,不是验登录态,是验当前密码,防 Cookie 被盗后的横向移动。

3. 所有后台查询日志落库,带完整 SQL(脱敏后),定期跑脚本扫有没有没加 where user_id 的查询——这办法蠢,但真能抓到漏网之鱼。

4. 开发环境 APP_DEBUG 用独立 .env.local,提交前脚本检查,禁止含 debug=true 的配置进仓库。

五、最后吐槽

安全这事,看文档的时候觉得"这谁不会啊",真踩坑才发现全是细节。我现在每次写后台接口,脑子里过三遍:功能权限过了吗?数据范围加了吗?参数是白名单还是直接透传?

有次改到凌晨,脑子糊了,把 where(['user_id' => ...]) 写成了 where('user_id', ...) 但变量传的是 $request->user_id——用户传啥就是啥,等于没鉴权。Code Review 的时候被同事指出来,一身冷汗。

你们有没有类似"以为防住了其实漏成筛子"的经历?或者有什么笨但有效的土办法,欢迎丢过来,我抄作业。

评论2
回复 · 2
Knox
Knox 新手 · #2 ·
感谢分享!
chenshao
chenshao 新手 · #1 ·
nmmjj
微信客服 微信客服