bigywater.tech
Flask · 128

CORS 安全边界

CORS 配置写成什么样才算安全?带 cookie 的接口能用通配符吗?用正则匹配来源要注意什么?CORS 能挡住 curl 吗?这篇只检查响应头:对几种配置分别发不同的 Origin,看服务端回什么。

任意 Origin
默认或 "*" 加 supports_credentials 时回显的来源范围
evilexample.com
不严的正则 https://.*example.com 会放行的别人的域名
200
Origin 不在名单里的请求,服务端仍然处理并返回的状态码

常见配置对不同 Origin

实测 · 本机执行输出
  [origins 不写(默认 *)]
     Origin: https://app.example.com  → {'Access-Control-Allow-Origin': 'https://app.example.com', 'Vary': 'Origin'}
     Origin: https://other.test       → {'Access-Control-Allow-Origin': 'https://other.test', 'Vary': 'Origin'}
     Origin: null                     → {'Access-Control-Allow-Origin': 'null', 'Vary': 'Origin'}
  [origins="*" + supports_credentials=True]
     Origin: https://app.example.com  → {'Access-Control-Allow-Origin': 'https://app.example.com', 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin'}
     Origin: https://other.test       → {'Access-Control-Allow-Origin': 'https://other.test', 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin'}
     Origin: null                     → {'Access-Control-Allow-Origin': 'null', 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin'}
  [origins=[自己的前端]]
     Origin: https://app.example.com  → {'Access-Control-Allow-Origin': 'https://app.example.com'}
     Origin: https://other.test       → {}
     Origin: null                     → {}
  [origins=[自己的前端] + supports_credentials]
     Origin: https://app.example.com  → {'Access-Control-Allow-Origin': 'https://app.example.com', 'Access-Control-Allow-Credentials': 'true'}
     Origin: https://other.test       → {}
     Origin: null                     → {}

上一篇:Host Header 校验;下一篇:Rate Limiting。

第 79 篇与第 112 篇在真实浏览器里验证过 CORS 与预检,这一篇只看响应头与配置边界。

正则匹配的陷阱

实测 · 本机执行输出
  [r"https://.*example.com"(不严)]
     https://app.example.com                    → 放行
     https://evilexample.com                    → 放行
     https://app.example.com.other.test         → 放行
     http://app.example.com                     → 拒绝
     https://sub.app.example.com                → 放行
  [r"https://app\.example\.com$"(锚定)]
     https://app.example.com                    → 放行
     https://evilexample.com                    → 拒绝
     https://app.example.com.other.test         → 拒绝
     http://app.example.com                     → 拒绝
     https://sub.app.example.com                → 拒绝
  [r"https://([a-z0-9-]+\.)?example\.com$"(允许子域)]
     https://app.example.com                    → 放行
     https://evilexample.com                    → 拒绝
     https://app.example.com.other.test         → 拒绝
     http://app.example.com                     → 拒绝
     https://sub.app.example.com                → 拒绝

CORS 不是认证

实测 · 本机执行输出
  Origin 不在名单: 200 {'ok': True} | 有没有 Allow-Origin: False
  根本不带 Origin(curl、服务器到服务器): 200 {'ok': True}

Vary: Origin

实测 · 本机执行输出
  Origin=app → {'Access-Control-Allow-Origin': 'https://app.example.com', 'Vary': 'Origin'}
  Origin=admin → {'Access-Control-Allow-Origin': 'https://admin.example.com', 'Vary': 'Origin'}

限定路径与方法

实测 · 本机执行输出
  /api/x     {'Access-Control-Allow-Origin': 'https://app.example.com'}
  /admin/y   {}
  预检请求 DELETE /api/x(只允许 GET)→ 200 {'Access-Control-Allow-Origin': 'https://app.example.com'}

分步演示

CORS 安全边界
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-CORS 6.0.5
    • 没有验证:真实浏览器里跨站读取的实际拦截(见第 79 篇)、Access-Control-Allow-Private-Network、反向代理层同时加 CORS 头造成重复头的问题、null Origin 的来源场景(沙箱 iframe、file://)的浏览器行为
    京ICP备2026067502号-1京公网安备11010502063487号