Skip to content

<!-- title: idor -->

越权访问(IDOR)详解:原理、利用与防御

一、漏洞原理

越权访问是"访问控制失效"(Broken Access Control)的统称,OWASP Top 10 多年来的头名常客。它不涉及任何花哨的技术手段——攻击者只是以合法身份,访问了本不属于他的数据或功能。其中最典型的形态叫 IDOR(Insecure Direct Object Reference,不安全的直接对象引用):接口用订单号、用户 ID、文件 ID 这类"直接引用"来定位资源,却不去校验"这个引用是否属于当前登录者"。

越权分两个方向:

  • 水平越权:普通用户 A 能看到/修改普通用户 B 的数据。例如把 URL 里的 ?orderid=1001 改成 1002,就翻开了别人的订单
  • 垂直越权:普通用户能执行管理员才能执行的功能。例如直接请求只有管理后台才有的接口路径,而服务端忘了校验角色

这类漏洞的特殊性在于:自动化扫描器几乎扫不出来。SQL 注入、XSS 都有明显的语法特征,而越权需要理解"业务上谁该看什么"——这正是人类测试和代码审计的价值所在,也是它常年霸榜的原因:开发太容易忘记"每一处"校验,而攻击者只需要找到"漏掉的一处"。

二、示例代码

以下是一个存在水平越权(IDOR)的订单查询接口(虚构站点 example.com):

php
<?php
// api/order.php —— 登录用户查询订单详情
session_start();
if (!isset($_SESSION['uid'])) { http_response_code(401); exit('未登录'); }

// 致命点:只校验了"登录没有",没校验"订单是不是你的"
$order = db_query("SELECT * FROM orders WHERE id = ?", [$_GET['id']]);
echo json_encode($order);
?>

注意这个接口甚至正确使用了参数化查询——防住了 SQL 注入,却防不住越权,两种防御彼此不可替代。修复后的安全写法是把"归属校验"写进查询条件本身:

php
<?php
// 安全版:查询条件里绑定当前用户身份
session_start();
if (!isset($_SESSION['uid'])) { http_response_code(401); exit('未登录'); }

$order = db_query(
    "SELECT * FROM orders WHERE id = ? AND user_id = ?",
    [$_GET['id'], $_SESSION['uid']]          // 双重条件:订单存在 且 属于本人
);
if (!$order) { http_response_code(404); exit('订单不存在'); }
echo json_encode($order);
?>

这种写法优于"先查出来再 if 判断归属"——后者容易在分支遗漏,而前者让越权数据根本查不出来。

三、三种典型 Payload(教学演示)

1. 递增 ID 遍历(最经典的 IDOR)

text
GET /api/order.php?id=1001     (换成 1002、1003……)

数字主键是最大的"路标"。在自测自己的应用时,开两个账号 A、B,用 A 的 Cookie 逐个请求 B 的资源 ID——凡返回 200 而非 403/404 的,都是越权点。防御上对这类资源应改用不可猜测的随机标识(UUID),作为纵深防御的一层。

2. 修改请求中的身份参数(水平越权变体)

text
Cookie: uid=1001   →   Cookie: uid=2002
POST /api/profile  body: {"email": "new@example.com", "uid": 2002}

把身份信息放在 Cookie 明文字段或请求体 uid 参数里并予以信任,等于把门钥匙挂在了门上。服务端身份必须且只能来自服务端会话,任何来自客户端的身份声明都应忽略或校验签名。

3. 直闯管理接口(垂直越权)

text
GET /admin/deleteUser?id=2002        (用普通用户的 Cookie 直接请求)

很多应用只在前端隐藏管理入口(按钮不渲染、菜单不显示),后端接口却人人可调。测试方法就是收集所有高权限接口路径,用低权限身份逐一请求。这类漏洞的教训是:前端隐藏是体验设计,后端校验才是安全边界

四、防御建议

  1. 默认拒绝,逐处校验:每个接口都要显式回答"这个角色能对这个对象做什么";无法回答就默认拒绝(Deny by Default)
  2. 把归属写进查询条件:数据查询一律绑定当前用户身份(如上例的 AND user_id = ?),让越权数据在 SQL 层就不可达
  3. 使用不可预测的资源标识:对外暴露的资源用 UUID/随机串代替自增 ID,抬高遍历成本(注意这是纵深层,不能替代权限校验)
  4. 统一鉴权中间件:把"登录校验 + 角色校验"收敛到框架的统一入口(拦截器、装饰器、中间件),避免散落在业务代码里"漏写一处";对管理接口按 URL 前缀统一挂权限
  5. 不信任客户端身份声明:会话身份只取服务端会话/JWT 的已验证字段;请求体里的 uid/userId 等字段一律忽略
  6. 写入自动化测试:把"用户 A 访问用户 B 的资源应得 403/404"写成单元测试与集成测试,回归时自动检查;上线前用双账号人工过一遍核心接口

五、总结

越权访问是"最朴素的漏洞"与"最持久的漏洞":没有特殊字符、没有报错特征,只有一句被遗漏的"这是谁的数据"。它的防御没有银弹,靠的是工程纪律——统一的鉴权层、默认拒绝的心态、把权限测试写进 CI。对学习者来说,学会"双账号对照测试法"是入门渗透测试最有性价比的第一课;对开发者来说,请从今天起把"当前用户是谁"当作每一条查询的默认谓词。

仅用于学习交流的防守型安全知识库