浏览器 Cookie 是 Web 会话管理的基石,但也是最常被误解和误用的安全机制之一。根据 OWASP 2025 年度报告,超过 60% 的 Web 应用存在 Cookie 配置不当的问题,其中 HttpOnly 缺失和 SameSite 未设置是最常见的两类漏洞。在第三方 Cookie 逐步淘汰、CHIPS(Cookies Having Independent Partitioned State)成为新标准的 2026 年,理解 Cookie 安全不仅是防御 XSS 和 CSRF 的基础,更是现代 Web 架构设计的必修课。
🔐 一、Cookie 安全属性深度解析
很多开发者知道 HttpOnly 和 Secure 这两个属性,但对它们的工作原理和组合效果却知之甚少。我们先从基础开始,逐个拆解每个安全属性的实际作用。
1.1 HttpOnly:阻断 XSS 的 Cookie 窃取
HttpOnly 属性是防御 XSS(Cross-Site Scripting)攻击的第一道防线。当一个 Cookie 被标记为 HttpOnly 时,JavaScript 无法通过 document.cookie 读取它,这意味着即使攻击者成功注入了 XSS 脚本,也无法直接窃取用户的会话令牌。
// ❌ 危险:未设置 HttpOnly 的 Cookie
// 服务端设置
Set-Cookie: session_id=abc123; Path=/
// 攻击者的 XSS 脚本可以轻松窃取
const stolen = document.cookie
// => "session_id=abc123" — 攻击者拿到了用户的会话!
// ✅ 安全:设置了 HttpOnly 的 Cookie
// 服务端设置
Set-Cookie: session_id=abc123; Path=/; HttpOnly
// 攻击者的 XSS 脚本无法读取
const stolen = document.cookie
// => "" — 空字符串,Cookie 被保护了!
⚠️ 警告:
HttpOnly不能防御 XSS 本身,它只是阻止 XSS 攻击者直接窃取 Cookie。攻击者仍然可以通过 XSS 发起伪造请求(借助浏览器自动携带 Cookie 的特性)。所以HttpOnly必须与 CSP(Content Security Policy)配合使用。
1.2 SameSite:CSRF 防护的终极方案
SameSite 属性是近年来 Cookie 安全最重要的进展。它通过限制跨站请求中 Cookie 的发送行为,从浏览器层面阻断了 CSRF(Cross-Site Request Forgery)攻击。
SameSite 有三个值,它们的行为差异非常大:
| 属性值 | 跨站 GET 请求 | 跨站 POST 请求 | 跨站 <img>/<iframe> |
CSRF 防护 | 推荐场景 |
|---|---|---|---|---|---|
None |
✅ 发送 | ✅ 发送 | ✅ 发送 | ❌ 无防护 | 跨站 SSO(必须配合 Secure) |
Lax |
✅ 发送 | ❌ 不发送 | ❌ 不发送 | ⚠️ 部分防护 | 大多数 Web 应用的默认推荐 |
Strict |
❌ 不发送 | ❌ 不发送 | ❌ 不发送 | ✅ 完整防护 | 高安全场景(银行、支付) |
// ❌ 过去的默认行为(现在已不安全)
Set-Cookie: session_id=abc123; Path=/
// ✅ 推荐:使用 SameSite=Lax(现代浏览器的默认值)
Set-Cookie: session_id=abc123; Path=/; SameSite=Lax; HttpOnly; Secure
// ✅ 高安全场景:使用 SameSite=Strict
// 注意:用户从外部链接点击进入时不会携带 Cookie,需要二次验证
Set-Cookie: session_id=abc123; Path=/; SameSite=Strict; HttpOnly; Secure
💡 提示: 从 Chrome 80+ 开始,未设置
SameSite的 Cookie 默认行为已从None改为Lax。这意味着如果你的代码没有显式设置SameSite,跨站 POST 请求将不会携带 Cookie,这可能会破坏一些依赖 Cookie 的第三方集成。
1.3 Secure 与 __Host- 前缀
Secure 属性确保 Cookie 只在 HTTPS 连接中传输,防止中间人攻击窃取 Cookie。而 __Host- 前缀是更进一步的安全加固:
// 使用 __Host- 前缀的 Cookie 必须满足:
// 1. 必须设置 Secure
// 2. 不能设置 Domain(锁定到当前域名)
// 3. 必须设置 Path=/
// ✅ 使用 __Host- 前缀的高安全 Cookie
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax
// ❌ 这会失败 — __Host- 不能设置 Domain
Set-Cookie: __Host-session=abc123; Path=/; Secure; Domain=example.com
__Host- 前缀的价值在于它能防止子域名覆盖上级域名的 Cookie。考虑这个攻击场景:攻击者控制了 evil.example.com,设置了一个 Domain=.example.com 的 Cookie 来覆盖 example.com 的会话。如果原始 Cookie 使用了 __Host- 前缀,这种攻击就无法成功。
🚀 二、CHIPS 与分区 Cookie:第三方 Cookie 的替代方案
2026 年,Chrome 已经全面淘汰第三方 Cookie,取而代之的是 CHIPS(Cookies Having Independent Partitioned State)。这是理解现代 Cookie 安全的关键变化。
2.1 什么是 CHIPS?
传统的第三方 Cookie 有一个致命问题:它允许跨站追踪。广告网络通过在多个网站上嵌入 <iframe> 或 <script>,利用第三方 Cookie 追踪用户行为。
CHIPS 通过"分区"机制解决了这个问题:Cookie 的作用域不再只是 Domain + Path,而是变成了 Top-Level Site + Domain + Path。这意味着 tracker.com 在 site-a.com 和 site-b.com 上设置的 Cookie 将被隔离,无法互相访问。
// 传统第三方 Cookie(已废弃)
// tracker.com 在 site-a.com 上设置
Set-Cookie: user_id=123; Domain=tracker.com; SameSite=None; Secure
// 在 site-b.com 的请求中,这个 Cookie 也会被发送
// => tracker.com 可以跨站追踪用户!
// CHIPS 分区 Cookie(新标准)
// tracker.com 在 site-a.com 上设置
Set-Cookie: user_id=123; Domain=tracker.com; SameSite=None; Secure; Partitioned
// 在 site-b.com 的请求中,这个 Cookie 不会被发送
// => 用户隐私得到保护
2.2 CHIPS 迁移实战
如果你的服务涉及跨站 Cookie(如嵌入式支付、第三方登录、CDN 服务),需要尽快迁移到 CHIPS:
// Node.js Express 示例:设置分区 Cookie
const express = require('express');
const app = express();
app.get('/embed/widget', (req, res) => {
// ❌ 旧方式:第三方 Cookie(Chrome 已阻止)
res.cookie('widget_session', 'abc123', {
domain: '.widget-service.com',
secure: true,
sameSite: 'none',
});
// ✅ 新方式:CHIPS 分区 Cookie
res.cookie('widget_session', 'abc123', {
domain: '.widget-service.com',
secure: true,
sameSite: 'none',
partitioned: true, // 关键:添加分区标记
});
res.json({ status: 'ok' });
});
// Spring Boot 示例:设置分区 Cookie
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
@RestController
public class WidgetController {
@GetMapping("/embed/widget")
public ResponseEntity<Map<String, String>> getWidget(
HttpServletResponse response) {
Cookie cookie = new Cookie("widget_session", "abc123");
cookie.setDomain(".widget-service.com");
cookie.setSecure(true);
cookie.setPath("/");
cookie.setAttribute("SameSite", "None");
cookie.setAttribute("Partitioned", ""); // CHIPS 分区标记
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("status", "ok"));
}
}
📌 记住: 使用
Partitioned属性时,SameSite=None和Secure是必须的。分区 Cookie 天然要求 HTTPS 环境。
2.3 CHIPS 对架构的影响
CHIPS 不仅仅是加一个属性那么简单,它对系统架构有深远影响:
| 影响维度 | 第三方 Cookie 时代 | CHIPS 时代 | 需要的改造 |
|---|---|---|---|
| 跨站 SSO | 一个 Cookie 全站通用 | 每个站点独立的 Cookie | 改用 OAuth2 + 前端 Token |
| 嵌入式分析 | <iframe> 共享 Cookie |
Cookie 被分区隔离 | 改用 Server-Side 事件上报 |
| CDN 认证 | 共享认证 Cookie | 无法跨站共享 | 改用签名 URL 或 Token |
| 支付网关 | 嵌入式 Cookie 追踪 | Cookie 被分区 | 改用 Payment Request API |
⚠️ 三、常见安全陷阱与避坑指南
3.1 陷阱一:Cookie 覆盖攻击
子域名可以为父域名设置 Cookie,这是一个常被忽视的安全风险:
// 攻击者控制了 evil.example.com,执行以下代码
document.cookie = "session_id=hacked; Domain=.example.com; Path=/";
// 现在访问 example.com 时,这个恶意 Cookie 会被发送
// 如果服务端只检查 session_id 的值而不验证来源,就会被攻击
防御方案:
// ✅ 使用 __Host- 前缀阻止 Cookie 覆盖
// 服务端设置(只能通过 HTTPS 从精确域名设置)
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax
// ✅ 服务端验证 Cookie 的完整性
const crypto = require('crypto');
function signCookie(value, secret) {
const signature = crypto
.createHmac('sha256', secret)
.update(value)
.digest('base64url');
return `${value}.${signature}`;
}
function verifyCookie(signed, secret) {
const [value, sig] = signed.split('.');
const expected = crypto
.createHmac('sha256', secret)
.update(value)
.digest('base64url');
return sig === expected ? value : null;
}
// 设置签名 Cookie
const sessionId = generateSessionId();
const signed = signCookie(sessionId, process.env.COOKIE_SECRET);
res.cookie('__Host-session', signed, {
path: '/',
secure: true,
httpOnly: true,
sameSite: 'lax',
partitioned: false,
});
3.2 陷阱二:LocalStorage 替代 Cookie 的误区
很多开发者认为"把 Token 存在 LocalStorage 就安全了",这是一个严重的误解:
| 存储方式 | XSS 防护 | CSRF 防护 | 自动发送 | 推荐场景 |
|---|---|---|---|---|
| Cookie + HttpOnly | ✅ JS 无法读取 | ⚠️ 需要 SameSite | ✅ 浏览器自动 | 传统 Web 应用 |
| LocalStorage + Bearer Token | ❌ XSS 可读取 | ✅ 不会自动发送 | ❌ 需手动添加 | SPA + API 网关 |
| In-Memory Token | ✅ 刷新即丢失 | ✅ 不会自动发送 | ❌ 需手动添加 | 高安全 SPA |
| Cookie(无 HttpOnly) | ❌ XSS 可读取 | ❌ 可能被 CSRF | ✅ 浏览器自动 | ❌ 不推荐 |
⚡ 关键结论: 最安全的 SPA 方案是将 Access Token 存储在内存中(非 LocalStorage),配合 HttpOnly Refresh Cookie。这样即使 XSS 发生,攻击者只能拿到短期有效的 Access Token;而 Refresh Token 由于 HttpOnly 保护无法被 JavaScript 读取。
3.3 陷阱三:Session 固定攻击
Session 固定(Session Fixation)是一种容易被忽视的攻击方式。攻击者先获取一个有效的 Session ID,然后诱导用户使用这个 Session ID 登入:
// ❌ 危险:登录后不重新生成 Session ID
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);
if (user) {
req.session.userId = user.id; // 复用了旧的 Session ID
res.json({ success: true });
}
});
// ✅ 安全:登录后重新生成 Session ID
const crypto = require('crypto');
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);
if (user) {
// 保存旧 Session 数据
const oldSession = { ...req.session };
// 重新生成 Session ID
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
// 恢复 Session 数据
Object.assign(req.session, oldSession);
req.session.userId = user.id;
res.json({ success: true });
});
}
});
3.4 陷阱四:Cookie 的 Domain 属性陷阱
// ❌ 危险:设置过宽的 Domain
Set-Cookie: session=abc; Domain=.com; Secure
// 这会让所有 .com 网站都能访问这个 Cookie!
// ❌ 常见错误:多加了一个点
Set-Cookie: session=abc; Domain=.example.com; Secure
// 虽然浏览器会忽略开头的点,但语义上不清晰
// ✅ 正确:不设置 Domain(默认锁定到当前精确域名)
Set-Cookie: session=abc; Path=/; Secure; HttpOnly; SameSite=Lax
// 只有 example.com 可以访问,子域名不能访问
// ✅ 需要子域名共享时:明确设置
Set-Cookie: session=abc; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax
// example.com 和 sub.example.com 都可以访问
💡 四、现代 Cookie 安全最佳实践
4.1 完整的 Cookie 安全配置清单
根据你的应用场景,选择合适的安全配置:
// 场景 1:传统 Web 应用的会话 Cookie(推荐配置)
Set-Cookie: __Host-session=abc123;
Path=/;
Secure;
HttpOnly;
SameSite=Lax;
Max-Age=3600
// 场景 2:需要跨站使用的 Cookie(如嵌入式小部件)
Set-Cookie: widget_token=xyz789;
Domain=.widget-service.com;
Path=/;
Secure;
HttpOnly;
SameSite=None;
Partitioned;
Max-Age=86400
// 场景 3:纯前端可读的 Cookie(如主题偏好,非敏感数据)
Set-Cookie: theme=dark;
Path=/;
SameSite=Lax;
Max-Age=31536000
4.2 Express.js 完整安全配置
// cookie-security.js — 生产级 Cookie 安全中间件
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');
const app = express();
// 生产级 Session 配置
app.use(session({
secret: process.env.SESSION_SECRET, // 至少 32 字节的随机密钥
name: '__Host-session', // 使用 __Host- 前缀
cookie: {
secure: true, // 仅 HTTPS
httpOnly: true, // JS 不可读
sameSite: 'lax', // CSRF 防护
maxAge: 60 * 60 * 1000, // 1 小时过期
path: '/', // 全站可用
// 不设置 domain — 锁定到当前域名
},
resave: false, // 不强制保存未修改的 Session
saveUninitialized: false, // 不保存空 Session
rolling: true, // 每次请求刷新过期时间
}));
// 登录后重新生成 Session ID(防 Session 固定)
app.post('/api/login', async (req, res) => {
const user = await authenticateUser(req.body);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// 防止 Session 固定攻击
const sessionData = { ...req.session };
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
Object.assign(req.session, sessionData);
req.session.userId = user.id;
req.session.role = user.role;
res.json({ success: true });
});
});
// 登出时销毁 Session
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Logout error' });
res.clearCookie('__Host-session');
res.json({ success: true });
});
});
4.3 安全审计脚本
// 自动化 Cookie 安全审计脚本 audit-cookies.js
const https = require('https');
function auditCookies(url) {
return new Promise((resolve) => {
https.get(url, (res) => {
const cookies = res.headers['set-cookie'] || [];
const results = cookies.map((cookie) => ({
cookie: cookie.split(';')[0],
httpOnly: /httponly/i.test(cookie),
secure: /secure/i.test(cookie),
sameSite: /samesite=/i.test(cookie),
partitioned: /partitioned/i.test(cookie),
}));
resolve(results);
});
});
}
// 使用示例:auditCookies('https://your-site.com').then(console.log)
📊 五、总结与行动清单
Cookie 安全不是一个可以"设置后遗忘"的话题。随着浏览器策略的持续演进,开发者需要持续关注最新的安全特性。
立即行动清单:
- ✅ 所有会话 Cookie 设置
HttpOnly; Secure; SameSite=Lax - ✅ 敏感场景使用
__Host-前缀防止 Cookie 覆盖 - ✅ 登入/登出时重新生成 Session ID
- ✅ 跨站 Cookie 迁移到 CHIPS(
Partitioned属性) - ✅ 定期审计 Cookie 配置,使用自动化脚本
- ❌ 不要将敏感数据存在非 HttpOnly 的 Cookie 中
- ❌ 不要使用
SameSite=None而不加Partitioned - ❌ 不要设置过宽的
Domain属性
⚡ 关键结论: 2026 年的 Cookie 安全最佳实践是:使用
__Host-前缀 +HttpOnly+Secure+SameSite=Lax的组合作为默认配置,仅在明确需要跨站场景时才使用SameSite=None; Partitioned。记住,Cookie 安全是纵深防御的一层,必须与 CSP、HTTPS、输入验证等其他安全措施配合使用。
相关工具推荐:使用 jsjson.com 的 JSON 格式化工具 调试 Cookie 相关的 API 响应,MD5/SHA 哈希工具 验证 Cookie 签名的正确性。