rea-reverse

JS bundle 与 source map 还原

打开 source map、压缩后的 bundle,或两者一起:查看 map 里有什么,用 sourcesContent 重建原始源码目录并打包成 .zip 下载,把 bundle 中的位置或粘贴的报错堆栈映射回原始文件、行号和名称。

文件不会离开你的浏览器. map 和 bundle 在一个禁止联网的 Web Worker 中本地读取,不上传任何内容,文件里的 URL 也不会被加载。 如何自己验证

把 .map 文件和/或 bundle 拖到这里

或

一个 .map、一个 .js / .mjs / .cjs / .css bundle,或两者一起。内嵌 data: map 的 bundle 单独上传即可。合计最大 256 MiB。

这个工具做不到的事

  • 只能还原 map 里带着的内容:没有 sourcesContent 就没有原始代码,只有文件名和位置。
  • 没有 source map 的 bundle 不会被反压缩或拆包(不含 Wakaru,也不会重命名变量)。

生产环境的 JavaScript bundle 往往只有一行或几行很长的代码,变量名也都被改掉了。如果构建时同时产出了 source map(单独的 .map 文件,或者写在 bundle 末尾的内联 data: URL),这个 map 就记录了 bundle 里每个位置对应原始代码的哪个文件、哪一行哪一列。很多 map 还会把原始文件本身塞进一个叫 sourcesContent 的字段里。

这个工具在浏览器里读取 source map,给你三样东西:一份 map 内容报告;用 sourcesContent 重建出来的原始源码目录(可以浏览、预览、单独下载某个文件,或者整体下载成 .zip);以及位置查询——把 bundle 中的位置或粘贴进来的报错堆栈,换算成原始文件、行号、列号和名称。整个过程不上传任何文件。

能做什么

输入。 拖入或选择一个或多个文件:一个 .map 文件,一个 .js、.mjs、.cjs 或 .css bundle,或者两者一起。其他扩展名的文件会按内容判断(map 以 { 开头)。加入 bundle 时,页面会读取它最后一条 sourceMappingURL 注释:

  • 内联 map(//# sourceMappingURL=data:application/json;base64,...)会被解码直接使用,所以单独上传这个 bundle 就够了;
  • 外部引用(//# sourceMappingURL=app.js.map)会在报告里列出,并提示你把那个文件一起上传。页面不会替你去加载它。

如果上传了多个 map,优先使用 bundle 声明的那个(其次是 <bundle>.map,再其次是第一个),其余的会出现在警告里。上限:所有文件合计 256 MiB,300 万个映射段,200 万行生成代码,20 万个源文件,索引 map 最多 1 万个 section、嵌套 64 层,分析超时时间 2 分钟。

报告。

  • map 字段:格式(普通 map,或带 sections 的索引 map,会被展平)、file、sourceRoot、源文件数量以及其中带 sourcesContent 的数量、名称数量、映射段数和生成代码行数。
  • 忽略列表:有多少源文件在 ignoreList 或更早的 x_google_ignoreList 里(打包工具标记为第三方或生成的代码)。这些源文件在目录树和查询结果里会打上标记。
  • 打包工具线索:统计看起来像 webpack(webpack://)、Turbopack(turbopack://)、Vite(/@fs/、/@vite/)、Rollup(\0 虚拟模块、commonjsHelpers)、Rolldown、esbuild(<stdin>)或 Parcel(/__parcel_source_root/)的源文件名。这只是从文件名得到的线索,不能证明一定是哪个工具打的包。
  • node_modules 占比:有多少源文件位于 node_modules/ 目录下。
  • Bundle:大小、行数,以及它的 sourceMappingURL 写了什么。

原始源码。 每个带内容的源文件都会出现在一棵可折叠、可搜索的目录树里。路径会先规整:拼上 sourceRoot,去掉 webpack://、file:// 之类的 URL 协议,解析掉 . 和 ..,保证路径不会跑出目录树。你可以以文本方式预览、单独下载一个文件,或者点 全部下载为 .zip。map 里只有名字、没带内容的源文件会单独列出。

位置查询。 在查询框里粘贴一行或多行:

  • 位置,例如 1:2446(行号和列号都从 1 开始,与 DevTools 和堆栈里显示的一致);
  • Chrome 或 Node 的堆栈行(at explode (https://example.com/assets/app.js:1:2446)),或者 Firefox、Safari 的堆栈行(explode@https://example.com/assets/app.js:1:2446)。

每条结果给出原始的 路径:行:列、map 里记录的原始名称(如果有),以及目标位置附近几行原始代码,并标出目标行。如果上传了 bundle,还会显示该位置附近的生成代码。指向其他文件的堆栈帧会标为其他文件;如果没有任何一帧指向上传的 bundle(比如文件改过名),则所有帧都会被映射。

原理

source map 第 3 版由 ECMA-426 定义。mappings 字段是一长串 Base64 VLQ 数字:; 分隔生成代码的行,, 分隔映射段,每个段有 1、4 或 5 个数(生成列号,然后是源文件索引、原始行、原始列,以及可选的名称索引)。

在真正解码之前,页面先检查 map 的结构,再把 mappings 不解码地扫一遍。这一遍会统计映射段数和行数,拒绝 Base64 以外的字符、只有 2 或 3 个数的段,以及超过 32 位的数字,一旦超过上限就立刻停下。索引 map 逐个 section 做同样的检查:偏移必须严格递增;通过 url 引用其他 map 的 section 会被拒绝,因为页面不从网络加载任何东西。之后用 @jridgewell/trace-mapping 0.3.31 解码,并逐段检查解码结果是否落在它所属 section 的源文件和名称范围内。

查询时,取不晚于你给出位置的最大生成位置(可以跨行往前找),并返回这个位置上的所有映射段。这条规则、预检的内容和顺序都参照了 morluto 的开源项目 REA(MIT 许可)在 JavaScript 分析中追踪 source map 位置的做法。

.zip 在页面里直接生成:不压缩(stored),文件名用 UTF-8,每个文件带 CRC-32。写入之前会清理每条路径,保证解压时不会写到目标文件夹之外:没有绝对路径,没有 ..,没有空文件名,没有 Windows 不允许的字符;重名的文件(包括只差大小写的)会加上 ~2 这样的后缀。

文件如何保持私密

文件不会上传:

  • 解析、解码、查询和打包 .zip 都在你设备上的 Web Worker 里完成。文件里的 URL,包括 bundle 的 sourceMappingURL 和索引 map section 的 url,都不会被请求。
  • 源码、文件名、路径和代码片段都以纯文本显示,文件里的任何内容都不会被当作 HTML 解析或执行。
  • 统计事件只有粗粒度信息:成功或错误码、map 的来源(file、inline 或 none)、源文件数量区间(如 11-100)、大小区间和耗时区间。不会包含文件名、源文件路径、位置、堆栈或代码。完整列表见隐私页。
  • 工具页不展示广告。

如何自己验证

  1. 在选择文件之前,打开浏览器开发者工具,切到 Network(网络) 面板。分析一个 map,做一次位置查询,再下载 .zip。你只会看到页面资源和 worker 脚本 /_astro/sourcemap-worker-*.js 的请求,没有任何请求携带你的文件。
  2. 或者先打开页面,再断网(关掉 Wi-Fi,或者在 Network 面板勾选 Offline),然后分析文件,依然可以正常工作。
  3. worker 运行在内容安全策略 default-src 'none'; script-src 'self'; connect-src 'none' 之下,浏览器不允许它发出任何网络请求。

局限

  • 没有 map 就无法还原。 没有 source map 时,这个工具不会做反压缩、变量重命名或拆分模块。那需要 Wakaru(unminify / unpack)这类反混淆工具,不在本工具范围内;bundle 源码还原教程介绍了 REA 如何调用它。
  • 只有 map 带了内容才有原始代码。 关闭了 sourcesContent 的 map(例如 webpack 的 nosources-source-map)只能给出文件名和位置,没有原始代码。
  • 原始代码是打包工具看到的版本。 对 TypeScript、Vue 或 JSX,通常就是原始文件,但有些 loader 存的是已经转换过的中间版本。
  • 不会加载外部 map。 如果 bundle 指向单独的 .map 文件,请自己把它加进来。section 使用 url 的索引 map 需要先合并成一个文件。
  • section 互相重叠的 map 会被拒绝,和 REA 的做法一致,即使有些工具能接受。
  • 列号。 查询使用 UTF-16 列号,与 JavaScript 引擎和 source map 格式一致。位于一行最后一个映射之后的位置,会归到这最后一个映射上。
  • 查询上限。 每次最多映射 200 个位置或堆栈行,其余的只计数、不映射。超过 1024 个字符的行会被忽略。
  • zip 格式。 不支持 ZIP64:一个压缩包最多 65,534 个文件、4 GiB。

常见问题

网站的 source map 在哪里找?

看脚本最后一行有没有 sourceMappingURL 注释,或者响应头里有没有 SourceMap。在 DevTools 的 Sources(源代码) 面板里,有 map 时能看到映射出来的原始文件。只下载你有权分析的网站的 map。

为什么有些文件只有名字没有内容?

map 里列出了它们,但对应的 sourcesContent 是 null 或缺失。它们会出现在“没有内容的源文件”列表里;位置查询仍然能告诉你它们的文件、行和列。

堆栈映射到了错误的位置。

确认 map 和抛错的 bundle 来自同一次构建(不同部署里同名的 bundle 对不上),并且列号是浏览器给出的,而不是来自格式化过的文件副本。

为什么有些路径里带 ~2?

两个源文件在路径清理后落到了同一个位置(比如 webpack://app/./src/a.js 和 webpack:///src/a.js,或者 A.js 和 a.js)。第二个会被改名,保证在任何系统上解压时两个文件都能保留下来。

能把原始位置反查到 bundle 里吗?

这个工具不支持。它只把生成代码的位置映射到原始位置,这正是看报错堆栈和读压缩代码时需要的方向。

合规

请只分析你自己的项目、你获得授权评估的项目或开源软件的 bundle 和 map。尊重被还原代码的许可证(包括其中限制逆向工程的条款)以及适用的法律。参见免责声明。

相关教程