{"repro_id":"REPRO-2026-00044","version":5,"title":"jsPDF Local File Inclusion/Path Traversal in Node.js builds","repro_type":"security","status":"published","severity":"critical","description":"User control of the first argument of the loadFile method in the node.js build allows local file inclusion/path traversal.\n\nIf given the possibility to pass unsanitized paths to the loadFile method, a user can retrieve file contents of arbitrary files in the local file system the node process is running in. The file contents are included verbatim in the generated PDFs.\n\nOther affected methods are: addImage, html, addFont.\n\nOnly the node.js builds of the library are affected, namely the dist/jspdf.node.js and dist/jspdf.node.min.js files.","root_cause":"# Root Cause Analysis - GHSA-f8cm-6447-x5h2 (jsPDF Local File Inclusion / Path Traversal)\n\nSummary\n- Vulnerability: Local File Inclusion/Path Traversal via file-loading helpers in jsPDF Node builds\n- Impact: Attacker-controlled path arguments to jsPDF APIs can read arbitrary files on the server and embed their raw bytes into generated PDFs\n- Affected: jsPDF <= 3.0.4 (Node builds: dist/jspdf.node.js and dist/jspdf.node.min.js)\n- Fixed: jsPDF >= 4.0.0 (default deny FS reads; requires explicit enable or Node --permission)\n\nReproduction Evidence\n- Vulnerable version 3.0.4: Passing a local path to doc.addImage(secret.txt, ...) produced a PDF containing the secret token from a local file\n  - See logs: logs/run_*.log -> shows [HIT] for vulnerable:addImage\n  - Artifact: repro_work/vuln/out_addImage.pdf contains the token string (search via `grep -a`)\n- Patched version 4.0.0 (latest at test time): Same PoC raises an error and does not create a PDF\n  - Error: \"Trying to read a file from local file system... set jsPDF.allowFsRead or use --permission --allow-fs-read\"\n  - No token present in patched outputs; bypass suite attempts did not exfiltrate\n\nRoot Cause Details\n- In Node builds <=3.0.4, helper function loadFile() directly accepted user-provided paths and read files without restriction. Several high-level APIs (addImage, html, addFont) used this helper when passed a string path (as opposed to binary data), causing raw file contents to be embedded in the PDF stream.\n- The 4.0.0 patch introduces permission gating for file system access with jsPDF.allowFsRead defaulting to false and honoring Node.js permission model flags. Calls trying to resolve string paths emit a hard error unless enabled.\n\nExploitation Path\n1. Import jsPDF Node build (require('jspdf/dist/jspdf.node.js'))\n2. Provide attacker-controlled relative or absolute path to doc.addImage (or html/addFont)\n3. Vulnerable loader reads the file and embeds content verbatim into the PDF\n4. Attacker obtains the PDF and extracts embedded sensitive content\n\nPatched Behavior Verification\n- Tested latest npm release (4.0.0) which is newer than the first patched tag or equal; verified it blocks file reads by default\n- Error message confirms new permission gate; no PDFs contained secret token\n\nMitigations/Recommendations\n- Upgrade to jsPDF >= 4.0.0\n- In production Node 22+/23+/24+, use --permission with --allow-fs-read narrowly scoped if jsPDF needs local asset reads\n- For older Node versions, sanitize and whitelist any paths passed to jsPDF APIs; avoid passing user-provided paths directly\n\nArtifacts\n- logs/run_*.log: Full run logs including errors and hits/misses\n- logs/summary.log: Reproduction summary (FOUND/MISSING etc.)\n- repro_work/vuln/out_addImage.pdf: Vulnerable PoC artifact containing secret token\n- repro_work/patched/*: Patched tests and bypass attempts outputs\n","ghsa_id":"GHSA-f8cm-6447-x5h2","cve_id":"CVE-2025-68428","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2025-68428","package":{"name":"jspdf","ecosystem":"npm","affected_versions":"<= 3.0.4","fixed_version":"4.0.0"},"reproduced_at":"2026-01-07T19:54:33.503895+00:00","duration_secs":493.5013349056244,"tool_calls":34,"turns":2,"handoffs":1,"quality":{"idempotent_verified":false,"community_verifications":0},"key_moments":[{"agent":"repro","description":"The reproduction script was written to automate the exploit steps.","index":8,"time":493498.694896698,"title":"Repro Script Created","type":"poc_created"},{"agent":"repro","description":"The vulnerable environment was set up and the reproduction script was run.","index":9,"time":493498.9500045776,"title":"Repro Script Executed","type":"env_setup"},{"agent":"repro","description":"The exploit was executed again to trigger the vulnerability.","index":14,"time":493499.40490722656,"title":"Exploit Attempted","type":"vuln_triggered"},{"agent":"repro","description":"A critical secret token related to the vulnerability was found in the logs.","index":19,"time":493499.6469020843,"title":"Secret Token Located","type":"discovery"},{"agent":"repro","description":"A fully self-contained reproduction was created and validated successfully.","index":37,"time":493500.71716308594,"title":"Repro Validated","type":"confirmation"}],"published_at":"2026-01-07T19:54:50.420649+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":2860,"category":"analysis"},{"path":"reproduction_steps.sh","filename":"reproduction_steps.sh","size":11524,"category":"reproduction_script"},{"path":"logs/run_20260107_182602.log","filename":"run_20260107_182602.log","size":26460,"category":"log"},{"path":"logs/run_20260107_182758.log","filename":"run_20260107_182758.log","size":27579,"category":"log"},{"path":"logs/run_20260107_182811.log","filename":"run_20260107_182811.log","size":27579,"category":"log"},{"path":"logs/summary.log","filename":"summary.log","size":112,"category":"log"},{"path":"repro/patch_analysis.md","filename":"patch_analysis.md","size":3167,"category":"documentation"},{"path":"repro/repro_notes.txt","filename":"repro_notes.txt","size":500,"category":"other"},{"path":"repro_work/patched/attempts/5_addFont_rel.pdf","filename":"5_addFont_rel.pdf","size":3318,"category":"other"},{"path":"repro_work/patched/attempts/6_addFont_abs.pdf","filename":"6_addFont_abs.pdf","size":3387,"category":"other"},{"path":"repro_work/patched/package.json","filename":"package.json","size":50,"category":"other"},{"path":"repro_work/patched/patch_attempt_1.js","filename":"patch_attempt_1.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_10.js","filename":"patch_attempt_10.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_11.js","filename":"patch_attempt_11.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_2.js","filename":"patch_attempt_2.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_3.js","filename":"patch_attempt_3.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_4.js","filename":"patch_attempt_4.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_5.js","filename":"patch_attempt_5.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_6.js","filename":"patch_attempt_6.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_7.js","filename":"patch_attempt_7.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_8.js","filename":"patch_attempt_8.js","size":1946,"category":"other"},{"path":"repro_work/patched/patch_attempt_9.js","filename":"patch_attempt_9.js","size":1946,"category":"other"},{"path":"repro_work/patched/poc_addimage.js","filename":"poc_addimage.js","size":718,"category":"other"},{"path":"repro_work/vuln/out_addFont.pdf","filename":"out_addFont.pdf","size":3397,"category":"other"},{"path":"repro_work/vuln/out_addImage.pdf","filename":"out_addImage.pdf","size":3473,"category":"other"},{"path":"repro_work/vuln/package.json","filename":"package.json","size":50,"category":"other"},{"path":"repro_work/vuln/poc_addfont.js","filename":"poc_addfont.js","size":674,"category":"other"},{"path":"repro_work/vuln/poc_addimage.js","filename":"poc_addimage.js","size":718,"category":"other"},{"path":"secret.txt","filename":"secret.txt","size":119,"category":"other"},{"path":"ticket.md","filename":"ticket.md","size":2032,"category":"ticket"}]}