【漏洞挖掘】前端源码泄天机?记一次从 Nuxt Payload 挖掘 Storyblok CMS 预发布内容泄露的奇妙之旅

0x01 故事背景

在现代前后端分离的架构中,无头(Headless)CMS 越来越受欢迎。这类系统通常通过 API 给前端吐数据。正常情况下,普通用户只能看到“已发布”的内容。

但如果我告诉你,只需要在浏览器里按一下 F12,就能提前看到这家公司还未发布的招聘薪资、未公开的新闻稿、甚至公司高管变动的预发公告呢?

这不是科幻小说,而是一个真实存在的漏洞。今天,我们就来聊聊我是如何通过一个泄露的 Draft Token,撕开 investor.example.com 站点防御的缺口的。

0x02 信息收集

当拿到目标 https://investor.example.com/ 时,我习惯性地先看技术栈。Wappalyzer 告诉我这是一个基于 Nuxt.js 构建的前端项目。

作为一个在各种框架里摸爬滚打的网络安全爱好者,我的直觉雷达立刻响了:Nuxt 应用的 _payload.json 或者前端 JS Bundle 中,经常会“不小心”带出后端的环境变量。

我打开浏览器的开发者工具(F12),开始审查网络请求。果不其然,在访问公开前端页面时,我抓到了一个极为惹眼的请求:

GET https://investor.example.com/en/_payload.json?[REDACTED_ID]

在这个庞大的 JSON 响应体中,我熟练地 Ctrl+F 搜索 token、secret、key 等关键字。突然,一行配置跳进了我的视线:
“storyblokToken”: “[REDACTED_DRAFT_TOKEN_xxx]”,
“storyblokVersion”: “published”

这是一个 Storyblok CDN accessToken

此时,一般的新手可能会觉得:“这只是一个前端调用的公开 Token 罢了,有什么大惊小怪的?” 但经验告诉我,Storyblok 的 Token 分为两类:Public Token(只能看已发布内容)和 Preview/Draft Token(可以看草稿和预发布内容)。

那么,究竟开发者有没有把高权限的 Draft Token 错当成 Public Token 丢在前端?

0x03 漏洞利用

我们直接使用官方 API 进行测试

第一步:验证 Token 的基础权限
我构造了第一个请求,查询正常状态下 (version=published) 的 CMS 文章总数:

GET https://api.storyblok.com/v2/cdn/stories?token=[REDACTED_DRAFT_TOKEN_xxx]&version=published&per_page=1

API 响应头返回: Total: 523

这说明站点公开发布了 523 条内容,一切正常

第二步:逻辑转换,直击软肋
既然 Token 在手,如果我在请求中强行把版本参数改为 version=draft 会发生什么?如果这是一个低权限的 Public Token,Storyblok API 会无情地返回 403 或者是同样的 523 条数据

GET https://api.storyblok.com/v2/cdn/stories?token=[REDACTED_DRAFT_TOKEN_xxx]&version=draft&per_page=1

API 响应头返回: Total: 596


这意味着有整整 73 条 处于 draft-only(仅草稿)或 published_at = null 的未发布内容,此刻完全暴露在我的面前!

第三步:提取未发布机密
为了进一步证明危害,我挑了一个特定的 slug(例如一个数据分析师的职位招聘)进行越权读取验证:

请求 1:尝试在 Published 模式下读取(模拟普通用户)

GET https://api.storyblok.com/v2/cdn/stories/career/jobs/data-analyst-shop-analytics-all-genders-hamburg-or-berlin-germany?token=[REDACTED_DRAFT_TOKEN_xxx]&version=published

响应: [“This record could not be found”] (符合预期,前端不可见)

请求 2:使用 Draft 模式读取(模拟攻击者)

GET https://api.storyblok.com/v2/cdn/stories/career/jobs/data-analyst-shop-analytics-all-genders-hamburg-or-berlin-germany?token=[REDACTED_DRAFT_TOKEN_xxx]&version=draft

响应 (200 OK):

{
  "story": {
    "name": "Data Analyst - Shop Analytics (all genders) – Hamburg or Berlin, Germany",
    "created_at": "202X-05-11T14:44:35.274Z",
    "published_at": null,
    "content": {
      "salary": "60000 - 70000 EUR",
      "main_recruiter": "Carolin Werner",
      "internal_job_id": "6a87ee8e-84fb-4d2c-9a97-..."
    }
  }
}

看到返回体里的 salary、main_recruiter 和 internal_job_id 时,我知道,这个漏洞稳了。

0x04 危害影响

不要小看这多出来的 73 条草稿内容。对于一个跨国企业的 IR(投资者关系)站点来说,这不仅仅是“信息泄露”那么简单,它可能演变成一场灾难:

  1. 商业机密泄露与合规风险: 新闻稿、财报公告通常会提前在 CMS 中排版为 Draft 状态。攻击者若提前读取到这些信息,甚至可能引发内幕交易等严重合规问题。
  2. 内部架构与人员暴露: 未发布的职位描述中,赫然写着内部招聘负责人名字、薪资预算范围(如上文的 6-7 万欧元)以及内部系统 ID。这些是社会工程学(Social Engineering)攻击的绝佳素材。
  3. 零门槛利用: 攻击者无需登录、无需任何特殊权限,只要写个简单的 Python 爬虫,就能持续监控该公司未来所有的业务规划。

0x05 防御与反思

从攻击者的快感中抽身,回归到安全建设者的视角。产生这个漏洞的 Root Cause(根本原因)非常典型:前端工程化过程中的配置不当

很多开发者为了本地调试方便,将 Storyblok 的 Preview/Draft Token 写死在了环境变量(如 .env)中,打包时没有做环境区分,直接注入到了 Nuxt 的 Payload 和 JS Bundle 里被推上了生产线。

作为安全专业的学生,在此向各位前端大佬和架构师们献上 3 条避坑指南:

  1. 绝对隔离: 生产环境(Production)前端必须且只能使用公钥(Public Token,仅能读取 version=published)。
  2. 环境校验: Draft/Preview Token 只能存在于受认证保护的预发/测试环境中。建议在网络层(Nginx/WAF)对访问预发环境的 IP 进行严格的白名单限制。
  3. 应急响应: 对于本次受影响的站点,立刻在 Storyblok 后台**轮换(Rotate)**已泄露的 Token,并全局审计其他业务线是否使用了同款“裸奔”配置。

网安的魅力就在于此,你永远不知道下一个字符背后藏着怎样的一个世界。保持好奇心,保持对每一行代码的敬畏。咱们下个漏洞再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注