
接口在 http://127.0.0.1:8812 下发 Cookie,页面从 http://localhost:8811 跨站发请求,两个端口都是本机真实服务,请求由本机 Chrome 发出,服务端把每条到达请求的方法与路径记下来,加上来源页和 Cookie 名字,Cookie 只记名字不记值。整套跑法在 tools/verify-cookie-scope.mjs,制表脚本 tools/verify-cookie-scope-table.py。
六种属性里,只有 None 加 Secure 落了盘
| 下发的属性 | 跨站 fetch(credentials: include) |
|---|---|
| 不写 SameSite(默认 Lax) | 没带 |
| SameSite=Lax | 没带 |
| SameSite=Strict | 没带 |
| SameSite=None; Secure | 带上 |
| SameSite=None 没加 Secure | 没带 |
| HttpOnly; SameSite=None; Secure | 带上 |
前两行与第三行的区别在浏览器行为上不明显,在服务端记录上却能分清:不写 SameSite 的那条 Cookie 从来没有出现在任何后续请求里,说明它在跨站响应里就被丢掉了,与显式写 Lax 一样被拒收。SameSite=None 却没加 Secure 的那条也是同样的下场,名字一次都没出现过。整张表里只有带 None 与 Secure 的两条真正落盘并随请求回来了。
路径前缀、credentials 与删除时机
| 场景 | 结果 |
|---|---|
| Path=/a 的 Cookie,请求 /a/echo | 带上 |
| Path=/a 的 Cookie,请求 /echo | 没带 |
| SameSite=None 的 Cookie,fetch 没写 credentials | 没带 |
| Max-Age=0 之前 | 带上 |
| Max-Age=0 之后 | 没带 |
Path 那一行与第三行说明两件事:Cookie 的可见范围按路径前缀判,请求路径不在前缀里就完全看不到;跨站请求即使 Cookie 本身合法,少了 credentials: include 也不会带上,这一条经常被当成 CORS 头没配对,其实开关在发请求那一侧。Max-Age=0 的删除立即生效,删除后的请求里名字就不见了。
document.cookie 里为什么少一个 f_httponly
同一批 Cookie 在顶层导航里表现完全不同。页面自己跳转(同站)时,服务端收到的名字是 sid, c_none_secure, c_httponly, f_default, f_lax, f_strict, f_none, f_httponly,八条全到。从 localhost 那个页面发起的跨站跳转,服务端收到的是 sid, c_none_secure, c_httponly, f_default, f_lax, f_none, f_httponly,少了 f_strict,这正是 Lax 与 Strict 的分界:Lax 允许顶层导航携带,Strict 不允许。
document.cookie 那一侧是另一套限制。第一方页面里读出来的字符串是 sid=abc123; c_none_secure=1; f_default=1; f_lax=1; f_strict=1; f_none=1,五个 f_ 名字里唯独少了 f_httponly,而服务端在同一个页面的后续请求里收到了它。HttpOnly 的含义就在这里:脚本读不到,请求照样带。另外页面跑在 localhost、Cookie 属于 127.0.0.1,所以 document.cookie 里也读不到 c_ 开头的那些,前半段测出来的「浏览器可见:否」就是这个原因,与 HttpOnly 无关,别把两件事混在一起看。
本机 Chrome 153 上的三条边界
要给跨站请求下发 Cookie,属性必须写全 SameSite=None; Secure,其余组合在本次实测里都被丢弃。要读 document.cookie 又不想被脚本拿走的值,加 HttpOnly。跨站 fetch 想带上 Cookie,credentials: include 不能漏,这比调 CORS 响应头更常见。
这几组数字全部来自本机 Chrome 153 的默认配置,第三方 Cookie 策略近两年改动频繁,换版本或换 Safari、Firefox 结果可能不同,这一部分我没有跨浏览器验证。日志里那条 sid=abc123 是早前测试留在浏览器 profile 里的 Cookie,正好顺带说明同一 profile 的历史 Cookie 会一起带上,做这类实验时最好用干净 profile。Secure 属性在 http://127.0.0.1 上能被正常接收并回传,因为本机地址被浏览器当成可信来源,换成一个真实域名走 http,这一条就没法照搬。
来源:tools/verify-cookie-scope.mjs 的服务端日志(页面跑在 http://localhost:8811,接口在 http://127.0.0.1:8812,跨站)
=== 一、跨站 fetch(页面 localhost → 接口 127.0.0.1,credentials: include)===
场景 这一条 Cookie 带上了吗
不写 SameSite(默认 Lax) 没带
SameSite=Lax 没带
SameSite=Strict 没带
SameSite=None; Secure 带上
SameSite=None 没加 Secure 没带
HttpOnly + None + Secure 带上
=== 二、Path 作用域与删除 ===
Path=/a 的 Cookie,请求 /a/echo 带上(服务端看到:c_path,c_none_secure,c_httponly)
Path=/a 的 Cookie,请求 /echo 没带(服务端看到:c_none_secure,c_httponly)
SameSite=None 的 Cookie,但 fetch 没写 credentials没带(服务端看到:(无))
Max-Age=0 之前 带上(服务端看到:c_none_secure,c_httponly,c_del)
Max-Age=0 之后 没带(服务端看到:c_none_secure,c_httponly)
=== 三、顶层导航(同一个浏览器,两种发起方)===
发起页面 http://127.0.0.1:8812/page 请求 /echo?case=nav_same_site
服务端收到 Cookie: ['sid', 'c_none_secure', 'c_httponly', 'f_default', 'f_lax', 'f_strict', 'f_none', 'f_httponly']
发起页面 http://localhost:8811/ 请求 /echo?case=nav_from_localhost
服务端收到 Cookie: ['sid', 'c_none_secure', 'c_httponly', 'f_default', 'f_lax', 'f_none', 'f_httponly']
=== 四、第一方页面(http://127.0.0.1:8812/page)里 document.cookie ===
响应头种下: f_default=1; Path=/ | f_lax=1; Path=/; SameSite=Lax | f_strict=1; Path=/; SameSite=Strict | f_none=1; Path=/; SameSite=None; Secure | f_httponly=1; Path=/; HttpOnly
同站导航时服务端收到: sid, c_none_secure, c_httponly, f_default, f_lax, f_strict, f_none, f_httponly
浏览器侧取回的原始结果(tools/cdp-run.mjs 读 window.__results):
{"documentCookie":"sid=abc123; c_none_secure=1; f_default=1; f_lax=1; f_strict=1; f_none=1","visible":["sid","c_none_secure","f_default","f_lax","f_strict","f_none"]}