你写的那行代码,可能正在“裸奔”
说实话,我第一次认真审视 Cookie 安全的时候,是在凌晨三点。那时候产品刚上线,用户反馈登录有点慢,我顺手加了个逻辑:把用户的“最近浏览记录”或者“偏好设置”直接塞进 Cookie 里,好让下次请求快一点。
当时的我觉得挺聪明,直到安全团队在代码审查里扔过来一张图,指着那行简单的 Set-Cookie 说:“这要是被注入了,整个用户库都得丢。”
很多开发者(包括以前的我)都有一个误区:觉得 Cookie 只是存个 ID 或者简单的配置项,不敏感。但实际上,Cookie 是客户端和服务端沟通的桥梁,也是攻击者最容易下手的“后门”。今天咱们就掰开揉碎了聊聊,怎么别让用户的输入直接变成 Cookie 的内容,或者让攻击者往你的 Cookie 里塞毒苹果。
为什么“直接进 Cookie”这么危险?
咱们先从原理说起。Cookie 本质上是 HTTP 响应头里的一行 Set-Cookie,然后在后续请求中通过 Cookie 头自动携带。听起来很公平对吧?
但问题出在信任边界上。
浏览器认为 Cookie 是可信的,因为它是服务器签发的。攻击者一旦能在 Cookie 里注入恶意内容(比如 JavaScript 代码、恶意的 Session ID、或者覆盖掉你的安全标记),浏览器会照单全收,并把这些恶意内容带回给服务器,或者在用户浏览器上执行。
最常见的两种“坑”:
- XSS(跨站脚本攻击):攻击者把
<script>标签塞进 Cookie 值,结果你的网站把这个值原样输出了,或者浏览器执行了它。 - Cookie 伪造/篡改:攻击者修改 Cookie 内容,伪装成管理员或者另一个用户。
场景一:用户输入直接进 Cookie —— 典型的自杀行为
假设你有个功能,用户选择“深色模式”或“浅色模式”,你直接把用户的选择存进 Cookie:
// 危险示范!千万别这么写!
app.get('/set-theme', (req, res) => {
const theme = req.query.theme; // 用户输入:?theme=dark
res.cookie('user_theme', theme, { httpOnly: false }); // 直接设置!
res.send('主题已设置');
});
如果用户在 URL 里传的是:?theme=<script>alert('XSS')</script>
这时候,你的 Cookie 就变成了:
user_theme=<script>alert('XSS')</script>
接下来,只要你的网站任何地方把这个 Cookie 值渲染到页面上(比如用 document.cookie 或者后端模板直接输出),恶意脚本就会在用户浏览器里执行。攻击者可以窃取用户的 Session Token,从而接管账号。
正确的做法是什么?
第一步:严格白名单验证
永远不要信任用户输入。对于 Cookie 值,尤其是可能影响前端渲染的,只做白名单校验。
// 安全示范
const VALID_THEMES = ['light', 'dark', 'system'];
app.get('/set-theme', (req, res) => {
let theme = req.query.theme;
// 强制转换为小写,防止大小写绕过
theme = theme.toLowerCase();
// 白名单校验
if (!VALID_THEMES.includes(theme)) {
theme = 'system'; // 默认值
}
// 只允许纯字符串,禁止特殊字符
res.cookie('user_theme', theme, {
httpOnly: false, // 注意:前端可读的 cookie 更要小心
secure: true, // 仅 HTTPS
sameSite: 'Strict'
});
res.send('主题已设置');
});
第二步:编码与转义
如果确实需要存储用户输入(比如昵称),必须进行编码。
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
const userNickname = req.query.nickname;
const safeNickname = escapeHtml(userNickname);
res.cookie('display_name', safeNickname);
第三步:分离存储
尽量把敏感或可能包含特殊字符的数据存在服务端(数据库),Cookie 里只存一个唯一的 ID(如 user_pref_id)。这样既安全,又干净。
// 最佳实践:Cookie 只存 ID
const prefId = generateUniqueId(); // 例如:uuid v4
const preferences = { theme: userTheme, nickname: userNickname };
await db.savePreferences(prefId, preferences); // 存到数据库
res.cookie('user_pref_id', prefId, {
httpOnly: true, // 禁止 JS 访问,防 XSS 窃取
secure: true, // 仅 HTTPS
sameSite: 'Strict', // 防 CSRF
maxAge: 3600000 // 1小时过期
});
场景二:Cookie 属性配置不当 —— 给攻击者留门
即使你没让用户输入直接进 Cookie,如果 Cookie 属性配错了,照样出事。
1. httpOnly 没开
httpOnly 的作用是禁止 JavaScript 通过 document.cookie 读取 Cookie。这是防止 XSS 窃取 Session 的最后防线。
// 错误:没有 httpOnly
res.cookie('session_id', sessionId, { secure: true });
// 正确:加上 httpOnly
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
注意:httpOnly 只防 XSS 窃取,不防服务端篡改。所以敏感数据别放 Cookie 里!
2. secure 没开
secure 确保 Cookie 只通过 HTTPS 传输,防止中间人攻击(MITM)窃听。
// 错误:HTTP 下也能收到 Cookie
res.cookie('token', token, { httpOnly: true });
// 正确:强制 HTTPS
res.cookie('token', token, { httpOnly: true, secure: true });
3. sameSite 没设或设错
sameSite 防止跨站请求伪造(CSRF)。有三个值:
Strict:最严格,跨站请求完全不携带 Cookie。Lax:默认值,导航到目标网址时携带(如点击链接)。None:跨站也携带,必须配合secure: true使用,否则浏览器会拒绝设置。
// 推荐:Strict,除非你有明确的跨站需求
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
场景三:Cookie 大小与污染
Cookie 有大小限制(通常 4KB)。如果你把大量数据(如用户画像、完整地址)塞进 Cookie,不仅会导致请求变慢(每次请求都携带大量冗余数据),还可能触发安全策略被截断,造成数据不一致。
例子:
// 错误:存了太多数据
res.cookie('user_profile', JSON.stringify({
name: '张三',
email: 'zhangsan@example.com',
address: '北京市朝阳区xxx路xxx号',
phone: '138xxxxxxxx',
... // 更多字段
}), { maxAge: 86400000 });
后果:
- 每个 API 请求都携带几 KB 的冗余数据。
- 如果用户修改了部分信息,Cookie 里还是旧数据,导致状态不一致。
- 某些浏览器可能拒绝设置过大的 Cookie。
正确做法:
- Cookie 只存非敏感、小体积的标识符(如
user_id、session_id、theme_pref)。 - 详细数据存服务端,按需查询。
场景四:客户端 JS 操作 Cookie —— 高风险区
有时候,前端 JS 会主动读写 Cookie,比如用于 A/B 测试、埋点等。这时候风险最高,因为 JS 直接暴露在 XSS 攻击下。
危险示例:
// 前端危险代码
document.cookie = "search_query=" + userInput; // 用户输入直接进 Cookie
如果 userInput 包含 <script> 标签,而后续页面又通过 JS 读取这个 Cookie 并插入 DOM,XSS 就发生了。
安全建议:
- 前端尽量不存敏感数据。
- 如果必须存,进行encodeURIComponent编码。
- 后端读取时,再次解码并校验。
// 前端:编码
const safeQuery = encodeURIComponent(userInput);
document.cookie = `search_query=${safeQuery}; path=/`;
// 后端:解码并验证
const query = decodeURIComponent(req.cookies.search_query);
// 再次校验是否为纯文本,无特殊字符
if (/[^a-zA-Z0-9\u4e00-\u9fa5\s]/.test(query)) {
// 拒绝或重置
res.clearCookie('search_query');
}
实际案例:某电商平台 Cookie 泄露事件
两年前,某大型电商平台曝出用户信息泄露。原因很简单:
商家可以在商品描述里插入 JS 代码。平台把商品描述里的某个字段存进了 Cookie(用于显示“最近浏览”),但没有过滤 <script> 标签。
攻击者构造一个包含恶意 JS 的商品描述,当用户访问该商品页面时,恶意代码被写入 Cookie。之后,用户浏览其他页面,前端 JS 读取 Cookie 并执行,攻击者便窃取了用户的登录 Token。
根因分析:
- 用户输入未过滤:商品描述直接进 Cookie。
- Cookie 未设 httpOnly:JS 能读到 Token。
- 未分离存储:敏感信息直接存在客户端。
修复方案:
- 所有用户输入严格过滤,禁止 HTML/JS 标签。
- Session Token 存 httpOnly Cookie。
- 商品描述存数据库,Cookie 只存商品 ID。
防坑检查清单
每次设置 Cookie 前,问自己这几个问题:
- 这个数据必要吗? 能不能存在服务端?
- 用户输入过吗? 如果有,是否做了白名单校验或转义?
- Cookie 属性对吗?
httpOnly、secure、sameSite都设了吗? - 大小合适吗? 是否超过 4KB?是否包含敏感信息?
- 前端会读到吗? 如果前端需要,是否用了
encodeURIComponent?
总结
Cookie 是双刃剑。用好了,它方便用户;用错了,它就是攻击者的武器。
记住三条铁律:
- 不要让用户输入直接进 Cookie,尤其是可能渲染到页面的内容。
- 敏感数据别放 Cookie,放服务端,Cookie 只存 ID。
- 正确配置 Cookie 属性,
httpOnly、secure、sameSite一个都不能少。
安全无小事,一次疏忽可能代价惨重。希望这篇指南能帮你避开 Cookie 注入的坑,让你的应用更安全、更可靠。
如果有具体的代码场景需要审查,欢迎随时拿来讨论,咱们一起把漏洞堵上。
