HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户代理(一般是浏览器,以下统一用浏览器表示)并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。虽然 Cookie 具有许多降低其安全性的缺陷和隐私问题,但由于历史原因,Cookie 和 Set-Cookie 头字段还是被广泛的在互联网上使用。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie 曾一度用于浏览器数据的存储,但浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销,随着新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB ,Cookie 作为浏览器数据存储的功能渐渐被淘汰。

在 Chrome 浏览器中可以通过 chrome settings 管理浏览器 Cookie。也可以 F12 打开 dev-tools 在 Application-Storage-Cookie 下查看当前页面相关的 Cookie 。

另外还有 DNS CookiesTCP Cookie Transactions 也被叫做 Cookie,但都没有 HTTP Cookie 知名。

服务器使用 Set-Cookie 响应头部向浏览器发送 Cookie 信息

header
1
Set-Cookie: <cookie name>=<cookie value>[;Expires=<date>][;Max-Age=<non-zero-digit>][;Domain=<domain-value>][;Path=<path-value>][;Secure][;HttpOnly][;SameSite=<Strict|Lax|None>];

例如在 nodejs 中

1
response.setHeader('Set-Cookie', ['locale=zh-cn', 'language=javascript']);
  • <cookie-name> 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。同时不能包含以下分隔字符: ( ) < > @ , ; : \ “ / [ ] ? = { }.
  • <cookie-value> 是可选的,如果存在的话,那么需要包含在双引号里面。支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。关于编码:许多应用会对 Cookie 值按照 URL 编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过需要满足规范中对于 <cookie-value> 所允许使用的字符的要求。

浏览器使用 document.cookie API 获取 Cookie,使用 document.cookie = "<cookie name>=<cookie value>[;Expires=<date>][;Max-Age=<non-zero-digit>][;Domain=<domain-value>][;Path=<path-value>][;Secure][;SameSite=<Strict|Lax|None>]"; 来创建 Cookie。浏览器读取 Cookie 需要进行字符串解析与 Unicode 编码,直接使用 API 进行变更不太方便,有一些库可以更方便的对 Cookie 进行设置 例如 js-cookie

Cookie 并不直接提供修改、删除的操作,如果要修改某个 Cookie,只需要新建一个 Name、Domain、Path 均与原 Cookie 相同的 Cookie,即会覆盖原 Cookie。如果要删除某个Cookie,只需要新建一个 Name、Domain、Path 均与原 Cookie 相同的 Cookie,并将 Max-Age 设置为 0 或者 将 Expires 设置为过去的时间即可。

Cookie 具有以下属性
名称/值 Name Value
作用域 Domain Path
有效期 Expires Max-Age
安全属性 HttpOnly Secure SameSite

Domain 和 Path 标识定义了 Cookie 的作用域:即允许 Cookie 应该发送给哪些 URL。一般情况下,一个 Cookie 只有 Domain 和 Path 均比 HTTP(S) 请求的域名和路径更高层级(或同层级)时才会发送。 Name Domain Path 可以决定一个唯一的 Cookie,当设置一个 Name Domain Path 相同的 Cookie 时,Cookie 的值会被覆盖。

Domain

Domain=<domain-value> 指定 Cookie 可以送达的域名。默认值为当前文档访问地址中的域名(window.location.origin),而且不包含子域名(存储时 host-only-flag 为 true)。设定 Cookie 时如果指定了 Domain ,则包含子域名,而且 指定的 Domain 必须涵盖原始服务器的域名(属于当前域名的父级域名),否则会被浏览器拒绝。

Path

Path=<path-value> 指定 Cookie 可以送达的 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie header。默认值为当前文档的路径(window.location.pathname)的上一层。

Cookie 根据生命周期可以分为两种, 会话期 Cookie 和 持久性 Cookie。
不指定过期时间(Expires)或者有效期(Max-Age)的 Cookie 是会话期 Cookie,浏览器关闭之后它会被自动删除。(如果浏览器支持会话恢复功能,会话期 Cookie 会一直生效。)
持久性 Cookie 的生命周期取决于过期时间(Expires)或有效期(Max-Age)指定的一段时间。

Expires

Expires=<date> Cookie 的最长有效时间,形式为符合 RFC7231 HTTP-date 规范的时间戳。设置早于当前时间的 Expires 会使 Cookie 直接过期。

Max-Age

Max-Age=<non-zero-digit> 在 Cookie 失效之前需要经过的秒数。秒数为 0 或 -1 将会使 Cookie 直接过期。如果 Expires 和 Max-Age 均存在,那么 Max-Age 优先级更高。

Secure

Secure 一个带有 Secure 属性的 Cookie 只有在请求使用 HTTPS 协议的时候才会被发送到服务器。(但是在 Chrome 52 和 Firefox 52 以下版本中,仍旧能够在使用 HTTP 时被浏览器覆盖。)其目的是为了防范中间人攻击(MitM).

HttpOnly

HttpOnly 设置了 HttpOnly 属性的 Cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问。也即浏览器不能直接访问。其目的是为了防范跨站脚本攻击(XSS)。

SameSite

SameSite=<Strict|Lax|None> 允许服务器要求某个 Cookie 在跨站请求时不会被发送。其目的是为了阻止跨站请求伪造攻击(CSRF)和解决第三方 Cookie 导致的用户追踪问题。该属性在RFC6265bis中进行了定义。

  • Strict 完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。
  • Lax 允许部分第三方请求携带 Cookie。
  • None 无论是否跨站都会发送 Cookie,只支持 HTTPS(也即 Cookie 有 Secure 属性)。

SameSite 是 Chrome 51 新增的属性,从 Chrome 84 开始 默认值从 None 变更为 Lax。现在 Chrome 的 SameSite 默认值是 Lax ,而 Safari 的默认值是 Strict 。

请求类型 实例 Strict Lax None
链接 <a href="..."></a> 不发送 发送cookie 发送cookie
预加载 <link rel="prerender" href="..." /> 不发送 发送cookie 发送cookie
get 表单 <form method="GET" action="..." ></form> 不发送 发送cookie 发送cookie
post 表单 <form method="POST" action="..." ></form> 不发送 不发送 发送cookie
iframe <iframe src="..." /> 不发送 不发送 发送cookie
Ajax $.get("...") 不发送 不发送 发送cookie
Image <img src="..." /> 不发送 不发送 发送cookie
  • Secure- 前缀:以 Secure- 为前缀的 Cookie(其中连接符是前缀的一部分),必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS 访问的页面)。
  • Host- 前缀: 以 Host- 为前缀的 Cookie,必须与 secure 属性一同设置,必须应用于安全页面(即使用 HTTPS 访问的页面),必须不能设置 domain 属性 (也就不会发送给子域),同时 path 属性的值必须为“/”。

SameParty

同主体标志,结合 First-Party Sets 来判断不同域名是否为同一主体,是否需要发送 Cookie 。SameParty Cookie 必须包含 Secure 且不得包含 SameSite=Strict。该属性主要是为了限制第三方 Cookie 及解决同一主体拥有不同域名时跨站 Cookie 共享的问题。

Priority

当 Cookie 超过每个域的 Cookie 容量时,将会被删除。Priority 属性(分为 High Medium(默认) Low)决定较低优先级的旧 Cookie 会被优先删除。

Cookie的发送规则

浏览器会将 Domain(当前请求的域名和 Cookie 的 Domain 相同,或者 Cookie 的 Domain 符合请求的上级域名)、Path(当前请求的路径和 Cookie 的 Path 相同,或者 Cookie 的 Path 符合请求的父级路径)、 Secure(当前请求为https请求) 均符合的 Cookie 按照 Path 长的在前、创建时间早的在前的规则发送给服务器,只发送 Name 和 Value。

  • 单个 Cookie 最大 4k,任何 Cookie 大小超过限制都被忽略
  • 单个 域名 至少提供50条 Cookie
  • 总共 提供 至少 3000条 Cookie

根据这些限制条件,浏览器随时可能清除 Cookie,服务器应当使 Cookie 的体积小,数量少。

其他安全策略与问题

  • 为了防止 Cookie 被篡改,服务器通常可以将 会话标志符 存储在 Cookie 中,而不是直接存储会话信息。这样,即使攻击者获取到了 Cookie 内容,如果不知道会话标志符编码解码规则,就无法进行修改。

  • 如果站点使用 Cookie 对用户进行身份验证,则每当用户进行身份验证时,它都应重新生成并重新发送会话 Cookie,即使会话 Cookie 已经存在。此技术有助于防止会话固定攻击(session fixation attacks)。

  • Cookie 不提供端口隔离。如果 cookie 可由运行在一个端口上的服务读取/写入,则该 cookie 也可由运行在同一服务器的另一个端口上的服务读取/写入。出于这个原因,当使用 Cookie 来存储安全敏感信息时,服务器不应该在同一主机的不同端口上运行互不信任的服务。

  • Cookie 不对路径提供完整性保护。document.cookie API 不会隔离从不同路径获取的 Cookie,浏览器会接受 Set-Cookie 头中的任意 Path 属性 。出于这个原因,当使用 Cookie 来存储安全敏感信息时,服务器不应该在同一主机的不同路径上运行互不信任的服务。

  • Cookie 不为同级域(及其子域)提供完整性保证。例如,考虑 foo.example.com 和 bar.example.com。foo.example.com 服务器可以设置一个域属性为 “example.com” 的 cookie (可能覆盖由 bar.example.com 设置的现有 “example.com” cookie ),并且浏览器会将该 cookie 包含在对 bar.example.com 的 HTTP 请求。这里面最坏的情况是,bar.example.com 将无法区分这个 cookie和它自己设置的 cookie。foo.example.com 服务器可能能够利用这种能力对 bar.example.com 发起攻击。

  • Cookie 是依赖 DNS 来确保安全的,如果 DNS 层面已经不安全了(比如发生 DNS劫持),那 Cookie 也无法保证安全。

参考资料

RFC6265 HTTP State Management Mechanism
MDN Web Docs HTTP cookies
MDN Web Docs Set-Cookie
MDN Web Docs Using HTTP cookies
MDN Web Docs Document.cookie
MDN Web Docs Type of attacks
MDN Web Docs MitM
Github first-party-sets
Github sameparty
web.dev samesite-cookies-explained
图解 http