VYPR
Critical severityOSV Advisory· Published Jan 5, 2026· Updated Jan 6, 2026

jsPDF has Local File Inclusion/Path Traversal vulnerability

CVE-2025-68428

Description

jsPDF is a library to generate PDFs in JavaScript. Prior to version 4.0.0, user control of the first argument of the loadFile method in the node.js build allows local file inclusion/path traversal. If 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. Other affected methods are addImage, html, and addFont. Only the node.js builds of the library are affected, namely the dist/jspdf.node.js and dist/jspdf.node.min.js files. The vulnerability has been fixed in jsPDF@4.0.0. This version restricts file system access per default. This semver-major update does not introduce other breaking changes. Some workarounds areavailable. With recent node versions, jsPDF recommends using the --permission flag in production. The feature was introduced experimentally in v20.0.0 and is stable since v22.13.0/v23.5.0/v24.0.0. For older node versions, sanitize user-provided paths before passing them to jsPDF.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
jspdfnpm
< 4.0.04.0.0

Affected products

1

Patches

1
a688c8f47992

restrict file system access in node build (#3931)

https://github.com/parallax/jsPDFLukas HolländerDec 18, 2025via ghsa
6 files changed · +224 9
  • README.md+30 0 modified
    @@ -119,6 +119,36 @@ doc.save("a4.pdf");
     
     </details>
     
    +## Security
    +
    +We strongly advise you to sanitize user input before passing it to jsPDF!
    +
    +For reporting security vulnerabilities, please see [SECURITY.md](https://github.com/parallax/jsPDF/blob/master/SECURITY.md).
    +
    +### Reading files from the local file system on node
    +
    +When running under Node.js, jsPDF will restrict reading files from the local file system by default.
    +
    +Strongly recommended: use Node's permission flags so the runtime enforces access:
    +
    +```sh
    +node --permission --allow-fs-read=... ./scripts/generate.js
    +```
    +
    +See [Node's documentation](https://nodejs.org/api/permissions.html) for details. Note that you need to include
    +all imported JavaScript files (including all dependencies) in the `--allow-fs-read` flag.
    +
    +Fallback (not recommended): you can allow jsPDF to read specific files by setting `jsPDF.allowFsRead` in your script.
    +
    +```js
    +import { jsPDF } from "jspdf";
    +
    +const doc = new jsPDF();
    +doc.allowFsRead = ["./fonts/*", "./images/logo.png"]; // allow everything under ./fonts and a single file
    +```
    +
    +Warning: We strongly recommend the Node flags over `jsPDF.allowFsRead`, as the flags are enforced by the runtime and offer stronger security.
    +
     ### Optional dependencies
     
     Some functions of jsPDF require optional dependencies. E.g. the `html` method, which depends on `html2canvas` and,
    
  • src/modules/fileloading.js+78 5 modified
    @@ -14,8 +14,6 @@ import { jsPDF } from "../jspdf.js";
      * @module
      */
     (function(jsPDFAPI) {
    -  "use strict";
    -
       /**
        * @name loadFile
        * @function
    @@ -31,10 +29,44 @@ import { jsPDF } from "../jspdf.js";
     
         // @if MODULE_FORMAT='cjs'
         // eslint-disable-next-line no-unreachable
    -    return nodeReadFile(url, sync, callback);
    +    return nodeReadFile.call(this, url, sync, callback);
         // @endif
       };
     
    +  /**
    +   * @name allowFsRead
    +   * @property
    +   * @type {string[]|undefined}
    +   *
    +   * Controls which local files may be read by jsPDF when running under Node.js.
    +   *
    +   * Security recommendation:
    +   * - We strongly recommend using Node's permission flags (`node --permission --allow-fs-read=...`) instead of this property,
    +   *   especially in production. The Node flags are enforced by the runtime and provide stronger guarantees.
    +   *
    +   * Behavior:
    +   * - When present, jsPDF will allow reading only if the requested, resolved absolute path matches any entry in this array.
    +   * - Each entry can be either:
    +   *   - An absolute or relative file path for an exact match, or
    +   *   - A prefix ending with a single wildcard `*` to allow all paths starting with that prefix.
    +   * - Examples of allowed patterns:
    +   *   - `"./fonts/MyFont.ttf"` (exact match by resolved path)
    +   *   - `"/abs/path/to/file.txt"` (exact absolute path)
    +   *   - `"./assets/*"` (any file whose resolved path starts with the resolved `./assets/` directory)
    +   *
    +   * Notes:
    +   * - If Node's permission API is available (`process.permission`), it is checked first. If it denies access, reading will fail regardless of `allowFsRead`.
    +   * - If neither `process.permission` nor `allowFsRead` is set, reading from the local file system is disabled and an error is thrown.
    +   *
    +   * Example:
    +   * ```js
    +   * const doc = jsPDF();
    +   * doc.allowFsRead = ["./fonts/*", "./images/logo.png"]; // allow everything under ./fonts and a single file
    +   * const ttf = doc.loadFile("./fonts/MyFont.ttf", true);
    +   * ```
    +   */
    +  jsPDFAPI.allowFsRead = undefined;
    +
       /**
        * @name loadImageFile
        * @function
    @@ -98,10 +130,51 @@ import { jsPDF } from "../jspdf.js";
         var fs = require("fs");
         var path = require("path");
     
    -    url = path.resolve(url);
    +    if (!process.permission && !this.allowFsRead) {
    +      throw new Error(
    +        "Trying to read a file from local file system. To enable this feature either run node with the --permission and --allow-fs-read flags or set the jsPDF.allowFsRead property."
    +      );
    +    }
    +
    +    try {
    +      url = fs.realpathSync(path.resolve(url));
    +    } catch (e) {
    +      if (sync) {
    +        return undefined;
    +      } else {
    +        callback(undefined);
    +        return;
    +      }
    +    }
    +
    +    if (process.permission && !process.permission.has("fs.read", url)) {
    +      throw new Error(`Cannot read file '${url}'. Permission denied.`);
    +    }
    +
    +    if (this.allowFsRead) {
    +      const allowRead = this.allowFsRead.some(allowedUrl => {
    +        const starIndex = allowedUrl.indexOf("*");
    +        if (starIndex >= 0) {
    +          const fixedPart = allowedUrl.substring(0, starIndex);
    +          let resolved = path.resolve(fixedPart);
    +          if (fixedPart.endsWith(path.sep) && !resolved.endsWith(path.sep)) {
    +            resolved += path.sep;
    +          }
    +          return url.startsWith(resolved);
    +        } else {
    +          return url === path.resolve(allowedUrl);
    +        }
    +      });
    +      if (!allowRead) {
    +        throw new Error(`Cannot read file '${url}'. Permission denied.`);
    +      }
    +    }
    +
         if (sync) {
           try {
    -        result = fs.readFileSync(url, { encoding: "latin1" });
    +        result = fs.readFileSync(url, {
    +          encoding: "latin1"
    +        });
           } catch (e) {
             return undefined;
           }
    
  • test/specs/fileloading.spec.js+103 0 modified
    @@ -8,18 +8,27 @@ describe("Module: FileLoad", () => {
           : "/base/test/reference/success.txt";
       it("should load a file (sync)", () => {
         const doc = jsPDF();
    +    if (typeof isNode !== "undefined" && isNode) {
    +      doc.allowFsRead = [successURL];
    +    }
         var file = doc.loadFile(successURL, undefined, undefined);
         expect(file).toEqual("success");
       });
     
       it("should fail to load a file (sync)", () => {
         const doc = jsPDF();
    +    if (typeof isNode !== "undefined" && isNode) {
    +      doc.allowFsRead = ["fail.txt"];
    +    }
         var file = doc.loadFile("fail.txt", undefined, undefined);
         expect(file).toEqual(undefined);
       });
     
       it("should load a file (async)", done => {
         const doc = jsPDF();
    +    if (typeof isNode !== "undefined" && isNode) {
    +      doc.allowFsRead = [successURL];
    +    }
         doc.loadFile(successURL, false, function(data) {
           expect(data).toEqual("success");
           done();
    @@ -28,9 +37,103 @@ describe("Module: FileLoad", () => {
     
       it("should fail to load a file (async)", done => {
         const doc = jsPDF();
    +    if (typeof isNode !== "undefined" && isNode) {
    +      doc.allowFsRead = ["fail.txt"];
    +    }
         doc.loadFile("fail.txt", false, function(data) {
           expect(data).toEqual(undefined);
           done();
         });
       });
     });
    +
    +if (typeof isNode !== "undefined" && isNode) {
    +  const path = require("path");
    +
    +  describe("Module: FileLoad (Node permissions)", () => {
    +    const absSuccess = path.resolve("./test/reference/success.txt");
    +    let originalPermission;
    +
    +    beforeEach(() => {
    +      originalPermission = process.permission;
    +    });
    +
    +    afterEach(() => {
    +      process.permission = originalPermission;
    +    });
    +
    +    it("should throw if neither process.permission nor jsPDF.allowFsRead is set", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = undefined;
    +      process.permission = undefined;
    +
    +      expect(() => {
    +        doc.loadFile(absSuccess, true);
    +      }).toThrowError(/Trying to read a file from local file system/);
    +    });
    +
    +    it("should allow reading via process.permission for exact absolute path", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = undefined;
    +      process.permission = {
    +        has: (perm, url) => perm === "fs.read" && url === absSuccess
    +      };
    +
    +      const data = doc.loadFile(absSuccess, true);
    +      expect(data).toEqual("success");
    +    });
    +
    +    it("should deny reading via process.permission when has() returns false", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = undefined;
    +      process.permission = {
    +        has: () => false
    +      };
    +
    +      expect(() => {
    +        doc.loadFile(absSuccess, true);
    +      }).toThrowError(/Permission denied/);
    +    });
    +
    +    it("should allow reading via process.permission with wildcard-like directory prefix", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = undefined;
    +      const allowedDir = path.resolve("./test/reference/");
    +      process.permission = {
    +        has: (perm, url) => perm === "fs.read" && url.startsWith(allowedDir)
    +      };
    +
    +      const data = doc.loadFile(absSuccess, true);
    +      expect(data).toEqual("success");
    +    });
    +
    +    it("should allow reading via jsPDF.allowFsRead using absolute path (no wildcard)", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = [absSuccess];
    +      const data = doc.loadFile(absSuccess, true);
    +      expect(data).toEqual("success");
    +    });
    +
    +    it("should allow reading via jsPDF.allowFsRead using relative path (no wildcard)", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = ["./test/reference/success.txt"];
    +      const data = doc.loadFile("./test/reference/success.txt", true);
    +      expect(data).toEqual("success");
    +    });
    +
    +    it("should allow reading via jsPDF.allowFsRead using wildcard prefix", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = ["./test/reference/*"];
    +      const data = doc.loadFile("./test/reference/success.txt", true);
    +      expect(data).toEqual("success");
    +    });
    +
    +    it("should deny reading when jsPDF.allowFsRead pattern does not match", () => {
    +      const doc = jsPDF();
    +      doc.allowFsRead = ["./other/dir/*", "./test/reference/deny.txt"];
    +      expect(() => {
    +        doc.loadFile("./test/reference/success.txt", true);
    +      }).toThrowError(/Permission denied/);
    +    });
    +  });
    +}
    
  • test/specs/text.spec.js+10 4 modified
    @@ -179,6 +179,7 @@ break`
         const doc = jsPDF({ floatPrecision: 2 });
         var PTSans;
         if (typeof global === "object" && global.isNode === true) {
    +      doc.allowFsRead = ["./test/reference/PTSans.ttf"];
           PTSans = doc.loadFile("./test/reference/PTSans.ttf");
         } else {
           PTSans = doc.loadFile("base/test/reference/PTSans.ttf");
    @@ -187,10 +188,15 @@ break`
         doc.addFont("PTSans.ttf", "PTSans", "normal");
         doc.setFont("PTSans");
         doc.setFontSize(10);
    -    doc.text("А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! ", 10, 10, {
    -      align: "justify",
    -      maxWidth: 100,
    -    });
    +    doc.text(
    +      "А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! А ну чики брики и в дамки! ",
    +      10,
    +      10,
    +      {
    +        align: "justify",
    +        maxWidth: 100
    +      }
    +    );
         comparePdf(doc.output(), "justify-custom-font.pdf", "text");
       });
     
    
  • test/specs/ttfsupport.spec.js+2 0 modified
    @@ -16,6 +16,7 @@ describe("TTFSupport", () => {
         });
         var PTSans;
         if (typeof global === "object" && global.isNode === true) {
    +      doc.allowFsRead = ["./test/reference/PTSans.ttf"];
           PTSans = doc.loadFile("./test/reference/PTSans.ttf");
         } else {
           PTSans = doc.loadFile("base/test/reference/PTSans.ttf");
    @@ -37,6 +38,7 @@ describe("TTFSupport", () => {
         });
     
         if (typeof global === "object" && global.isNode === true) {
    +      doc.allowFsRead = ["./test/reference/PTSans.ttf"];
           doc.addFont("./test/reference/PTSans.ttf", "PTSans", "normal");
         } else {
           doc.addFont("base/test/reference/PTSans.ttf", "PTSans", "normal");
    
  • types/index.d.ts+1 0 modified
    @@ -1099,6 +1099,7 @@ declare module "jspdf" {
           sync: false,
           callback: (data: string) => string
         ): void;
    +    allowFsRead: string[] | undefined;
     
         // jsPDF plugin: html
         html(src: string | HTMLElement, options?: HTMLOptions): HTMLWorker;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.