测试 CORS 与身份认证的边界
目标
对比允许与拒绝的预检请求,防止把 CORS 误当成认证而引发的回归。
为什么重要
来自未被允许的 Origin 的请求在服务器上被执行了,开发者因此认定是 CORS 库的 bug。但浏览器的读取限制与服务器的权限检查是两种不同的职责。必须准确区分测试名称和断言究竟保证了什么。
步骤
- 在
/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
-
在
/root/work/test-cors-browser-boundary-lab/test_service.py中测试所提供 service.py 的下列公开契约:origins(values) 先用 origin 验证每一项,再按首次出现的顺序去重,返回新列表。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。 -
在
/root/work/test-cors-browser-boundary-lab/test_service.py中测试所提供 service.py 的下列公开契约:methods(values) 只允许 GET、POST、PUT、DELETE、OPTIONS,转为大写后去重。空列表或其他值抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。
参考
- 无需联网,也无需安装软件包,在现有的 lab-dev 环境中进行。
- 每一步都在 45 秒的评分预算内运行。不要添加真实的 sleep 或网络调用。
- 提交的测试会在另外的临时文件夹中,分别在正确实现和有缺陷的实现上运行。在正确实现上,实际运行的测试必须全部通过;在有缺陷的实现上,测试本体必须失败。收集错误、运行 0 个、全部跳过、被强制终止都不算通过。只使用 pytest 的基本功能和提供的库。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:TestClient 不是浏览器。它能检查 CORS 响应头和预检请求,但并没有实现浏览器自身对读取的拦截。带着未被允许的 Origin 发送的普通 GET,同样可能在服务器上被执行。敏感操作必须用独立的认证、权限和 CSRF 策略来保护。提供的实现可以阅读,但评分使用的是另外的副本。不要通过检查源码措辞或修改文件来绕过缺陷,而要检查公开接口的实际运行结果。
验证源的格式——测试
在 /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 确认。