TT Lab
开始
学习 学习路径 课程

测试工具实战

发现访问控制缺陷的测试矩阵:设计原理

在 TT Lab 中继续学习

一句话总结

编写负面测试,把“未认证”“无权限”“他人所有”和“正常请求”分开。

为什么需要它

只确认成功响应的访问控制测试一直是绿灯。即使在路径中漏掉了 Header 注入,或者删掉了 scope 检查,测试也不会失败。验证者不能只罗列允许的用例,而必须分别观察每一种拒绝的原因。

工作原理

每个步骤都要亲手构造用户、文档和权限的组合。断言要同时覆盖响应 dict 和状态码,还要暴露出会修改原始 scopes 的复制缺陷。最后用 TestClient 实际运行 200、401、403、404 矩阵。

학생 테스트 → 정상 구현: 실제 시험 모두 통과
           └→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·0개 실행·강제 종료 ≠ 결함 검출

阅读契约并预测失败的工作表

下面不是让你背下整个实现的答案,而是逐步骤的代码评审。每个改动片段都会故意破坏契约。请注意,改动之后正常用例仍然可能通过。运行之前先预测:观察哪些输入、异常和状态,差异才会暴露出来;实现之后,再把预测与结果进行比较。

1. 拆分 Bearer 请求头——测试

测试所提供 service.py 的下列公开契约:bearer(header) 在请求头恰好以 'Bearer ' 开头、后面只有一个不含空白的令牌时返回该令牌。None、空令牌、其他 scheme 和多余的空白都会抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:如果随意把请求头切成多段,就可能把空白错误当成正常令牌接受。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

header[6:]

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

2. 复制身份后再返回——测试

测试所提供 service.py 的下列公开契约:principal(token, users) 返回令牌字典中的用户 {id, scopes},并且连 scopes 列表也一并复制。未知令牌会抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:修改返回的 scopes 时,如果连原始用户的权限也被改变,不同请求之间的权限就会混在一起。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

user["scopes"]

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

3. 精确比较权限——测试

测试所提供 service.py 的下列公开契约:require_scope(user, scope) 在 scopes 中恰好存在该 scope 字符串时返回 None,否则抛出 PermissionError。read-all 不等于 read。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:子串比较会把更长的权限名误认为另一个权限。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

if False:

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

4. 单独确认所有权——测试

测试所提供 service.py 的下列公开契约:visible(user, document) 仅当 document 不是 None,且 owner 与 user 的 id 完全相同时才为 True。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:把资源不存在与他人所有归为同一个判定。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

True

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

5. 用白名单挑选响应字段——测试

测试所提供 service.py 的下列公开契约:public_document(document) 返回一个只含 id 和 title 的新字典。不包含 owner 或 internal_cost。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:不要从原始数据中删除字段,而是组装一个新的响应。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

("id", "title", "owner")

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

6. 让错误符合 HTTP 契约——测试

测试所提供 service.py 的下列公开契约:authenticate(header, users) 把 bearer 与 principal 连接起来。ValueError 要变成 HTTPException(401),且 headers 中 WWW-Authenticate 的值为 Bearer。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:不要把认证失败和应用错误笼统地合成一个 500。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

HTTPException(403,

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

7. 固定拒绝的顺序——测试

测试所提供 service.py 的下列公开契约:read_document(user, documents, document_id) 在没有 read scope 时抛出 HTTPException(403),文档不存在或属于他人时抛出 HTTPException(404),否则返回 public_document 的结果。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:即使认证通过之后,也必须分别确认 scope 和所有权。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

HTTPException(403, "not found")

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

8. 在真实请求中封住边界——测试

测试所提供 service.py 的下列公开契约:create_app(users, documents) 返回一个 FastAPI 应用,它在 GET /documents/{document_id} 上接收 Authorization 请求头,并调用 authenticate 和 read_document。通过真实请求验证 200、401、403、404 以及非公开字段的移除。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:即使各个函数单独都是对的,只要在路径里漏掉了调用,访问控制就不会生效。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

authorization: str | None = None

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

在现场相遇的样子

固定的令牌字典只是教学用的输入。生产环境的认证还需要过期、签名、撤销和安全保存。仅仅让 404 保持一致,也不会消除通过响应时间或访问日志得出的所有推断。还要同时检查被拒绝的请求是否连原始数据也改动了。提供的实现可以阅读,但评分使用的是另外的副本。不要通过检查源码措辞或修改文件来绕过缺陷,而要检查公开接口的实际运行结果。

下一项实验要做什么

八个步骤会连成一个可运行的成果。拆分 Bearer 请求头——测试 → 复制身份后再返回——测试 → 精确比较权限——测试 → 单独确认所有权——测试 → 用白名单挑选响应字段——测试 → 让错误符合 HTTP 契约——测试 → 固定拒绝的顺序——测试 → 在真实请求中封住边界——测试。

每一步检查的都不是函数或文件是否存在,而是实际的返回值、异常和状态变化。看过正确答案之后,请故意改动边界比较或清理代码,确认哪些测试会失败。说明为什么前面的测试在后面的步骤中仍然保留,并写出一种本实验不能保证的生产条件。