
如果你負責維護的服務端 PDF 生成服務正好依賴 jsPDF那這篇文章建議你耐著性子看完。前陣子我們針對 jsPDF 的本地文件包含 / 路徑遍歷類風險做了一次徹底復盤內部用 CVE-2025-68428 作為這次漏洞研究的跟蹤編號。全程走下來我發(fā)現(xiàn)這類問題遠比想象中隱蔽——它不發(fā)生在 PDF 語法層面而是藏在“到底誰有權限決定該把哪個文件塞進 PDF”這一步。這篇文章我會從原理、復現(xiàn)、影響評估到修復方案一條線講清楚重點說實現(xiàn)場排查思路和加固經(jīng)驗不提供惡意利用手法純粹站在防御和研究視角適合負責安全審計、服務端研發(fā)以及做依賴選型評審的工程師閱讀。1. 漏洞背景與研究起因1.1 為什么盯上 jsPDF 這個庫jsPDF 在 JavaScript 生態(tài)里算是 PDF 生成的老牌選擇瀏覽器端調用new jsPDF()就能快速創(chuàng)建文檔社區(qū)活躍度、導出質量、API 完善度都不錯。很多團隊不止把它用在前端還會通過 Node.js 封裝一層 HTTP 接口把“富文本內容轉 PDF”“報表導出”“電子合同生成”這類能力變成后端服務。服務端化之后jsPDF 的輸入就不再只是“用戶頁面上已有的數(shù)據(jù)”而是夾雜著文件名、路徑、圖片地址、外部資源引用等不可信數(shù)據(jù)。我以前也犯過一個錯誤認知jsPDF 是個純前端庫不直接操作文件系統(tǒng)能有什么安全問題實際上風險從來不在庫本身而在我們基于它搭建的調用鏈。一旦出現(xiàn)“用戶可控參數(shù) → 文件讀取 → PDF 內容輸出”這樣的鏈路本地文件包含和路徑遍歷就會冒頭。CVE-2025-68428 這個案子本質上是某項目在服務端用 jsPDF 生成報告時圖片參數(shù)處理不當導致任意文件內容可以被嵌入到 PDF 中。前端庫也好后端庫也罷只要它落在了一個不安全的設計里漏洞就是水到渠成的事。1.2 本地文件包含與路徑遍歷先把概念對齊路徑遍歷Path Traversal比較好理解應用程序把用戶輸入直接拼進文件路徑攻擊者通過../甚至絕對路徑跳出預期目錄讀取或操作非授權文件。本地文件包含Local File Inclusion在 PHP 語境里更常見指應用動態(tài)加載本地文件并將內容回顯到響應中。放在 PDF 生成場景里兩者會合流服務端拿到受害者傳來的“圖片路徑”用文件讀取函數(shù)加載再把二進制內容編碼后交給 jsPDF 的addImage拼進 PDF。這一步如果能被../控制PDF 就成了一個“文件內容中轉站”。打個比方這就像酒店前臺讓你自己報一個“備品倉庫房間號”它不核對門牌前綴就派人去取你只要把房間號改成隔壁樓層甚至改成總控室它也會老老實實把東西端到你面前。文件讀取函數(shù)沒有做前綴校驗和規(guī)范化攻擊路徑等同于“串門”這就是整個漏洞鏈條的核心。jsPDF 本身不負責判定路徑是否合法真正的判斷邏輯得由使用者來寫。問題恰恰在于大量業(yè)務代碼只會校驗“文件后綴是不是.png”或者“路徑是不是以某個目錄開頭”但這些校驗在路徑穿越面前幾乎形同虛設。2. 漏洞原理與觸發(fā)鏈路解析2.1 jsPDF 資源加載機制中的盲點jsPDF 提供addImage(imageData, format, x, y, width, height)方法第一個參數(shù)支持base64字符串、HTMLImageElement、HTMLCanvasElement以及像素數(shù)組。瀏覽器端我們通常會先讀文件轉成DataURL再把DataURL傳給addImage。服務端場景則常見兩種寫法一是直接傳一個遠程 URL讓jsPDF內部去拉取圖片二是先把文件讀成Buffer轉成base64后交給addImage。第二種寫法就是風險集中地。很多實現(xiàn)長這樣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 格式本身對嵌入數(shù)據(jù)有容錯即便數(shù)據(jù)不是合法圖片addImage在不少版本里也不會直接報錯而是把數(shù)據(jù)如實寫入 PDF 對象流。這暴露了 jsPDF 的一個特性盲點它只負責“把給定的數(shù)據(jù)變成 PDF 內部對象”不負責驗證源頭的合法性。資源加載機制本身是可靠的盲點在于調用它之前的文件定位邏輯完全失控。2.2 CVE-2025-68428 的根因定位我把這次研究的根因拆成三個層面。第一層是輸入未經(jīng)過任何白名單約束imagePath原樣進入文件系統(tǒng) API第二層是文件讀取與文件類型校驗脫節(jié)先讀文件再判斷擴展名而不是先判斷路徑再讀取第三層是錯誤處理不當導致輸出通道穩(wěn)定即便讀取非圖片文件PDF 依然能正常生成攻擊者可以反復探測服務器文件布局而不會被察覺。路徑規(guī)整問題也值得單獨說。很多團隊會認為加了path.join就安全了這其實是誤解。path.join(/var/app/uploads, ../../etc/passwd)的結果是/var/app/etc/passwd依然越界。還有不少實現(xiàn)用了path.normalize再判斷startsWith但沒有注意到startsWith(/var/app/uploads)對/var/app/uploads_evil/file也會返回true——這類問題我在實際代碼評審里遇到過太多次。CVE-2025-68428 的具體根因就是“路徑前綴字符串比對”加“用戶可控的相對路徑拼接”。修復前只要上傳目錄中存在攻擊者可控的符號鏈接或者目標服務器存在其他可通過相對路徑到達的文件漏洞就能穩(wěn)定觸發(fā)。這不是 jsPDF 一個庫的錯而是“API 使用模式”層面埋下的雷。2.3 最容易踩中的三類使用場景我歸納了三類高風險用法只要命中其中一條就該對照本文后續(xù)的修復方案做加固。第一類是直接透傳用戶輸入作為文件路徑的服務端接口典型的就是上文那個imagePath參數(shù)。這類接口在內部系統(tǒng)中尤其多因為“反正內網(wǎng)、外部訪問不到”的僥幸心理參數(shù)校驗往往最薄弱。第二類是模板文件或配置文件中引用外部資源。PDF 模板引擎允許在模板里寫img src${userData.logo} /然后由服務端渲染層去讀取logo對應的文件。如果logo字段來源是用戶上傳或外部系統(tǒng)路徑穿越就會通過模板注入的方式卷土重來。第三類是批量導出時拼接文件名。導出用戶指定 ID 的文件ID 是數(shù)字但代碼寫著const file path.join(BASE, id - userInput .png)其中userInput若來自查詢參數(shù)攻擊者插入../就能修改最終路徑的目錄層級實現(xiàn)跨目錄讀取。這三類場景的共同特征是PDF 內容生成的成功與否直接取決于外部傳入路徑能否被文件系統(tǒng)正確處理。而 jsPDF 對此沒有任何干預能力攔路職責完全在業(yè)務代碼。3. 環(huán)境復現(xiàn)與驗證過程3.1 搭建最小復現(xiàn)環(huán)境復現(xiàn)不用追求“打得穿生產(chǎn)系統(tǒng)”我把實驗控制在完全隔離的容器環(huán)境里模擬目標服務的調用鏈。環(huán)境由三部分組成一個接收圖片路徑參數(shù)的服務端接口、一個受限的資源目錄、以及一個用于確認“文件內容確實進了 PDF”的解析腳本。實驗目錄結構如下/opt/pdf-lab/ ├── uploads/ # 業(yè)務允許讀取的資源目錄 │ └── demo.png # 合法圖片 ├── secret/ # 模擬敏感數(shù)據(jù)目錄 │ └── internal.txt ├── server.js # 模擬 PDF 生成服務 └── inspect.js # PDF 內容提取腳本server.js的核心就是前文那段fs.readFileSync(imagePath)的代碼只是我在外面套了一層express路由并限制監(jiān)聽在本地回環(huán)地址上。整個環(huán)境不連接外部網(wǎng)絡所有驗證動作都在容器內閉環(huán)完成。這個環(huán)境的價值在于它把真實的業(yè)務復雜度剝離掉只留下漏洞鏈路的最小骨架。排查任何同類問題都可以先抽象出這樣的最小場景比自己一頭扎進生產(chǎn)日志效率高得多。3.2 從請求到 PDF 輸出的完整驗證驗證思路是先確認合法路徑能正常生成 PDF再驗證越界路徑是否也能生成 PDF最后從 PDF 中提取數(shù)據(jù)判斷內容來源。第一步我構造了一個指向uploads/demo.png的請求接口返回200PDF 能正常打開說明基線是通的。第二步我把參數(shù)改成../secret/internal.txt構造了模擬攻擊請求。服務端代碼里fs.readFileSync(../secret/internal.txt)會依據(jù)服務進程的工作目錄解析。為了讓問題更可控我在啟動server.js時把工作目錄固定在/opt/pdf-lab/因此該相對路徑實際指向/opt/secret/internal.txt。這里要注意工作目錄不同越界讀取的落點就不同這也是復現(xiàn)時最容易被忽略的變量。第三步我用inspect.js解析生成的 PDF。PDF 內部對象結構里嵌入的圖片數(shù)據(jù)通常以流對象存放我用pdfjs這類解析庫把流內容提取成文本。結果internal.txt的內容完整出現(xiàn)在 PDF 的頁面數(shù)據(jù)里。整個驗證過程最關鍵的觀察點是jsPDF 沒有對非圖片數(shù)據(jù)報錯。這讓我意識到這類漏洞不只是“讀取文件”它把任意文本文件內容都變成了可回顯的 PDF 內容這成了一種隱蔽的帶外通道——傳統(tǒng) HTTP 響應里如果直接輸出/etc/passwdWAF 很容易識別但塞進 PDF 后檢測難度明顯上升。3.3 危害層級的判斷標準驗證完成后我按三個維度評估危害層級。第一維度是可讀取文件的范圍取決于服務進程的運行身份。如果服務以高權限用戶運行影響面會顯著擴大如果以受限容器用戶運行則只有該用戶可讀的文件受影響。第二維度是讀取結果能否被穩(wěn)定提取。我們驗證了文本文件內容能完整進入 PDF但二進制文件、超大文件不一定能穩(wěn)定提取。如果文件讀取后直接交給addImage部分解析版本遇到超大或異常二進制數(shù)據(jù)可能拋異常這種情況下漏洞就變成“服務端拒絕服務”而非“信息泄露”危害類型會轉化。第三維度是是否有二次寫入或聯(lián)動利用。如果 PDF 生成接口還能控制輸出文件保存位置攻擊者就存在把越界讀取的文件寫入自定義路徑的可能這時漏洞就從信息泄露升級為文件寫入危害等級完全不一樣了。任何復現(xiàn)工作都不能只停留在“讀到了/etc/passwd”這一步一定要把讀取范圍、輸出穩(wěn)定性、后續(xù)聯(lián)動三個問題全部回答清楚風險評估才有依據(jù)。4. 影響范圍與風險評估4.1 攻擊前置條件的全景梳理這類漏洞不會無差別命中所有 jsPDF 項目它需要一組前置條件同時成立。我把條件列全方便對照自檢。前置條件說明常見滿足場景服務端使用 jsPDF純?yōu)g覽器端使用無法直接讀取服務器文件Node.js 封裝導出服務用戶可控路徑直通文件讀取函數(shù)缺少白名單校驗或路徑規(guī)范圖片上傳、模板資源加載、文件導出接口讀取產(chǎn)物會被編碼進 PDF使用addImage/html等方式嵌入二進制內容報告生成、證書導出、郵件附件服務進程具備跨目錄讀取權限容器或主機賬號權限未收斂以 root 運行、共享賬號、掛載盤權限過大四條缺一不可。反過來看只要斷掉其中一環(huán)漏洞就無法形成閉環(huán)。這也是我最推薦做“縱深防御”的原因——與其賭單點校驗做得完美不如讓攻擊者即使突破一環(huán)也走不通整條鏈路。4.2 受影響的應用形態(tài)與現(xiàn)實場景受影響最大的應用形態(tài)是“上傳 → 生成 PDF → 下載”模式。這種模式里用戶上傳文件后系統(tǒng)需要讀取文件內容并渲染為 PDF。如果上傳目錄與被讀取目錄分離且文件名由服務器生成風險相對較低如果文件名由用戶拼接、路徑由用戶指定風險就會顯著上升。第二類受影響形態(tài)是電子合同和票據(jù)系統(tǒng)。這類系統(tǒng)里存在大量模板文件例如印章圖片、簽名圖片、企業(yè) Logo。模板配置常允許非開發(fā)人員修改圖片路徑一旦運維同事把路徑寫成變量且變量來源于登錄用戶輸入路徑穿越就有機會借道模板進入 PDF。第三類受影響形態(tài)是數(shù)據(jù)分析報表服務平臺。平臺提供“導出帶圖表的 PDF 報告”能力圖表圖片先落盤再讀取。報表服務通常運行在數(shù)據(jù)處理集群中可讀文件范圍往往包括數(shù)據(jù)管道配置、連接串參數(shù)等敏感信息一旦被 PDF 帶出泄露的就是基礎設施層面的密鑰材料。在這些場景里jsPDF 的版本新舊反而不是核心問題——只要調用鏈存在新版本 jsPDF 一樣會被帶進坑里。所以評估影響范圍時重點不是“要不要升級 jsPDF”而是“我們的調用鏈上有沒有路徑直通”。4.3 對企業(yè)業(yè)務的潛在危害分解危害至少體現(xiàn)在三個層面。短期看是敏感信息泄露服務器上的配置、源碼、密鑰、業(yè)務數(shù)據(jù)都能被轉成 PDF 外帶這個危害最直接也是漏洞定級的主要依據(jù)。中期看是攻擊面擴大。一旦拿到了服務器上的配置文件和連接信息攻擊者就有條件進入內網(wǎng)橫向階段。PDF 導出接口通常部署在業(yè)務前置機上與核心數(shù)據(jù)庫的網(wǎng)絡可達性往往沒有被嚴格隔離這就給“PDF 漏洞 → 內網(wǎng)突破”提供了跳板。長期看是信任體系受損。這類漏洞暴露出的是團隊在“外部輸入不可信”這條安全基線上的執(zhí)行問題修復一個點容易但類似問題如果遍布其他服務安全團隊就很難逐個排查。我復盤時最大的感觸是單點漏洞的價值不只是修復它本身更重要的是借此推動一次團隊層面的安全設計評審把“用戶可控路徑”這類反模式從所有代碼庫里清理干凈。5. 修復方案與防御實踐5.1 版本升級不是萬能藥先說結論如果你看到 jsPDF 新版本修復了某個路徑處理缺陷當然建議升級但別把升級當作終點。很多同類漏洞的本質不在 jsPDF 內部而是業(yè)務代碼把不受信任的路徑傳進了文件讀取函數(shù)。這種情況下哪怕升級到最新版你的代碼依然會把../../etc/passwd交給fs.readFileSync。真正的修復順序應該是先清理調用鏈再談版本。調用鏈上只要用戶輸入沒有經(jīng)過嚴格白名單就不該有任何文件系統(tǒng) API 能接觸它。5.2 代碼層面的三層校驗硬規(guī)范我把自己在實際修復中落地的三層校驗邏輯分享出來。第一層是白名單前綴校驗但不是簡單的startsWith字符串比較而是規(guī)范化后再比較并且強制加上分隔符const path require(path); const fs require(fs); function safeResolve(baseDir, userPath) { // 先解析成絕對路徑再統(tǒng)一用路徑前綴判斷 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這類庫檢查文件魔數(shù)確認它確實是圖片。這樣即使第一層校驗被繞過非預期文件也會在進入 PDF 之前被拒絕。第三層是最小權限運行。服務進程使用獨立低權限用戶運行容器內不掛載與業(yè)務無關的數(shù)據(jù)盤。即使攻擊者成功越界讀取也會受限于賬號權限讀不到系統(tǒng)關鍵文件。這里有個很實用的細節(jié)很多團隊在校驗文件名時圖省事直接判斷userPath.indexOf(..) -1看似簡單粗暴有效但遇到絕對路徑就失靈。建議一律用path.resolve做規(guī)范化之后再判斷而不是對原始字符串做直覺式檢查。5.3 縱深防御從入口到出站的多層兜底代碼修復之后我還在整個鏈路里補了四道縱深防線確保未來即使有人改代碼引入了類似問題也有其他機制兜底。第一道防線是統(tǒng)一文件訪問網(wǎng)關。所有 PDF 生成服務不再直接調用fs.readFileSync而是通過一個內部文件訪問服務獲取資源。這個服務只暴露按ID取文件的接口不接受路徑參數(shù)從源頭上掐斷路徑直通。第二道防線是出站內容檢測。在 PDF 生成接口的響應出口加一層內容檢測如果 PDF 內嵌入的數(shù)據(jù)熵值過高、含有明顯的配置文件特征例如出現(xiàn)密鑰字段名稱則攔截響應并告警。這套邏輯不追求完美識別只求針對已知泄露模式提高攻擊成本。第三道防線是運行時行為監(jiān)控。針對服務進程監(jiān)控異常文件讀取行為例如在短時間內讀取了大量非圖片擴展名文件、讀取了不在掛載目錄白名單內的文件。這些行為特征一旦命中就直接切斷進程或觸發(fā)熔斷。第四道防線是WAF 和網(wǎng)關的懶校驗兜底。在 Web 網(wǎng)關層對請求參數(shù)做啟發(fā)式檢測凡是路徑參數(shù)中帶有../、.../、%2e%2e等典型穿越特征一律攔截。注意這個方案只用于兜底不能作為主要防御因為編碼繞過的變種太多主防御必須放在應用層。四道防線的投入成本逐級遞增實際落地不必一步到位。我的建議是先做代碼修復和應用層校驗再逐步推進文件訪問網(wǎng)關化和行為監(jiān)控這些措施單獨拎出來都不復雜但疊加在一起就能把同類漏洞的可利用性壓到很低。6. 常見問題與排查實錄6.1 復現(xiàn)階段最容易踩的四個坑復現(xiàn)過程中我踩過不少坑挑四個影響最大的記錄下來。第一個坑是工作目錄漂移。fs.readFileSync(../secret.txt)的解析依賴進程啟動時的cwd不同啟動方式下得到的絕對路徑完全不同。排查時如果不確認服務進程的工作目錄會得出“漏洞不存在”的錯誤結論。解決方案是復現(xiàn)時顯式指定cwd并在日志中打印絕對路徑用于對賬。第二個坑是相對路徑拼接被提前規(guī)整。有的版本path.join(base, ../../x)會自動把結果歸一化到當前盤的根目錄看起來像是“沒有越界”實際是已經(jīng)越過。要區(qū)分path.join與path.resolve的行為復現(xiàn)時優(yōu)先用path.resolve看真實解析結果。第三個坑是PDF 解析器對圖片流的處理不一致。某些 PDF 解析庫會把嵌錯格式的圖片數(shù)據(jù)丟棄導致明明服務端讀取了文件卻無法從 PDF 里提取出內容。建議同時用兩種解析方式確認一是直接解析 PDF 對象流二是用文本提取工具全量掃描避免誤判。第四個坑是符號鏈接繞過。攻擊者如果能在上傳目錄里放置符號鏈接指向敏感文件那么即使路徑前綴校驗全做對了也會被鏈接帶偏。這類問題在文件上傳一鍵校驗的場景里尤其隱蔽需要在安全策略中收緊“上傳目錄不允許創(chuàng)建符號鏈接”等文件系統(tǒng)權限。6.2 一次真實排查案例的完整記錄我們內部某業(yè)務線也出現(xiàn)過類似告警排查過程很有參考價值。業(yè)務場景是用戶上傳簽名圖片服務端導出 PDF 合同。安全巡檢告警顯示某個請求的圖片參數(shù)里出現(xiàn)了多次../編碼變體。排查第一步是定位參數(shù)入口。我們找到GET /contract/export?signxxx.png接口sign參數(shù)直接拼接在讀取路徑上。當時代碼里有一段注釋寫著“圖片參數(shù)已校驗僅支持本地上傳文件”但校驗實現(xiàn)是判斷參數(shù)是否以/uploads/開頭。第二步是驗證編碼繞過。安全巡檢攔截的是明文../但攻擊者改用了%2e%2e%2f三段編碼應用層解碼后變?yōu)?./。由于校驗發(fā)生在解碼之前字符串比對看到的是%2e%2e...而以/uploads/開頭的校驗自然被跳過。這個案例充分說明校驗必須發(fā)生在解碼、規(guī)范化之后否則等于白寫。第三步是修復驗證。我們直接用修復后的safeResolve函數(shù)跑了一批歷史攻擊樣本包括普通穿越、編碼穿越、絕對路徑注入、符號鏈接引用結果全部返回“路徑越界”錯誤。同時用合法上傳文件回歸測試確認功能未受影響。這類排查的記錄價值在于它把抽象的風險變成了可追溯的流程。安全團隊面對告警時如果能按“入口參數(shù)確認 → 解碼鏈路梳理 → 規(guī)范化校驗 → 攻擊樣本回歸”的順序推進排查效率會高很多。6.3 安全復盤清單與個人心得復盤結束后我整理了一份自查清單供團隊在設計和評審階段直接使用。清單重點不是“jsPDF 有哪些選項”而是“這份 PDF 導出代碼面對外部輸入時是否守住了邊界”。是否允許用戶直接傳文件路徑、圖片地址、模板路徑路徑讀取前是否完成了解碼和規(guī)范化路徑校驗是否基于解析后的絕對路徑而不是原始字符串文件讀取后是否進行了文件類型魔數(shù)校驗服務進程是否以最小權限運行可讀文件范圍是否明確上傳目錄是否禁止符號鏈接和可執(zhí)行文件PDF 接口響應是否需要出站內容檢測是否建立了針對異常文件讀取行為的安全監(jiān)控清單最后一條我特別想強調不要只問“這個庫有沒有漏洞”要問“我們的代碼把用戶輸入放在什么位置”。jsPDF 的 CVE-2025-68428 這類編號只是表象表象之下的問題是“路徑參數(shù)直通文件系統(tǒng)”這個反模式。把反模式從所有業(yè)務代碼里清除干凈比盯著某一個 CVE 打補丁更有長期價值。在做完這輪研究后我個人對“前端庫安全邊界”的認知又刷新了一次。過去總覺得瀏覽器里跑的代碼碰不到服務器文件天然安全實際上一旦同一套庫被搬到 Node 端邊界就完全變了。每次引入開源組件時除了看功能、看性能真的應該多問一句這個組件將來會不會被放在服務端處理不可信輸入如果會那安全設計就必須前置到接入的那一天而不是等出了漏洞再補課。