题目与适用场景
一家 B2B 文档服务使用共享数据库。用户可以属于多个租户,并在每个租户中拥有 viewer、 editor 或 admin 角色;文档还可以单独共享给同租户成员。服务提供单条读取、修改、删除、批量 导出和异步生成下载包的 API。JWT 已经过签名、过期时间和受众校验,但旧代码会先按客户端提供的 document_id 全局查询文档,再判断请求是否已登录。
攻击者从审计记录、共享链接或另一个接口获得有效 UUID 后,把自己的文档 ID 换成该 UUID。UUID 很难枚举,但服务器若没有检查“当前主体能否在当前租户对这个对象执行这项动作”,仍会返回或修改 文档。OWASP 把这类问题称为 Broken Object Level Authorization(BOLA);在常见 Web 安全语境中 也称 Insecure Direct Object Reference(IDOR)。对象标识可以位于路径、查询参数、请求头、JSON、 GraphQL 变量、文件名或批量列表中。
两份公开 API 面试准备材料都直接要求解释或测试 IDOR。2026 年一则公开讨论还在追问“加密对象 ID 是否能修复 IDOR”,说明“隐藏 ID”仍是候选人需要拆穿的现实误区。OWASP API Security Top 10 的 API1:2023 与 2026 年公开实证研究为风险和技术边界提供了进一步依据;这些证据支持题目的代表性, 不用于推断具体公司或面试频率。
本题归入后端:核心工作是把认证上下文、租户关系、对象属性和动作落实到 API、数据访问、事务、 缓存与后台任务的共同授权边界。现有题库的 OAuth 文章处理身份与授权码流程,SQL 注入文章处理查询 结构,SSRF 文章处理出站目标授权;它们都不能替代本题的对象级访问决策。
面试官考察什么
第一项信号是准确区分概念。认证回答“调用者是谁”;对象级授权回答“这个主体现在能否对这个对象做 这个动作”。用户有权调用 PATCH /documents/{id},却无权修改另一个租户的文档,属于 BOLA。 普通成员能调用仅管理员可用的批量删除函数,更接近 Broken Function Level Authorization;能够修改 自己文档中本不允许更新的 owner_id,属于对象属性级授权问题。
第二项信号是授权模型完整。只比较 document.owner_id == user.id 会漏掉租户角色、团队共享、只读 权限、权限撤销和支持人员临时访问。强回答会把一次决策写成:
allow(subject, tenant, action, resource, context) -> decision + reason其中租户来自经过验证的成员关系,动作区分 read、update、delete、share、export,资源包含 所属租户、状态和共享关系,上下文可包含临时支持授权与策略版本。未命中允许规则时默认拒绝。
第三项信号是把安全边界放到查询和写入处。先执行 findById(id),再靠某个控制器补一次检查,很容易 被批量接口、后台任务、缓存和新路由绕过。推荐路径从已授权数据集查询,写操作把作用域与版本条件放进 同一条语句或事务,并检查受影响行数。
第四项信号是认识纵深防御的边界。随机 UUID、404、限流和 PostgreSQL Row-Level Security(RLS)都 有价值,但没有一项能代替完整业务授权。PostgreSQL 18 文档还明确说明超级用户、BYPASSRLS 角色和 通常情况下的表所有者会绕过 RLS;策略启用、连接角色和请求租户上下文都必须验证。
最后看能否证明“所有路径都拒绝越权”。只测自己的 GET 成功属于功能测试。安全测试要用多个账户、 多个租户、多个角色和已知有效的他人对象 ID,覆盖读取、修改、删除、批量、导出、下载、GraphQL、 缓存命中和异步执行,还要断言没有数据库变化、对象存储产物或消息副作用。
回答前要确认的问题
- 租户如何选择? 用户可以切换活跃租户,但客户端的
X-Tenant-ID只能表达选择;服务器必须用
已验证身份重新确认成员关系,不能把请求头直接当成可信租户。
- 权限由什么决定? 只有租户角色,还是还包含文档所有权、团队、显式共享、文档状态和临时支持
授权?规则越动态,越需要集中策略和一致的审计原因。
- 哪些动作需要独立权限? 读取不自动包含下载、导出、共享或删除。批量操作必须为每个对象做同样
的动作级判断。
- 未授权时返回 403 还是 404? 对外对象 ID 若不应暴露存在性,可以把不存在与不可见统一成 404;
同租户内已知对象但动作不足可按产品契约返回 403。无论状态码如何,都不能返回敏感差异。
- 异步任务采用当前权限还是提交时快照? 本题在入队时检查一次,执行时重新检查当前权限;撤权后
尚未执行的导出必须停止。若业务需要不可撤销的授权快照,应把它设计成有范围、有期限的显式授权。
- 支持人员能否跨租户访问? 允许时走独立的临时授权路径,要求工单、理由、审批、到期和完整审计,
不能给普通应用连接一个长期全局管理员开关。
- 是否使用 PostgreSQL RLS? 本题把应用层作用域查询作为主控制,并用 RLS 防御遗漏。也可以选择
其他组合,但要说明数据库角色、连接池上下文、迁移和后台任务如何保持策略成立。
30 秒回答框架
“我会先把授权写成 subject + tenant + action + resource + context 的决策,并默认拒绝。认证中间件 只建立主体;活跃租户必须从已验证成员关系取得。数据访问层不提供无作用域的文档查询,读取使用 tenantid + documentid,再结合角色、所有权和共享关系判断动作;更新与删除把这些条件放进同一条 语句或事务,零行受影响就按不可见处理。UUID 只降低枚举,不能代替授权。
批量、缓存、下载和异步导出都沿用同一规则:缓存键带租户,能力链接限制对象、动作和期限,任务入队 与执行各检查一次。PostgreSQL RLS 可以兜底,但应用角色不能绕过策略,并要测试连接池租户切换。最后 用两个租户的多角色矩阵替换路径、正文和批量 ID,断言响应、数据、文件和消息都没有跨租户副作用。”
分步骤深入解答
第一步:列出授权元组和对象入口
先把业务规则写成矩阵,避免把角色名散落在控制器中:
| 主体关系 | read | update | delete | share | export | |---|---:|---:|---:|---:|---:| | 租户 viewer,未共享 | 拒绝 | 拒绝 | 拒绝 | 拒绝 | 拒绝 | | 租户 viewer,显式共享 | 允许 | 拒绝 | 拒绝 | 拒绝 | 允许或按产品规则 | | 租户 editor | 允许 | 允许 | 拒绝 | 按策略 | 允许 | | 文档所有者 | 允许 | 允许 | 按策略 | 允许 | 允许 | | 租户 admin | 允许 | 允许 | 允许 | 允许 | 允许 |
表格只是本题假设。真实系统要由产品和安全负责人确认,并为每条允许规则写出理由。默认拒绝意味着 新增动作、未知角色、缺失租户上下文或策略错误都会失败关闭。
随后建立对象入口清单:路径参数、查询条件、请求体、批量数组、GraphQL 节点、共享令牌、对象存储键、 缓存键、消息和后台任务。OWASP 的定义不要求 ID 是递增数字;UUID、文件名和通用字符串同样是对象 引用。逐个入口标注主体来源、租户来源、动作、资源查找方式和最终副作用,才能发现旁路。
第二步:从认证上下文建立可信租户
JWT 验证通过后得到稳定 user_id,并不自动信任其中长期缓存的所有角色,也不能信任客户端另传的 租户 ID。用户选择租户 tenant_b 时,服务器查询或验证当前有效成员关系,构造只在本请求内生效的 AuthContext:
AuthContext {
user_id,
tenant_id,
membership_id,
roles,
policy_version,
support_grant_id?
}中间件负责保证上下文存在,领域策略仍负责对象与动作判断。连接池、任务消费者和并行请求不能共用可变 全局租户变量;每次借出连接都在事务作用域设置上下文,结束后清理,避免上一个请求的租户泄漏给下一个。
第三步:把租户作用域放进查询与数据约束
危险查询先全局取对象:
SELECT * FROM documents WHERE id = :document_id;基础防线至少同时限定可信租户:
SELECT *
FROM documents
WHERE tenant_id = :auth_tenant_id
AND id = :document_id;若显式共享决定可见性,可以在作用域查询中加入共享关系,或先取同租户资源后交给集中策略引擎判断。 不能从请求体读取 tenant_id 填入条件。对外可统一把零行解释成 404,从响应上不区分“别的租户存在” 和“不存在”。
表结构让错误更难发生。共享表、版本表、附件表和导出项都保存 tenant_id;唯一键与外键在需要时采用 (tenantid, resourceid) 组合,使一个租户的子记录无法引用另一个租户的父记录。应用仍要做授权, 数据库约束负责阻止跨租户关联被意外写入。
第四步:让授权与写入处于同一正确性边界
“读取文档、在应用中判断、稍后更新”存在检查后使用竞态。权限或文档状态可能在两步之间变化。简单 规则可以直接使用条件更新:
UPDATE documents
SET title = :title, version = version + 1
WHERE tenant_id = :auth_tenant_id
AND id = :document_id
AND version = :expected_version
AND (
owner_id = :user_id
OR :can_edit_tenant_documents
);受影响行数为零时,不再发消息、生成审计成功记录或更新缓存。复杂共享关系可以在同一事务中锁定相关 成员与资源版本,或让策略可转成数据库谓词。关键是授权依据与副作用共享明确的事务/版本边界,不能 检查旧快照后无条件写入。
批量操作必须先定义原子性。本题选择全有或全无:在同一可信租户和动作下解析全部 ID,去重后查询已 授权集合;数量与输入不一致就整批拒绝,不能悄悄导出可见子集让调用方探测存在性。若产品选择逐项 结果,也要逐对象授权并对未授权项使用不会泄漏的统一结果。
第五步:覆盖缓存、下载与异步任务
缓存键至少包含租户与资源版本,例如 document:{tenantid}:{documentid}:{version}。命中缓存只取得 数据,当前请求仍要通过动作授权。若缓存策略决策,键必须覆盖主体、租户、动作、资源、关系或策略版本, 并在撤权时失效;复杂系统通常更安全的做法是缓存关系数据,再重新计算短小决策。
预签名下载 URL 是一种短期能力。一旦签发,持有者可能在有效期内直接访问存储,因此签发前检查 download 权限,并把对象、动作、过期时间和内容处置绑定到链接。高敏感文档使用短期限、单次或代理 下载与撤销机制。签名证明链接由服务端签发,不会让原本无权的调用者自动获得授权。
异步导出在两个时点检查:API 入队前验证每个文档的 export 权限;工作进程执行时用任务中的主体和 租户引用重新加载当前成员关系与资源权限。权限已撤销、文档换租户或支持授权过期就停止,并确保尚未 生成可下载产物。任务载荷不能接受调用方自称的管理员标志,也不能保存一份永久有效的角色数组。
第六步:把 PostgreSQL RLS 当作可验证的纵深防御
共享表可启用 RLS,让策略按事务内设置的可信租户筛选现有行,并用 WITH CHECK 约束新增或修改后的 行。没有适用策略时默认拒绝,是理想的失败方式。但上线前必须验证:
- 应用连接角色不是超级用户,没有
BYPASSRLS,也不是会默认绕过策略的表所有者;需要时使用
FORCE ROW LEVEL SECURITY;
- 每个事务都设置当前租户,连接归还池前清理;缺少上下文时拒绝,而非使用默认租户;
USING覆盖可见旧行,WITH CHECK覆盖插入和更新后的新行;- 迁移、备份、后台任务和支持工具使用单独角色与明确流程;
- 多个 permissive 策略默认以
OR组合,新增策略不会意外扩大访问;复杂子查询策略还要评估并发
快照与性能。
RLS 不能表达所有产品关系,也不能保护绕开数据库的对象存储或搜索索引。应用策略负责完整业务语义, RLS 负责在某条查询忘加租户条件时阻止泄漏;两层要用同一权限矩阵做一致性测试。
第七步:设计错误、审计和可证伪测试
对跨租户或不可见对象,本题返回统一 404 和固定响应结构。拒绝路径不返回对象标题、租户名、所有者、 版本、文件大小或不同耗时提示。速率限制可降低枚举和审计噪声,但安全结论不能依赖攻击者猜不到 UUID。
审计记录 actor、可信 tenant、action、资源标识的受控表示、decision、reason code、policy version、 support grant 和 trace ID。日志不保存文档正文、下载能力或完整 JWT。异常指标包括跨租户拒绝、同一 主体短时间访问大量不存在对象、策略错误、RLS 拒绝、撤权后任务终止和支持授权使用。
测试矩阵至少包含:
- 两个租户,每个租户有 owner、viewer、editor、admin,再加入已撤权用户和临时支持人员;
- 自己的对象、同租户未共享对象、显式共享对象、他租户有效 UUID和不存在 UUID;
- list、GET、PATCH、DELETE、share、bulk export、GraphQL、download、cache hit 和 worker execute;
- 路径、查询、JSON、批量数组、嵌套 GraphQL 变量、文件键和任务载荷中的 ID 替换;
- 读取拒绝时响应不泄漏,写入拒绝时行版本、共享关系、文件、消息、搜索索引和审计成功事件均不变;
- 撤权与更新并发、连接池连续切换租户、RLS 上下文缺失、应用角色误配置和缓存旧权限。
在 CI 中从授权矩阵生成正向与负向用例,并让新端点必须登记资源和动作。扫描器可以发现一部分可枚举 路径,但它不了解业务所有权;多账户、已知有效的他人对象和副作用断言才是本题的核心证据。
高质量示范回答
“我会把这次缺陷定义为 BOLA:JWT 已经证明调用者身份,API 仍缺少对目标文档的动作级授权。UUID 降低枚举概率,不改变授权结论。我先建立 subject、tenant、action、resource、context 的矩阵, 默认拒绝。用户可选择活跃租户,但服务端必须从已验证成员关系生成租户上下文。
数据访问不再暴露全局 findById 给业务路由。读取先按可信 tenantid + documentid 缩小集合, 再依据角色、所有权和共享关系判断 read、update、delete、share 或 export。简单更新直接把 租户、对象、动作条件和版本放进一条条件语句,零行受影响就停止所有副作用;复杂策略在同一事务中固定 相关版本。子表用复合租户外键防止跨租户关联。
我会把旁路纳入同一模型。批量导出按每个 ID 授权,本题选择全有或全无;缓存键带租户且命中后仍重新 授权;下载链接在签发时检查 download,绑定对象与短期限;异步任务入队和执行都检查,确保撤权在执行 前生效。PostgreSQL RLS 用于兜底,但应用角色不能拥有 BYPASSRLS 或表所有权,并测试连接池事务上下文 不会串租户。
最后我会用两个租户的多角色账户,把一个已知有效的他人 UUID 放进路径、JSON、批量和 GraphQL,覆盖 读写删、导出、下载、缓存与任务。每次拒绝都检查响应不泄漏,并断言数据库版本、文件、消息和搜索索引 没有变化。这样能逐条证明对象级授权,并同时覆盖登录、错误响应与副作用。”
常见错误
- 把 UUID、Base64 或加密 ID 当成修复 → ID 可能从日志、分享或其他接口泄漏,持有有效引用仍可
越权 → 每次按主体、租户、对象和动作执行服务端授权,随机 ID 只做纵深防御。
- 验证 JWT 后允许读取任意 ID → 认证只确认主体,未证明其能访问该对象 → **从可信成员关系建立
租户上下文,再做对象级决策。**
- 控制器先全局查询、随后手写 owner 检查 → 新路由、批量、缓存和后台任务容易遗漏,团队共享也会
被错误拒绝 → 提供租户作用域数据访问和集中策略,默认拒绝。
- 只测 GET → PATCH、DELETE、导出、GraphQL 和下载可能仍泄漏或改变状态 → **按资源入口与动作生成
多账户负向矩阵。**
- 批量接口过滤掉无权 ID 后继续成功 → 返回数量与内容可能成为对象存在性探针,调用方也难以理解
部分成功 → 预先定义全有或全无/逐项契约,并对每个对象授权。
- 把 RLS 视为自动安全 → 表所有者、超级用户、
BYPASSRLS、缺失上下文或 permissive 策略组合都
可能破坏隔离 → 验证角色、事务上下文、USING/WITH CHECK 和故障模式。
- 只断言返回 403 或 404 → 拒绝前可能已经写库、发消息或生成文件 → **同时断言所有持久化和外部
副作用保持不变。**
- 为了排查记录完整对象和令牌 → 安全日志本身形成新的数据泄漏 → **记录最小身份、动作、原因和
受控资源标识。**
追问及应对
追问一:既然 UUIDv4 几乎猜不到,还需要对象级授权吗?
需要。对象引用可经共享链接、浏览器历史、日志、通知、分析系统、另一个 API 或误发消息泄漏。UUID 只能降低盲目枚举的成功率,不能表达持有者、租户、动作或期限。测试时直接给攻击账户一个已知有效的 他人 UUID;若仍能访问,就已经证明授权缺失。
追问二:跨租户对象统一返回 404,会不会妨碍调试?
外部响应可以统一,内部审计仍记录稳定原因码,例如 RESOURCENOTVISIBLE、ACTION_DENIED 或 TENANTCONTEXTINVALID。运维人员通过受控日志和 trace ID 排查,调用方看不到对象是否存在。若同租户 协作需要明确“无编辑权限”,可以在已经证明对象对该用户可见后返回 403;这要成为一致的 API 契约。
追问三:一个用户被移出租户时,已经排队的导出任务怎么办?
任务提交成功不等于获得永久读取权。本题在执行前重新加载成员关系和每个资源的 export 权限;撤权后 任务进入拒绝状态,删除临时文件,并且不签发下载链接。若合规流程要求提交时冻结权限,应创建明确的 短期授权快照,记录范围、批准者和到期时间,而不能悄悄沿用陈旧 JWT 角色。
追问四:应用已经在每条查询加了 tenant_id,RLS 还有什么价值?
它能拦住漏加条件的新查询和部分直接数据库路径,缩小单点疏漏的影响。不过它增加连接上下文、角色、 迁移和策略维护成本,也不能保护搜索索引、缓存和对象存储。高敏感共享表适合双层防御;采用前要证明 应用角色不绕过 RLS、缺租户时默认拒绝、连接池不串上下文,并用同一矩阵测试两层结果一致。
追问五:支持工程师需要临时查看客户文档,怎样避免长期后门?
建立独立的 just-in-time 支持授权:绑定工单、目标租户、允许动作、批准者、短期限和理由;敏感动作可 要求双人审批。支持接口与普通接口分开,展示明显会话状态,禁止批量下载,并记录每次对象访问。授权 到期立即失效,定期审查使用情况。普通应用角色和通用服务令牌不获得跨租户权限。