Secure、HttpOnly 和 SameSite 分别描述支持这些规则的浏览器如何限制 Cookie。服务端收到的是最终的名称与值,而不是一张证明这些限制已经执行的证书。浏览器行为与服务端授权必须分开:Cookie 完全可能被正确发送,但当前请求的业务动作仍然不被允许,不能把出现 Cookie 头当成全部安全条件已经满足。
分开看设置、发送与脚本读取
Secure 限制 Cookie 通过安全连接发送,通常就是 HTTPS。现代浏览器 Cookie 规则还会拒绝从不安全语境设置 Secure Cookie;localhost 等开发例外应在目标浏览器中核对,而不能推广为任意明文主机都可信。这不仅是发送阶段的标记。一次 HTTPS 重定向无法撤回此前明文请求已经泄露的令牌,因此应结合 HTTPS 与适当的 HSTS 策略,而不是让两者互相替代。
HttpOnly 阻止通过 document.cookie 等非 HTTP 脚本接口读取 Cookie,但不会禁止浏览器把它附到符合条件的脚本请求上。被注入的同源 JavaScript 即使读不到令牌,也仍可能以用户身份发起操作。因此必须单独预防和限制 XSS;HttpOnly 减少的是其中一条窃取路径,并不是把脚本注入漏洞消除了。
SameSite 增加请求语境限制。Strict 通常允许同站请求,Lax 还允许使用安全方法的跨站顶层导航。显式 Lax 不会仅因为某次跨站 POST 是从顶层页面发起,就让 Cookie 自动附上。None 去掉这一项限制,但在支持规则的浏览器中必须搭配 Secure。第三方 Cookie 阻止、存储分区以及 Fetch 的凭据模式,仍可能阻止发送,不能把 None 理解成任何请求都无条件携带。
站点与来源不是同一回事
对于普通 HTTPS 域名,相关的带协议站点边界由协议和可注册域构成。两个 HTTPS 兄弟子域可以同站,却仍然不同源。端口变化也会造成不同源,而未必改变站点。因此,Strict 不承诺排除所有跨源请求;同站兄弟应用被攻陷,仍然是评估 CSRF 防护时必须考虑的场景,不能只测试完全无关的外部域名。
省略或填写无效 SameSite,也不能可移植地等同于显式 Lax。规范允许针对最近创建、没有显式值的 Cookie 使用兼容模式,使部分不安全方法的顶层请求也携带它。常见的两分钟窗口是面向实现的兼容性限制,不是每一种浏览器都必须采用的保证。应写出预期值,并测试真正支持的浏览器版本,而不是依靠默认值的模糊印象。
坏掉的样子
会修改状态的 GET 在 Lax 下仍有危险,因为跨站顶层的安全方法导航可以携带 Cookie。修改状态应采用合适的方法和服务端 CSRF 防护。反过来,Strict 可能扣住跨站身份提供方回调所需的 Cookie。要检查真实的重定向链与回调方法;并非所有单点登录都会失败,而且单独打开一个书签,也不能证明发生了跨站重定向。
应使用身份库支持的关联校验与回调设计,而不是不加区分地放宽主要会话。Origin 或 Fetch Metadata 校验可以发挥作用,但需要明确处理头字段缺失、兼容客户端和合法跨站流程。非浏览器客户端可以伪造这些头,所以它们是浏览器语境信号,不是凭据。已经持有被盗会话令牌的客户端,需要认证和撤销机制来处理,不能指望再检查一次 SameSite 就把它识别出来。
快速验证
把代码保存为 cookie-flags.py,使用 Python 3.11 或更新版本执行 python3 cookie-flags.py。它使用真实标准库 CookieJar 与虚构响应元数据,不发送网络请求。目的在于展示这个 HTTP 客户端的 Cookie 政策与浏览器政策之间的差别,不是建立一个假的浏览器,再把模型结果冒充成真实浏览器行为。
from email.message import Message
from http.cookiejar import CookieJar
from http.cookies import SimpleCookie
from urllib.request import Request
class Response:
def info(self):
headers = Message()
for value in [
"secure_sid=synthetic; Path=/; Secure; HttpOnly; SameSite=Lax",
"strict_pref=synthetic; Path=/; SameSite=Strict",
"insecure_none=synthetic; Path=/; SameSite=None",
]:
headers.add_header("Set-Cookie", value)
return headers
jar = CookieJar()
jar.extract_cookies(Response(), Request("https://app.example.com/login"))
def outgoing(url):
request = Request(url)
jar.add_cookie_header(request)
header = request.get_header("Cookie", "")
parsed = SimpleCookie()
parsed.load(header)
assert "SameSite" not in header and "HttpOnly" not in header
return set(parsed)
assert outgoing("http://app.example.com/") == {"strict_pref", "insecure_none"}
assert outgoing("https://app.example.com/") == {
"secure_sid", "strict_pref", "insecure_none"
}
manual = Request("http://app.example.com/",
headers={"Cookie": "secure_sid=synthetic"})
assert manual.get_header("Cookie") == "secure_sid=synthetic"
print("PASS: CookieJar filters Secure; it does not enforce SameSite")
print("PASS: request headers omit attributes; a manual header bypasses the jar")
CookieJar 在 HTTP 上扣住 secure_sid,在 HTTPS 上发送它,即使它带有 HttpOnly。与此同时,它接受 insecure_none,也会在没有建模页面同站语境的情况下发送 strict_pref。后两项结果展示的是这个客户端的局限,不是浏览器的预期。手动构造 Cookie 头还可以完全绕过 CookieJar。本实验没有建立任何服务端响应或浏览器结果,也没有验证真正的 CSRF 拒绝路径。
验收浏览器时,应使用隔离的同站与跨站测试页面、虚构取值,分别检查 Cookie 能否存入以及实际是否随请求发送。覆盖顶层 GET、POST 回调、嵌入式 fetch,以及应用使用的凭据模式。用 curl 伪造 Sec-Fetch-Site 头无法证明 SameSite 已被执行,一个任意 HTTP 状态码也不能证明 Cookie 被接受或业务动作已获授权;应收集与问题直接相关的两端证据。
来源
- Cookie 规范草案第 22 版,存储、发送、SameSite 与可选兼容行为,已核验 2026-10-15。
- Python CookieJar,客户端 Cookie 处理及其文档列明的协议范围,已核验 2026-10-15。
- OWASP CSRF 防护,SameSite 局限、来源与 Fetch Metadata 防御,已核验 2026-10-15。