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.
| Package | Affected versions | Patched versions |
|---|---|---|
jspdfnpm | < 4.1.0 | 4.1.0 |
Affected products
1Patches
1efe54bf50f3fMerge commit from fork
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, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} - /** - * 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 addedtest/reference/xmpmetadata-rawXml.pdf+0 −0 addedtest/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- github.com/advisories/GHSA-vm32-vv63-w422ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24043ghsaADVISORY
- github.com/parallax/jsPDF/commit/efe54bf50f3f5e5416b2495e3c24624fc80b6cffghsax_refsource_MISCWEB
- github.com/parallax/jsPDF/releases/tag/v4.1.0ghsax_refsource_MISCWEB
- github.com/parallax/jsPDF/security/advisories/GHSA-vm32-vv63-w422ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.