2026/10/18 8:10:57

jsPDF服务端路径遍历与本地文件包含漏洞深度解析

jsPDF服务端路径遍历与本地文件包含漏洞深度解析 如果你负责维护的服务端 PDF 生成服务正好依赖 jsPDF那这篇文章建议你耐着性子看完。前阵子我们针对 jsPDF 的本地文件包含 / 路径遍历类风险做了一次彻底复盘内部用 CVE-2025-68428 作为这次漏洞研究的跟踪编号。全程走下来我发现这类问题远比想象中隐蔽——它不发生在 PDF 语法层面而是藏在“到底谁有权限决定该把哪个文件塞进 PDF”这一步。这篇文章我会从原理、复现、影响评估到修复方案一条线讲清楚重点说实现场排查思路和加固经验不提供恶意利用手法纯粹站在防御和研究视角适合负责安全审计、服务端研发以及做依赖选型评审的工程师阅读。1. 漏洞背景与研究起因1.1 为什么盯上 jsPDF 这个库jsPDF 在 JavaScript 生态里算是 PDF 生成的老牌选择浏览器端调用new jsPDF()就能快速创建文档社区活跃度、导出质量、API 完善度都不错。很多团队不止把它用在前端还会通过 Node.js 封装一层 HTTP 接口把“富文本内容转 PDF”“报表导出”“电子合同生成”这类能力变成后端服务。服务端化之后jsPDF 的输入就不再只是“用户页面上已有的数据”而是夹杂着文件名、路径、图片地址、外部资源引用等不可信数据。我以前也犯过一个错误认知jsPDF 是个纯前端库不直接操作文件系统能有什么安全问题实际上风险从来不在库本身而在我们基于它搭建的调用链。一旦出现“用户可控参数 → 文件读取 → PDF 内容输出”这样的链路本地文件包含和路径遍历就会冒头。CVE-2025-68428 这个案子本质上是某项目在服务端用 jsPDF 生成报告时图片参数处理不当导致任意文件内容可以被嵌入到 PDF 中。前端库也好后端库也罢只要它落在了一个不安全的设计里漏洞就是水到渠成的事。1.2 本地文件包含与路径遍历先把概念对齐路径遍历Path Traversal比较好理解应用程序把用户输入直接拼进文件路径攻击者通过../甚至绝对路径跳出预期目录读取或操作非授权文件。本地文件包含Local File Inclusion在 PHP 语境里更常见指应用动态加载本地文件并将内容回显到响应中。放在 PDF 生成场景里两者会合流服务端拿到受害者传来的“图片路径”用文件读取函数加载再把二进制内容编码后交给 jsPDF 的addImage拼进 PDF。这一步如果能被../控制PDF 就成了一个“文件内容中转站”。打个比方这就像酒店前台让你自己报一个“备品仓库房间号”它不核对门牌前缀就派人去取你只要把房间号改成隔壁楼层甚至改成总控室它也会老老实实把东西端到你面前。文件读取函数没有做前缀校验和规范化攻击路径等同于“串门”这就是整个漏洞链条的核心。jsPDF 本身不负责判定路径是否合法真正的判断逻辑得由使用者来写。问题恰恰在于大量业务代码只会校验“文件后缀是不是.png”或者“路径是不是以某个目录开头”但这些校验在路径穿越面前几乎形同虚设。2. 漏洞原理与触发链路解析2.1 jsPDF 资源加载机制中的盲点jsPDF 提供addImage(imageData, format, x, y, width, height)方法第一个参数支持base64字符串、HTMLImageElement、HTMLCanvasElement以及像素数组。浏览器端我们通常会先读文件转成DataURL再把DataURL传给addImage。服务端场景则常见两种写法一是直接传一个远程 URL让jsPDF内部去拉取图片二是先把文件读成Buffer转成base64后交给addImage。第二种写法就是风险集中地。很多实现长这样const fs require(fs); const path require(path); const { jsPDF } require(jspdf); app.post(/api/export-pdf, (req, res) { const imagePath req.body.imagePath; // 用户可控 const imageBuffer fs.readFileSync(imagePath); const imgBase64 imageBuffer.toString(base64); const doc new jsPDF(); doc.addImage(imgBase64, PNG, 10, 10, 100, 100); const pdfBuffer Buffer.from(doc.output(arraybuffer)); res.send(pdfBuffer); });这段代码演示的调用链非常典型。imagePath完全没有被校验fs.readFileSync(imagePath)会严格忠于传入路径。如果用户传入的是../../etc/hostname这种相对路径读出来的文件内容会被当作 PNG 编码进 PDF。PDF 格式本身对嵌入数据有容错即便数据不是合法图片addImage在不少版本里也不会直接报错而是把数据如实写入 PDF 对象流。这暴露了 jsPDF 的一个特性盲点它只负责“把给定的数据变成 PDF 内部对象”不负责验证源头的合法性。资源加载机制本身是可靠的盲点在于调用它之前的文件定位逻辑完全失控。2.2 CVE-2025-68428 的根因定位我把这次研究的根因拆成三个层面。第一层是输入未经过任何白名单约束imagePath原样进入文件系统 API第二层是文件读取与文件类型校验脱节先读文件再判断扩展名而不是先判断路径再读取第三层是错误处理不当导致输出通道稳定即便读取非图片文件PDF 依然能正常生成攻击者可以反复探测服务器文件布局而不会被察觉。路径规整问题也值得单独说。很多团队会认为加了path.join就安全了这其实是误解。path.join(/var/app/uploads, ../../etc/passwd)的结果是/var/app/etc/passwd依然越界。还有不少实现用了path.normalize再判断startsWith但没有注意到startsWith(/var/app/uploads)对/var/app/uploads_evil/file也会返回true——这类问题我在实际代码评审里遇到过太多次。CVE-2025-68428 的具体根因就是“路径前缀字符串比对”加“用户可控的相对路径拼接”。修复前只要上传目录中存在攻击者可控的符号链接或者目标服务器存在其他可通过相对路径到达的文件漏洞就能稳定触发。这不是 jsPDF 一个库的错而是“API 使用模式”层面埋下的雷。2.3 最容易踩中的三类使用场景我归纳了三类高风险用法只要命中其中一条就该对照本文后续的修复方案做加固。第一类是直接透传用户输入作为文件路径的服务端接口典型的就是上文那个imagePath参数。这类接口在内部系统中尤其多因为“反正内网、外部访问不到”的侥幸心理参数校验往往最薄弱。第二类是模板文件或配置文件中引用外部资源。PDF 模板引擎允许在模板里写img src${userData.logo} /然后由服务端渲染层去读取logo对应的文件。如果logo字段来源是用户上传或外部系统路径穿越就会通过模板注入的方式卷土重来。第三类是批量导出时拼接文件名。导出用户指定 ID 的文件ID 是数字但代码写着const file path.join(BASE, id - userInput .png)其中userInput若来自查询参数攻击者插入../就能修改最终路径的目录层级实现跨目录读取。这三类场景的共同特征是PDF 内容生成的成功与否直接取决于外部传入路径能否被文件系统正确处理。而 jsPDF 对此没有任何干预能力拦路职责完全在业务代码。3. 环境复现与验证过程3.1 搭建最小复现环境复现不用追求“打得穿生产系统”我把实验控制在完全隔离的容器环境里模拟目标服务的调用链。环境由三部分组成一个接收图片路径参数的服务端接口、一个受限的资源目录、以及一个用于确认“文件内容确实进了 PDF”的解析脚本。实验目录结构如下/opt/pdf-lab/ ├── uploads/ # 业务允许读取的资源目录 │ └── demo.png # 合法图片 ├── secret/ # 模拟敏感数据目录 │ └── internal.txt ├── server.js # 模拟 PDF 生成服务 └── inspect.js # PDF 内容提取脚本server.js的核心就是前文那段fs.readFileSync(imagePath)的代码只是我在外面套了一层express路由并限制监听在本地回环地址上。整个环境不连接外部网络所有验证动作都在容器内闭环完成。这个环境的价值在于它把真实的业务复杂度剥离掉只留下漏洞链路的最小骨架。排查任何同类问题都可以先抽象出这样的最小场景比自己一头扎进生产日志效率高得多。3.2 从请求到 PDF 输出的完整验证验证思路是先确认合法路径能正常生成 PDF再验证越界路径是否也能生成 PDF最后从 PDF 中提取数据判断内容来源。第一步我构造了一个指向uploads/demo.png的请求接口返回200PDF 能正常打开说明基线是通的。第二步我把参数改成../secret/internal.txt构造了模拟攻击请求。服务端代码里fs.readFileSync(../secret/internal.txt)会依据服务进程的工作目录解析。为了让问题更可控我在启动server.js时把工作目录固定在/opt/pdf-lab/因此该相对路径实际指向/opt/secret/internal.txt。这里要注意工作目录不同越界读取的落点就不同这也是复现时最容易被忽略的变量。第三步我用inspect.js解析生成的 PDF。PDF 内部对象结构里嵌入的图片数据通常以流对象存放我用pdfjs这类解析库把流内容提取成文本。结果internal.txt的内容完整出现在 PDF 的页面数据里。整个验证过程最关键的观察点是jsPDF 没有对非图片数据报错。这让我意识到这类漏洞不只是“读取文件”它把任意文本文件内容都变成了可回显的 PDF 内容这成了一种隐蔽的带外通道——传统 HTTP 响应里如果直接输出/etc/passwdWAF 很容易识别但塞进 PDF 后检测难度明显上升。3.3 危害层级的判断标准验证完成后我按三个维度评估危害层级。第一维度是可读取文件的范围取决于服务进程的运行身份。如果服务以高权限用户运行影响面会显著扩大如果以受限容器用户运行则只有该用户可读的文件受影响。第二维度是读取结果能否被稳定提取。我们验证了文本文件内容能完整进入 PDF但二进制文件、超大文件不一定能稳定提取。如果文件读取后直接交给addImage部分解析版本遇到超大或异常二进制数据可能抛异常这种情况下漏洞就变成“服务端拒绝服务”而非“信息泄露”危害类型会转化。第三维度是是否有二次写入或联动利用。如果 PDF 生成接口还能控制输出文件保存位置攻击者就存在把越界读取的文件写入自定义路径的可能这时漏洞就从信息泄露升级为文件写入危害等级完全不一样了。任何复现工作都不能只停留在“读到了/etc/passwd”这一步一定要把读取范围、输出稳定性、后续联动三个问题全部回答清楚风险评估才有依据。4. 影响范围与风险评估4.1 攻击前置条件的全景梳理这类漏洞不会无差别命中所有 jsPDF 项目它需要一组前置条件同时成立。我把条件列全方便对照自检。前置条件说明常见满足场景服务端使用 jsPDF纯浏览器端使用无法直接读取服务器文件Node.js 封装导出服务用户可控路径直通文件读取函数缺少白名单校验或路径规范图片上传、模板资源加载、文件导出接口读取产物会被编码进 PDF使用addImage/html等方式嵌入二进制内容报告生成、证书导出、邮件附件服务进程具备跨目录读取权限容器或主机账号权限未收敛以 root 运行、共享账号、挂载盘权限过大四条缺一不可。反过来看只要断掉其中一环漏洞就无法形成闭环。这也是我最推荐做“纵深防御”的原因——与其赌单点校验做得完美不如让攻击者即使突破一环也走不通整条链路。4.2 受影响的应用形态与现实场景受影响最大的应用形态是“上传 → 生成 PDF → 下载”模式。这种模式里用户上传文件后系统需要读取文件内容并渲染为 PDF。如果上传目录与被读取目录分离且文件名由服务器生成风险相对较低如果文件名由用户拼接、路径由用户指定风险就会显著上升。第二类受影响形态是电子合同和票据系统。这类系统里存在大量模板文件例如印章图片、签名图片、企业 Logo。模板配置常允许非开发人员修改图片路径一旦运维同事把路径写成变量且变量来源于登录用户输入路径穿越就有机会借道模板进入 PDF。第三类受影响形态是数据分析报表服务平台。平台提供“导出带图表的 PDF 报告”能力图表图片先落盘再读取。报表服务通常运行在数据处理集群中可读文件范围往往包括数据管道配置、连接串参数等敏感信息一旦被 PDF 带出泄露的就是基础设施层面的密钥材料。在这些场景里jsPDF 的版本新旧反而不是核心问题——只要调用链存在新版本 jsPDF 一样会被带进坑里。所以评估影响范围时重点不是“要不要升级 jsPDF”而是“我们的调用链上有没有路径直通”。4.3 对企业业务的潜在危害分解危害至少体现在三个层面。短期看是敏感信息泄露服务器上的配置、源码、密钥、业务数据都能被转成 PDF 外带这个危害最直接也是漏洞定级的主要依据。中期看是攻击面扩大。一旦拿到了服务器上的配置文件和连接信息攻击者就有条件进入内网横向阶段。PDF 导出接口通常部署在业务前置机上与核心数据库的网络可达性往往没有被严格隔离这就给“PDF 漏洞 → 内网突破”提供了跳板。长期看是信任体系受损。这类漏洞暴露出的是团队在“外部输入不可信”这条安全基线上的执行问题修复一个点容易但类似问题如果遍布其他服务安全团队就很难逐个排查。我复盘时最大的感触是单点漏洞的价值不只是修复它本身更重要的是借此推动一次团队层面的安全设计评审把“用户可控路径”这类反模式从所有代码库里清理干净。5. 修复方案与防御实践5.1 版本升级不是万能药先说结论如果你看到 jsPDF 新版本修复了某个路径处理缺陷当然建议升级但别把升级当作终点。很多同类漏洞的本质不在 jsPDF 内部而是业务代码把不受信任的路径传进了文件读取函数。这种情况下哪怕升级到最新版你的代码依然会把../../etc/passwd交给fs.readFileSync。真正的修复顺序应该是先清理调用链再谈版本。调用链上只要用户输入没有经过严格白名单就不该有任何文件系统 API 能接触它。5.2 代码层面的三层校验硬规范我把自己在实际修复中落地的三层校验逻辑分享出来。第一层是白名单前缀校验但不是简单的startsWith字符串比较而是规范化后再比较并且强制加上分隔符const path require(path); const fs require(fs); function safeResolve(baseDir, userPath) { // 先解析成绝对路径再统一用路径前缀判断 const resolvedBase path.resolve(baseDir); const resolvedPath path.resolve(baseDir, userPath); const relative path.relative(resolvedBase, resolvedPath); // 相对路径以 .. 开头或者本身就是 ..说明越界 if (relative .. || relative.startsWith(..${path.sep})) { throw new Error(路径越界); } return resolvedPath; }第二层是后缀与内容双重校验。文件读取之前先检查扩展名是否在允许列表内读取之后用file-type这类库检查文件魔数确认它确实是图片。这样即使第一层校验被绕过非预期文件也会在进入 PDF 之前被拒绝。第三层是最小权限运行。服务进程使用独立低权限用户运行容器内不挂载与业务无关的数据盘。即使攻击者成功越界读取也会受限于账号权限读不到系统关键文件。这里有个很实用的细节很多团队在校验文件名时图省事直接判断userPath.indexOf(..) -1看似简单粗暴有效但遇到绝对路径就失灵。建议一律用path.resolve做规范化之后再判断而不是对原始字符串做直觉式检查。5.3 纵深防御从入口到出站的多层兜底代码修复之后我还在整个链路里补了四道纵深防线确保未来即使有人改代码引入了类似问题也有其他机制兜底。第一道防线是统一文件访问网关。所有 PDF 生成服务不再直接调用fs.readFileSync而是通过一个内部文件访问服务获取资源。这个服务只暴露按ID取文件的接口不接受路径参数从源头上掐断路径直通。第二道防线是出站内容检测。在 PDF 生成接口的响应出口加一层内容检测如果 PDF 内嵌入的数据熵值过高、含有明显的配置文件特征例如出现密钥字段名称则拦截响应并告警。这套逻辑不追求完美识别只求针对已知泄露模式提高攻击成本。第三道防线是运行时行为监控。针对服务进程监控异常文件读取行为例如在短时间内读取了大量非图片扩展名文件、读取了不在挂载目录白名单内的文件。这些行为特征一旦命中就直接切断进程或触发熔断。第四道防线是WAF 和网关的懒校验兜底。在 Web 网关层对请求参数做启发式检测凡是路径参数中带有../、.../、%2e%2e等典型穿越特征一律拦截。注意这个方案只用于兜底不能作为主要防御因为编码绕过的变种太多主防御必须放在应用层。四道防线的投入成本逐级递增实际落地不必一步到位。我的建议是先做代码修复和应用层校验再逐步推进文件访问网关化和行为监控这些措施单独拎出来都不复杂但叠加在一起就能把同类漏洞的可利用性压到很低。6. 常见问题与排查实录6.1 复现阶段最容易踩的四个坑复现过程中我踩过不少坑挑四个影响最大的记录下来。第一个坑是工作目录漂移。fs.readFileSync(../secret.txt)的解析依赖进程启动时的cwd不同启动方式下得到的绝对路径完全不同。排查时如果不确认服务进程的工作目录会得出“漏洞不存在”的错误结论。解决方案是复现时显式指定cwd并在日志中打印绝对路径用于对账。第二个坑是相对路径拼接被提前规整。有的版本path.join(base, ../../x)会自动把结果归一化到当前盘的根目录看起来像是“没有越界”实际是已经越过。要区分path.join与path.resolve的行为复现时优先用path.resolve看真实解析结果。第三个坑是PDF 解析器对图片流的处理不一致。某些 PDF 解析库会把嵌错格式的图片数据丢弃导致明明服务端读取了文件却无法从 PDF 里提取出内容。建议同时用两种解析方式确认一是直接解析 PDF 对象流二是用文本提取工具全量扫描避免误判。第四个坑是符号链接绕过。攻击者如果能在上传目录里放置符号链接指向敏感文件那么即使路径前缀校验全做对了也会被链接带偏。这类问题在文件上传一键校验的场景里尤其隐蔽需要在安全策略中收紧“上传目录不允许创建符号链接”等文件系统权限。6.2 一次真实排查案例的完整记录我们内部某业务线也出现过类似告警排查过程很有参考价值。业务场景是用户上传签名图片服务端导出 PDF 合同。安全巡检告警显示某个请求的图片参数里出现了多次../编码变体。排查第一步是定位参数入口。我们找到GET /contract/export?signxxx.png接口sign参数直接拼接在读取路径上。当时代码里有一段注释写着“图片参数已校验仅支持本地上传文件”但校验实现是判断参数是否以/uploads/开头。第二步是验证编码绕过。安全巡检拦截的是明文../但攻击者改用了%2e%2e%2f三段编码应用层解码后变为../。由于校验发生在解码之前字符串比对看到的是%2e%2e...而以/uploads/开头的校验自然被跳过。这个案例充分说明校验必须发生在解码、规范化之后否则等于白写。第三步是修复验证。我们直接用修复后的safeResolve函数跑了一批历史攻击样本包括普通穿越、编码穿越、绝对路径注入、符号链接引用结果全部返回“路径越界”错误。同时用合法上传文件回归测试确认功能未受影响。这类排查的记录价值在于它把抽象的风险变成了可追溯的流程。安全团队面对告警时如果能按“入口参数确认 → 解码链路梳理 → 规范化校验 → 攻击样本回归”的顺序推进排查效率会高很多。6.3 安全复盘清单与个人心得复盘结束后我整理了一份自查清单供团队在设计和评审阶段直接使用。清单重点不是“jsPDF 有哪些选项”而是“这份 PDF 导出代码面对外部输入时是否守住了边界”。是否允许用户直接传文件路径、图片地址、模板路径路径读取前是否完成了解码和规范化路径校验是否基于解析后的绝对路径而不是原始字符串文件读取后是否进行了文件类型魔数校验服务进程是否以最小权限运行可读文件范围是否明确上传目录是否禁止符号链接和可执行文件PDF 接口响应是否需要出站内容检测是否建立了针对异常文件读取行为的安全监控清单最后一条我特别想强调不要只问“这个库有没有漏洞”要问“我们的代码把用户输入放在什么位置”。jsPDF 的 CVE-2025-68428 这类编号只是表象表象之下的问题是“路径参数直通文件系统”这个反模式。把反模式从所有业务代码里清除干净比盯着某一个 CVE 打补丁更有长期价值。在做完这轮研究后我个人对“前端库安全边界”的认知又刷新了一次。过去总觉得浏览器里跑的代码碰不到服务器文件天然安全实际上一旦同一套库被搬到 Node 端边界就完全变了。每次引入开源组件时除了看功能、看性能真的应该多问一句这个组件将来会不会被放在服务端处理不可信输入如果会那安全设计就必须前置到接入的那一天而不是等出了漏洞再补课。