(紧急修复!)老板急call:pdf阅读器不能用了?

1,868 阅读5分钟

客户那边说,发现了我们项目中有一个高危漏洞,需要修复下。我过去一看,好像不是我们的代码,是三方依赖的pdf.js 突然爆了高危,心想,这怎么可能,这东西都发布好多年了,用的好好的也没说高危啊。老板说,这他不管,客户那边的高危项一定要给解决。好吧,那我先去看看这个高危是个什么。

一番检索,发现真是 pdf.js 的高危漏洞,而且是今年24年4月26日内部报的,编号是 CVE-2024-4367,并且在今年24年4月30日的 4.2.67 版本上已经修复并发布了。

背景

不管如何,先看这个高危项,它允许攻击者在打开恶意 PDF 文件时立即执行任意 JavaScript 代码。主要是利用了pdf.js 中的字体渲染技术上的特性,当识别到当前浏览器支持 new Function("")并且在加载 pdf 资源时配置了 isEvalSupported 为 true(该值默认为true),此时如果我们在 pdf 资源中输入一些内容,用来控制字体渲染的参数,那么就可以在加载pdf 资源时,执行自己想要的任意的 JavaScript 代码,实现对应用系统的攻击。

解决方案

常规方案有三种

  • 完全杜绝的话可以直接将依赖的 pdf.js-dist 版本升级到 4.2.67+
  • 在使用 pdf.js-dist 的上层代码中将加载 pdf 的参数 isEvalSupported 设置为false
  • 禁用使用 eval 和 Function 构造函数

一般如果对兼容性要求不高的话就可以选择第一种,4.2.67 版本的兼容性legacy版本最低能兼容到以下版本的浏览器

Browser/environmentSupported
Firefox ESR+Yes
Chrome 98+Yes
OperaYes
EdgeYes
Safari 16.4+Mostly
Node.js 18+Mostly

但是如果像一些运行比较久远的至少要兼容到5年以上的设备的话,比如说我司产品,要兼容到 ios10.3(也不知道现在除了我司测试,到底还有谁在用 ios10.3),这种情况下,方案1就完全不可行了,那么就可以考虑使用方案2。

方案3与方案2有相似之处,通过重写 eval 和 Function 来控制内部的 isEvalSupported 的值,也可以避免 pdf文件在被渲染时使用 Function 加载 pdf 内容。

// 重写eval和Function
window.eval = function() {
    throw new Error("eval() is disabled");
};

window.Function = function() {
    throw new Error("Function constructor is disabled");
};
// pdf.js 中的Function 检测
function isEvalSupported() {
  try {
    new Function("");
    return true;
  } catch (e) {
    return false;
  }
}

const IsEvalSupportedCached = {
  get value() {
    return shadow(this, "value", isEvalSupported());
  }
};

上述的重写会影响全局的 eval 和 Function,若项目中不使用上述功能,可以考虑。若一些内部使用模块使用了以上两个功能,则不建议如此修改。

但是,我们的客户不认,只认依赖版本,我们的 pdf.js-dist 版本低于 4.2.67,这件事在他们的安全报告中,属于完全不能容忍的高危漏洞,一定要解决的,解释也没用,那现在咋办?总不能不用吧?也不能抛弃大部分的低版本客户吧?

那么这个时候,上述三种方案都不能解决问题了,就要考虑其他的方式了。

那么回归我们程序员的本质,只能 fork-modify-push-publish 了。因为只有内部产品使用,也不需要 publish 了,将本来作为第三方的依赖,转成项目内置模块来使用,这个时候想怎么改就能怎么改了。

模块内置后,客户找的安全检测机构也不知道还能不能检测出来,以防万一,还是得把 pdf.js 关于这条安全漏洞的修复给同步到我们的低版本上来。

修复内容

根据官方发布,这条漏洞主要在 pr[18015] 中修复了,那我们把这条 pr 中有关上述漏洞的部分迁移过来即可。不用把这条内容都迁,比如其中对于cmds的重写部分,我们只需要将和isEvalSupported 相关的部分迁移即可,毕竟此漏洞也是由 isEvalSupported 引起的。

主要修复内容:

  • 去除 font_loader.FontFaceObject 中的入参 isEvalSupported 及相关使用该参数的内容
  • 如果使用的版本中isEvalSupported 只用来做字体渲染,可以去除整个 pdf.js 中使用了 isEvalSupported 逻辑的相关内容

通过上述修复方式,客户那边应该也能安心了吧?检索不到低于4.2.67版本的 pdf.js 引用,也不会在解析渲染pdf 资源时,出现外部的 pdf文件对系统造成攻击

关于漏洞

总所周知,pdf.js 里不仅对pdf 文件进行了资源解析,也做了资源的渲染,其中就包含了很多字体字形的绘制,而该漏洞就来源于字体绘制时使用了 new Function(""),导致可以在 pdf 文件中写一些能够被解析的内容,在应用系统中使用 pdf.js 去解析 pdf 文件并在绘制时执行任意的 JavaScript 内容,造成对系统的攻击。

// pdf.js font_loader 字体绘制中能够执行任意js内容的部分
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
  const jsBuf = [];
  for (const current of cmds) {
    const args = current.args !== undefined ? current.args.join(",") : "";
    jsBuf.push("c.", current.cmd, "(", args, ");\n");
  }
  // eslint-disable-next-line no-new-func
  console.log(jsBuf.join(""));
  return (this.compiledGlyphs[character] = new Function(
    "c",
    "size",
    jsBuf.join("")
  ));
}

具体的绘制可以在在 PDF.js 中执行任意 JavaScript 中查看,以下为在 pdf 文件中输入任意代码的示例,

通过首先关闭c.transform(...)函数,并利用结尾括号来触发 alert:

/FontMatrix [1 2 3 4 5 (0); alert('foobar')]

将上述内容输入到 pdf 文件中,然后在火狐浏览器(未更新最新版本的 pdf 预览插件版本)中加载该 pdf 文件时的效果如下:

也可以使用 旧版本的 pdf.js 开源的 viewer 打开该文件,有一样的效果。

附录:

可用于攻击的 pdf 文件地址:codeanlabs.com/wp-content/…

CVE-2024-4367 漏洞详细攻击介绍:codeanlabs.com/blog/resear…

CVE-2024-4367 漏洞详情及相关修改 pr:github.com/mozilla/pdf…

pdf.js 相关文档推荐

前端接入 pdfjs-dist 渲染 pdf 文件踩坑

PDF.js 与 WebComponent:打造轻量级 PDF 预览器