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

测试工具实战

测试 CORS 与身份认证的边界

在 TT Lab 中继续学习

目标

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

为什么重要

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

步骤

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

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/test-cors-browser-boundary-lab
test -e /root/work/test-cors-browser-boundary-lab/service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/service.py /root/work/test-cors-browser-boundary-lab/service.py
test -e /root/work/test-cors-browser-boundary-lab/test_service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/test_service.py /root/work/test-cors-browser-boundary-lab/test_service.py
cd /root/work/test-cors-browser-boundary-lab
  1. 在 /root/work/test-cors-browser-boundary-lab/test_service.py 中测试所提供 service.py 的下列公开契约:origins(values) 先用 origin 验证每一项,再按首次出现的顺序去重,返回新列表。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

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

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

  4. 在 /root/work/test-cors-browser-boundary-lab/test_service.py 中测试所提供 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_ 函数。

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

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

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

参考

验证源的格式——测试

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

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/test-cors-browser-boundary-lab
test -e /root/work/test-cors-browser-boundary-lab/service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/service.py /root/work/test-cors-browser-boundary-lab/service.py
test -e /root/work/test-cors-browser-boundary-lab/test_service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/test_service.py /root/work/test-cors-browser-boundary-lab/test_service.py
cd /root/work/test-cors-browser-boundary-lab

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/01-contract.sh 确认。

去除重复的源——测试

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/02-contract.sh 确认。

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

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/03-contract.sh 确认。

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

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/04-contract.sh 确认。

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

在 /root/work/test-cors-browser-boundary-lab/test_service.py 中测试所提供 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 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/05-contract.sh 确认。

构造预检请求——测试

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/06-contract.sh 确认。

计算拒绝矩阵——测试

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/07-contract.sh 确认。

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

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

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

保存后用 bash /opt/lab/checks/test-cors-browser-boundary-lab/08-contract.sh 确认。