Medium severity6.1NVD Advisory· Published Jul 10, 2025· Updated Apr 15, 2026
CVE-2025-53626
CVE-2025-53626
Description
pdfme is a TypeScript-based PDF generator and React-based UI. The expression evaluation feature in pdfme 5.2.0 to 5.4.0 contains critical vulnerabilities allowing sandbox escape leading to XSS and prototype pollution attacks. This vulnerability is fixed in 5.4.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@pdfme/commonnpm | >= 5.2.0, < 5.4.1 | 5.4.1 |
Patches
10dd54739acffXSS vulnerability prevention for replacePlaceholders function (#1117)
2 files changed · +229 −1
packages/common/src/expression.ts+53 −1 modified@@ -37,13 +37,57 @@ const formatDate = (date: Date): string => const formatDateTime = (date: Date): string => `${formatDate(date)} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`; +// Safe assign function that prevents prototype pollution +const safeAssign = ( + target: Record<string, unknown>, + ...sources: Array<Record<string, unknown> | null | undefined> +): Record<string, unknown> => { + if (target == null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = { ...target }; + + for (const source of sources) { + if (source != null) { + for (const key in source) { + // Skip prototype pollution keys + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; + } + // Only copy own properties + if (Object.prototype.hasOwnProperty.call(source, key)) { + to[key] = source[key]; + } + } + } + } + + return to; +}; + +// Create a safe copy of Object with dangerous methods excluded +const safeObject = { + keys: Object.keys, + values: Object.values, + entries: Object.entries, + fromEntries: Object.fromEntries, + is: Object.is, + hasOwnProperty: Object.hasOwnProperty, + assign: safeAssign, // Safe version of Object.assign + // The following methods are excluded due to security concerns: + // - Side effects: create, freeze, seal (can still be used for attacks) + // - Prototype access: getOwnPropertyDescriptor, getPrototypeOf, setPrototypeOf, + // defineProperty, defineProperties, getOwnPropertyNames, getOwnPropertySymbols +}; + const allowedGlobals: Record<string, unknown> = { Math, String, Number, Boolean, Array, - Object, + Object: safeObject, Date, JSON, isNaN, @@ -89,6 +133,10 @@ const validateAST = (node: AcornNode): void => { if (['constructor', '__proto__', 'prototype'].includes(propName)) { throw new Error('Access to prohibited property'); } + // Block prototype pollution methods + if (['__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__'].includes(propName)) { + throw new Error(`Access to prohibited method: ${propName}`); + } const prohibitedMethods = ['toLocaleString', 'valueOf']; if (typeof propName === 'string' && prohibitedMethods.includes(propName)) { throw new Error(`Access to prohibited method: ${propName}`); @@ -234,6 +282,10 @@ const evaluateAST = (node: AcornNode, context: Record<string, unknown>): unknown if (typeof prop === 'string' && ['constructor', '__proto__', 'prototype'].includes(prop)) { throw new Error('Access to prohibited property'); } + // Block prototype pollution methods + if (typeof prop === 'string' && ['__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__'].includes(prop)) { + throw new Error(`Access to prohibited method: ${prop}`); + } return obj[prop]; } else { throw new Error('Invalid property access');
packages/common/__tests__/expression.test.ts+176 −0 modified@@ -369,4 +369,180 @@ describe('replacePlaceholders - Comparison Operators Tests', () => { // Execution of arbitrary functions is not allowed; placeholder remains unchanged expect(result).toBe('ArbitraryCode: {1 < (() => { return "Hacked" })()}'); }); +}); + +describe('replacePlaceholders - XSS Vulnerability Prevention Tests', () => { + it('should prevent XSS via Object.getOwnPropertyDescriptor and Object.getPrototypeOf (CVE payload 1)', () => { + const content = '{ ((f, g) => f(g(Object), "constructor").value)(Object.getOwnPropertyDescriptor, Object.getPrototypeOf)("alert(location)")() }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // The dangerous expression should not be evaluated and should return as-is + expect(result).toBe(content); + }); + + it('should prevent XSS via object property assignment (CVE payload 2)', () => { + const content = '{ { f: Object.getOwnPropertyDescriptor }.f({ g: Object.getPrototypeOf }.g(Object), "constructor").value("alert(location)")() }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // The dangerous expression should not be evaluated and should return as-is + expect(result).toBe(content); + }); + + it('should prevent direct access to Object.getOwnPropertyDescriptor', () => { + const content = '{ Object.getOwnPropertyDescriptor(Object, "constructor") }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent direct access to Object.getPrototypeOf', () => { + const content = '{ Object.getPrototypeOf(Object) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent access to Object.setPrototypeOf', () => { + const content = '{ Object.setPrototypeOf({}, null) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent access to Object.defineProperty', () => { + const content = '{ Object.defineProperty({}, "prop", { value: 42 }) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent access to Object.defineProperties', () => { + const content = '{ Object.defineProperties({}, { prop: { value: 42 } }) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent access to Object.getOwnPropertyNames', () => { + const content = '{ Object.getOwnPropertyNames(Object) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should prevent access to Object.getOwnPropertySymbols', () => { + const content = '{ Object.getOwnPropertySymbols(Object) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Direct access to dangerous method should be blocked + expect(result).toBe(content); + }); + + it('should allow safe Object methods', () => { + // Test Object.keys + const keysContent = '{ Object.keys({ a: 1, b: 2 }) }'; + const keysResult = replacePlaceholders({ content: keysContent, variables: {}, schemas: [] }); + expect(keysResult).toBe('a,b'); + + // Test Object.values + const valuesContent = '{ Object.values({ a: 1, b: 2 }) }'; + const valuesResult = replacePlaceholders({ content: valuesContent, variables: {}, schemas: [] }); + expect(valuesResult).toBe('1,2'); + + // Test Object.entries + const entriesContent = '{ Object.entries({ a: 1 })[0] }'; + const entriesResult = replacePlaceholders({ content: entriesContent, variables: {}, schemas: [] }); + expect(entriesResult).toBe('a,1'); + + // Test safe Object.assign + const assignContent = '{ Object.assign({}, { a: 1 }, { b: 2 }).a }'; + const assignResult = replacePlaceholders({ content: assignContent, variables: {}, schemas: [] }); + expect(assignResult).toBe('1'); // Safe assign should work + }); + + it('should prevent complex XSS attempts via nested function calls', () => { + const content = '{ [].map.call("abc", Object.getOwnPropertyDescriptor) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Complex attempts to access dangerous functions should be blocked + expect(result).toBe(content); + }); + + it('should prevent Function constructor access via constructor property', () => { + const content = '{ "".constructor.constructor("alert(1)")() }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Attempts to access Function constructor should be blocked + expect(result).toBe(content); + }); + + it('should prevent prototype pollution via Object.assign and __lookupGetter__', () => { + const content = '{ { assign: Object.assign }.assign({ f: {}.__lookupGetter__("__proto__") }.f(), { polluted: "yes" }) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // The dangerous expression should not be evaluated due to __lookupGetter__ being blocked + expect(result).toBe(content); + // Verify that prototype is not polluted + expect(({} as any).polluted).toBeUndefined(); + }); + + it('should prevent access to __lookupGetter__', () => { + const content = '{ {}.__lookupGetter__("__proto__") }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + expect(result).toBe(content); + }); + + it('should prevent access to __lookupSetter__', () => { + const content = '{ {}.__lookupSetter__("__proto__") }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + expect(result).toBe(content); + }); + + it('should prevent access to __defineGetter__', () => { + const content = '{ {}.__defineGetter__("test", () => "hacked") }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + expect(result).toBe(content); + }); + + it('should prevent access to __defineSetter__', () => { + const content = '{ {}.__defineSetter__("test", () => {}) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + expect(result).toBe(content); + }); + + it('should allow safe Object.assign but prevent prototype pollution', () => { + const content = '{ Object.assign({}, { a: 1 }) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Safe assign should work + expect(result).toBe('[object Object]'); + }); + + it('should prevent prototype pollution via Object.assign', () => { + const pollutionContent = '{ Object.assign({}, { "__proto__": { polluted: "yes" } }) }'; + const result = replacePlaceholders({ content: pollutionContent, variables: {}, schemas: [] }); + // Should execute but not pollute prototype + expect(result).toBe('[object Object]'); + expect(({} as any).polluted).toBeUndefined(); + + // Test with constructor + const constructorContent = '{ Object.assign({}, { "constructor": { polluted: "yes" } }) }'; + const result2 = replacePlaceholders({ content: constructorContent, variables: {}, schemas: [] }); + expect(result2).toBe('[object Object]'); + expect(({} as any).constructor.polluted).toBeUndefined(); + }); + + it('should no longer allow Object.create due to security concerns', () => { + const content = '{ Object.create(null) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Object.create is now blocked + expect(result).toBe(content); + }); + + it('should no longer allow Object.freeze due to security concerns', () => { + const content = '{ Object.freeze({}) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Object.freeze is now blocked + expect(result).toBe(content); + }); + + it('should no longer allow Object.seal due to security concerns', () => { + const content = '{ Object.seal({}) }'; + const result = replacePlaceholders({ content, variables: {}, schemas: [] }); + // Object.seal is now blocked + expect(result).toBe(content); + }); }); \ No newline at end of file
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
6- github.com/advisories/GHSA-54xv-94qv-2gfgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-53626ghsaADVISORY
- github.com/pdfme/pdfme/commit/0dd54739acff2c249ed68c001a896bee38f0fd85nvdWEB
- github.com/pdfme/pdfme/pull/1117ghsaWEB
- github.com/pdfme/pdfme/releases/tag/5.4.1ghsaWEB
- github.com/pdfme/pdfme/security/advisories/GHSA-54xv-94qv-2gfgnvdWEB
News mentions
0No linked articles in our index yet.