VYPR
Moderate severityOSV Advisory· Published Feb 2, 2026· Updated Feb 3, 2026

jsPDF Affected by Stored XMP Metadata Injection (Spoofing & Integrity Violation)

CVE-2026-24043

Description

jsPDF is a library to generate PDFs in JavaScript. Prior to 4.1.0, user control of the first argument of the addMetadata function allows users to inject arbitrary XML. If given the possibility to pass unsanitized input to the addMetadata method, a user can inject arbitrary XMP metadata into the generated PDF. If the generated PDF is signed, stored or otherwise processed after, the integrity of the PDF can no longer be guaranteed. The vulnerability has been fixed in jsPDF@4.1.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
jspdfnpm
< 4.1.04.1.0

Affected products

1

Patches

1
efe54bf50f3f

Merge commit from fork

https://github.com/parallax/jsPDFLukas HolländerFeb 2, 2026via ghsa
5 files changed · +97 71
  • src/modules/xmp_metadata.js+75 70 modified
    @@ -26,83 +26,88 @@
     
     import { jsPDF } from "../jspdf.js";
     
    -/**
    - * @name xmp_metadata
    - * @module
    - */
    -(function(jsPDFAPI) {
    -  "use strict";
    +function postPutResources() {
    +  const metadata = this.internal.__metadata__.metadata;
    +  const utf8Metadata = unescape(encodeURIComponent(metadata));
     
    -  var postPutResources = function() {
    -    var xmpmeta_beginning = '<x:xmpmeta xmlns:x="adobe:ns:meta/">';
    -    var rdf_beginning =
    +  const rawXml = this.internal.__metadata__.rawXml;
    +  let content;
    +  if (rawXml) {
    +    content = utf8Metadata;
    +  } else {
    +    const xmpmetaBeginning = '<x:xmpmeta xmlns:x="adobe:ns:meta/">';
    +    const rdfBeginning =
           '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:jspdf="' +
    -      this.internal.__metadata__.namespaceuri +
    +      this.internal.__metadata__.namespaceUri +
           '"><jspdf:metadata>';
    -    var rdf_ending = "</jspdf:metadata></rdf:Description></rdf:RDF>";
    -    var xmpmeta_ending = "</x:xmpmeta>";
    -    var utf8_xmpmeta_beginning = unescape(
    -      encodeURIComponent(xmpmeta_beginning)
    -    );
    -    var utf8_rdf_beginning = unescape(encodeURIComponent(rdf_beginning));
    -    var utf8_metadata = unescape(
    -      encodeURIComponent(this.internal.__metadata__.metadata)
    -    );
    -    var utf8_rdf_ending = unescape(encodeURIComponent(rdf_ending));
    -    var utf8_xmpmeta_ending = unescape(encodeURIComponent(xmpmeta_ending));
    +    const rdfEnding = "</jspdf:metadata></rdf:Description></rdf:RDF>";
    +    const xmpmetaEnding = "</x:xmpmeta>";
     
    -    var total_len =
    -      utf8_rdf_beginning.length +
    -      utf8_metadata.length +
    -      utf8_rdf_ending.length +
    -      utf8_xmpmeta_beginning.length +
    -      utf8_xmpmeta_ending.length;
    +    content =
    +      xmpmetaBeginning +
    +      rdfBeginning +
    +      escapeXml(utf8Metadata) +
    +      rdfEnding +
    +      xmpmetaEnding;
    +  }
     
    -    this.internal.__metadata__.metadata_object_number = this.internal.newObject();
    -    this.internal.write(
    -      "<< /Type /Metadata /Subtype /XML /Length " + total_len + " >>"
    -    );
    -    this.internal.write("stream");
    +  this.internal.__metadata__.metadataObjectNumber = this.internal.newObject();
    +  this.internal.write(
    +    "<< /Type /Metadata /Subtype /XML /Length " + content.length + " >>"
    +  );
    +  this.internal.write("stream");
    +  this.internal.write(content);
    +  this.internal.write("endstream");
    +  this.internal.write("endobj");
    +}
    +
    +function putCatalog() {
    +  if (this.internal.__metadata__.metadataObjectNumber) {
         this.internal.write(
    -      utf8_xmpmeta_beginning +
    -        utf8_rdf_beginning +
    -        utf8_metadata +
    -        utf8_rdf_ending +
    -        utf8_xmpmeta_ending
    +      "/Metadata " + this.internal.__metadata__.metadataObjectNumber + " 0 R"
         );
    -    this.internal.write("endstream");
    -    this.internal.write("endobj");
    -  };
    +  }
    +}
     
    -  var putCatalog = function() {
    -    if (this.internal.__metadata__.metadata_object_number) {
    -      this.internal.write(
    -        "/Metadata " +
    -          this.internal.__metadata__.metadata_object_number +
    -          " 0 R"
    -      );
    -    }
    -  };
    +function escapeXml(str) {
    +  return str
    +    .replace(/&/g, "&amp;")
    +    .replace(/</g, "&lt;")
    +    .replace(/>/g, "&gt;")
    +    .replace(/"/g, "&quot;")
    +    .replace(/'/g, "&apos;");
    +}
     
    -  /**
    -   * Adds XMP formatted metadata to PDF
    -   *
    -   * @name addMetadata
    -   * @function
    -   * @param {String} metadata The actual metadata to be added. The metadata shall be stored as XMP simple value. Note that if the metadata string contains XML markup characters "<", ">" or "&", those characters should be written using XML entities.
    -   * @param {String} namespaceuri Sets the namespace URI for the metadata. Last character should be slash or hash.
    -   * @returns {jsPDF} jsPDF-instance
    -   */
    -  jsPDFAPI.addMetadata = function(metadata, namespaceuri) {
    -    if (typeof this.internal.__metadata__ === "undefined") {
    -      this.internal.__metadata__ = {
    -        metadata: metadata,
    -        namespaceuri: namespaceuri || "http://jspdf.default.namespaceuri/"
    -      };
    -      this.internal.events.subscribe("putCatalog", putCatalog);
    +/**
    + * Adds XMP formatted metadata to PDF.
    + *
    + * WARNING: Passing raw XML is potentially insecure! Always sanitize user input before passing it to this function!
    + * @name addMetadata
    + * @function
    + * @param {string} metadata The actual metadata to be added. The interpretation of this parameter depends on the
    + *   second parameter.
    + * @param {boolean|string|undefined} rawXmlOrNamespaceUri If a string is passed it sets the namespace URI for the
    + *   metadata and the metadata shall be stored as XMP simple value. The last character should be a slash or hash.
    + *
    + *   If this argument is omitted, a string is passed, or `false` is passed, the `metadata` argument will be
    + *   XML-escaped before including it in the PDF.
    + *
    + *   If `true` is passed, the `metadata` argument will be interpreted as raw XMP and will be included verbatim
    + *   in the PDF. The passed metadata must be complete (including surrounding `xmpmeta` and `RDF` tags).
    + * @returns {jsPDF} jsPDF-instance
    + */
    +jsPDF.API.addMetadata = function(metadata, rawXmlOrNamespaceUri) {
    +  if (typeof this.internal.__metadata__ === "undefined") {
    +    this.internal.__metadata__ = {
    +      metadata: metadata,
    +      namespaceUri:
    +        rawXmlOrNamespaceUri ?? "http://jspdf.default.namespaceuri/",
    +      rawXml:
    +        typeof rawXmlOrNamespaceUri === "boolean" ? rawXmlOrNamespaceUri : false
    +    };
    +    this.internal.events.subscribe("putCatalog", putCatalog);
     
    -      this.internal.events.subscribe("postPutResources", postPutResources);
    -    }
    -    return this;
    -  };
    -})(jsPDF.API);
    +    this.internal.events.subscribe("postPutResources", postPutResources);
    +  }
    +  return this;
    +};
    
  • test/reference/xmpmetadata-escaped.pdf+0 0 added
  • test/reference/xmpmetadata-rawXml.pdf+0 0 added
  • test/specs/xmpmetadata.spec.js+17 0 modified
    @@ -2,14 +2,31 @@
     
     describe("Module: xmp_metadata", () => {
       beforeAll(loadGlobals);
    +
       it("make some metadata var. 1", () => {
         var doc = new jsPDF({ putOnlyUsedFonts: true, floatPrecision: 2 });
         doc.addMetadata("My metadata as a string.", "http://my.namespace.uri/");
         comparePdf(doc.output(), "xmpmetadata.pdf");
       });
    +
       it("make some metadata var. 2", () => {
         var doc = new jsPDF({ putOnlyUsedFonts: true, floatPrecision: 2 });
         doc.addMetadata("My metadata as a string.");
         comparePdf(doc.output(), "xmpmetadata-defaultNS.pdf");
       });
    +
    +  it("should support rawXml overload", () => {
    +    const doc = new jsPDF();
    +    const rawXml =
    +      '<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:custom="http://custom.ns/"><custom:data>Raw XML Data</custom:data></rdf:Description></rdf:RDF></x:xmpmeta>';
    +    doc.addMetadata(rawXml, true);
    +    comparePdf(doc.output(), "xmpmetadata-rawXml.pdf");
    +  });
    +
    +  it("should escape XML content when rawXml is not used", () => {
    +    const doc = new jsPDF();
    +    const metadataWithXml = 'Some metadata with <xml> & "special" characters';
    +    doc.addMetadata(metadataWithXml);
    +    comparePdf(doc.output(), "xmpmetadata-escaped.pdf");
    +  });
     });
    
  • types/index.d.ts+5 1 modified
    @@ -1340,7 +1340,11 @@ declare module "jspdf" {
         getFileFromVFS(filename: string): string;
     
         // jsPDF plugin: xmp_metadata
    -    addMetadata(metadata: string, namespaceuri?: string): jsPDF;
    +    addMetadata(metadata: string, namespaceUri?: string): jsPDF;
    +    /**
    +     * WARNING: Passing raw XML is potentially insecure! Always sanitize user input before passing it to this function!
    +     */
    +    addMetadata(metadata: string, rawXml?: boolean): jsPDF;
     
         Matrix(
           a: number,
    

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.