浏览器会悄悄丢弃不合规则的 Cookie
一句话总结
Cookie 是服务器托付给浏览器保管的一小段值,六个属性决定它发往哪里、保留多久、让谁看到。会话 Cookie 的这个值本身就等于登录状态,所以只要少一个属性,规则就会失效;而浏览器会对不符合规则的 Cookie 不报任何错误地直接丢弃。因此对 Cookie 要确认的不是“已发送”,而是“已被接受”。
为什么需要它
HTTP 的每个请求都没有记忆。要在下一个请求中认出已登录的人,就必须让对方每次重新发送某样东西,而这样东西就是 Cookie。它方便,是因为浏览器会自动附带 Cookie;它危险,也是同一个原因。如果服务器没有明确说明该附加到哪些请求、是否对脚本可见、是否允许通过明文 HTTP 发送,浏览器就会按最宽松的方式行事。
原始规范 RFC 6265 并不掩饰这种宽松。Cookie 不按端口隔离,所以同一主机上其他端口运行的服务也能读取同一个 Cookie。如果 foo.example.com 用 Domain=example.com 设置 Cookie,就可以覆盖 bar.example.com 的 Cookie,而接收方的服务器无法分辨它是谁设置的。规范中还写明,用 Path 划分路径也不能当作安全边界。属性和名称前缀,是后来为逐个收窄这些漏洞而补充的机制。
工作原理
属性以分号接在 Set-Cookie 一行后面。按 MDN Set-Cookie 整理如下。
Secure HTTPS 요청에만 붙인다
HttpOnly document.cookie 로 읽을 수 없다. 요청에는 그대로 붙는다
SameSite 다른 사이트에서 시작된 요청에 붙일지 (Strict / Lax / None)
Path 이 경로 아래 요청에만 붙인다. 보안 경계는 아니다
Domain 생략하면 보낸 호스트에만(호스트 전용), 적으면 그 도메인과 하위 전부
Max-Age/Expires 없으면 세션 쿠키, 있으면 영속 쿠키. 둘 다 있으면 Max-Age 가 이긴다
与直觉相反的是 Domain。写成 Domain=app.example.com 看起来范围会更窄,实际上 x.app.example.com 这样的子主机也会收到该 Cookie。范围最窄的做法是省略它。OWASP 会话管理备忘单 也建议干脆去掉 Domain。同一份文档还说不要把会话 Cookie 设为持久——让它在关闭浏览器时消失更好。持久 Cookie 也有上限:RFC 6265bis 草案 写明,过期时间不应超过 400 天。
HttpOnly 阻止的只有一件事,即脚本读取值。MDN 明确写道,HttpOnly Cookie 同样会附加在 fetch() 请求上。页面存在 XSS 时,攻击脚本只是偷不走 Cookie,仍然可以在该浏览器内以用户身份发送请求。HttpOnly 不是 XSS 防御,而是减小损失的机制。
名称前缀弥补了 Cookie 存储的弱点。存储只按名称、域、路径区分 Cookie,并不记录是谁设置的。前缀把条件刻进名称本身,这样服务器就能相信,只要存在该名称的 Cookie,它就是通过了这些条件才被存储的。以下依据的是 RFC 6265bis 草案(第 22 版)的存储模型。
__Secure- Secure 가 있어야 한다
__Host- Secure 가 있고, Domain 이 없고(호스트 전용), Path 속성이 / 여야 한다
브라우저는 접두를 대소문자 구분 없이 대조한다 (__host- 도 같은 규칙)
同一份草案要求忽略由非安全连接的响应发送的 Secure Cookie。MDN 把 localhost 列为例外,而本实验 Pod 中的 curl 8.5 即使对通过 http://127.0.0.1 收到的 Secure Cookie 也会保存并回传(实测)。所以本实验用明文也能运行。
会话 ID 按 OWASP 的标准,应由密码学安全的随机数生成器生成,并具有至少 64 位熵。如果用 Python,secrets.token_urlsafe(32) 就有 256 位。退出登录必须同时做两件事:使服务器端会话失效,并用 Max-Age=0 删除 Cookie。这时删除用的 Cookie 也必须通过前缀规则。用 curl 8.5 重新测试,缺少 Secure 的 __Host-sid=; Max-Age=0 被忽略,旧 Cookie 仍留在存储中。
在现场相遇的样子
很多应用的退出登录按钮只会删除 Cookie。界面上看起来已经退出,但如果用代理日志里或别人分享的 HAR 文件里残留的值发起请求,仍然处于登录状态。服务器没有清除记忆,所以那个值依然是钥匙。
另一种情况是“只有预发布环境登录不了”。预发布环境是明文 HTTP,而 Cookie 上带着 Secure,或者有人给 __Host- Cookie 加上了 Domain。服务器日志里 Set-Cookie 正常发出,浏览器却悄悄丢弃,所以要看的不是网络标签页里的响应,而是已存储的 Cookie 列表,才能看到原因。
下一项实验要做什么
在 127.0.0.1:8301 上搭建登录服务器,签发 __Host-sid 会话 Cookie。在响应头中确认属性,并查看 curl 的 Cookie 存储中是否以 #HttpOnly_ 标记保留。退出登录时,以不清除服务器会话这一常见错误为反例,亲自复现复制保存的旧 Cookie 是否会变成 401。最后按前缀规则判定多行 Set-Cookie。