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

测试工具实战

测试 CORS 与身份认证的边界:设计原理

在 TT Lab 中继续学习

一句话总结

对比允许与拒绝的预检请求,防止把 CORS 误当成认证而引发的回归。

为什么需要它

来自未被允许的 Origin 的请求在服务器上被执行了,开发者因此认定是 CORS 库的 bug。但浏览器的读取限制与服务器的权限检查是两种不同的职责。必须准确区分测试名称和断言究竟保证了什么。

工作原理

把 Origin 作为不带 path 的源进行验证,并测试重复与通配符策略。直接设置 OPTIONS 请求的 Access-Control-Request-Method。在比较普通请求与预检请求的状态和允许的请求头时,也要记录 TestClient 并未实现浏览器拦截这一局限。

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

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

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

1. 验证源的格式——测试

测试所提供 service.py 的下列公开契约:origin(value) 在 value 是 http 或 https URL、有 host,且没有 path、query、fragment 和用户信息时,返回输入字符串。其他情况抛出 ValueError。末尾的 / 也属于 path,因此要拒绝。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:如果把整个 URL 都当作源允许,就可能把路径或用户信息混淆进去。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

or url.query

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

2. 去除重复的源——测试

测试所提供 service.py 的下列公开契约:origins(values) 先用 origin 验证每一项,再按首次出现的顺序去重,返回新列表。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:白名单是精确的源列表,而不是字符串的部分匹配。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

[origin(value) for value in values]

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

3. 用白名单限制方法——测试

测试所提供 service.py 的下列公开契约:methods(values) 只允许 GET、POST、PUT、DELETE、OPTIONS,转为大写后去重。空列表或其他值抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:不要悄悄加入未被允许的 PATCH 和任意方法。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

"DELETE","OPTIONS","PATCH"

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

4. 不同时允许凭据与通配符——测试

测试所提供 service.py 的下列公开契约:policy(allowed, credentials) 确认 credentials 是 bool。如果 allowed 中含有 '*' 则抛出 ValueError,否则返回 {allow_origins:origins(allowed), allow_credentials:credentials}。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:本实验的明确策略是,无论是否携带凭据,都不接受通配符。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

not isinstance(credentials, (bool, int))

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

5. 挂上真正的 CORS 中间件——测试

测试所提供 service.py 的下列公开契约:create_app(allowed, credentials=True) 返回一个应用:先验证 policy,并配置 CORSMiddleware。只允许 GET/POST,允许 Content-Type 和 X-Request-ID 请求头,并 expose X-Trace 响应头。GET /data 返回 {ok:True} 以及 X-Trace='trace-1'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:如果分别手工给 preflight 和实际响应加请求头,两套策略很容易出现偏差。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

expose_headers=[]

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

6. 构造预检请求——测试

测试所提供 service.py 的下列公开契约:preflight_headers(source, method, requested='X-Request-ID') 返回含有 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 三个键的字典。method 为大写。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:实际的请求方法是 OPTIONS,而要检查的方法放在另一个请求头里。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

method.lower()

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

7. 计算拒绝矩阵——测试

测试所提供 service.py 的下列公开契约:preflight_status(app, source, method, requested='X-Request-ID') 用 TestClient 向 /data 发送 OPTIONS 请求并返回 HTTP 状态。不同的源、DELETE、X-Secret 请求头都必须得到 400。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:不要把三种拒绝原因混在同一个请求里,才能找出缺失的策略。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

client.get("/data",

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

8. 观察 CORS 与认证的区别——测试

测试所提供 service.py 的下列公开契约:cors_observation(app, source) 发送 GET /data,并返回 (状态, Access-Control-Allow-Origin 的值或 None, JSON 正文)。即使源未被允许,200 的正文也会被执行,但不应带有允许源的请求头。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:curl 或服务器之间的请求不会遵循浏览器的 CORS 读取限制。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

source

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

在现场相遇的样子

TestClient 不是浏览器。它能检查 CORS 响应头和预检请求,但并没有实现浏览器自身对读取的拦截。带着未被允许的 Origin 发送的普通 GET,同样可能在服务器上被执行。敏感操作必须用独立的认证、权限和 CSRF 策略来保护。提供的实现可以阅读,但评分使用的是另外的副本。不要通过检查源码措辞或修改文件来绕过缺陷,而要检查公开接口的实际运行结果。

下一项实验要做什么

八个步骤会连成一个可运行的成果。验证源的格式——测试 → 去除重复的源——测试 → 用白名单限制方法——测试 → 不同时允许凭据与通配符——测试 → 挂上真正的 CORS 中间件——测试 → 构造预检请求——测试 → 计算拒绝矩阵——测试 → 观察 CORS 与认证的区别——测试。

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