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