{"id":"CVE-2026-82865","aliases":["GHSA-xgx4-2wgv-4jhm"],"url":"https://o3.security/vulnerability/CVE-2026-82865","summary":"pdfme schemas before 5.5.10 Cross-Site Scripting via i18n Label","details":"## Summary\n\nThe multiVariableText property panel in `@pdfme/schemas` constructs HTML via string concatenation and assigns it to `innerHTML` using unsanitized i18n label values. An attacker who can control label overrides passed through `options.labels` can inject arbitrary JavaScript that executes in the context of any user who opens the Designer and selects a multiVariableText field with no `{variables}` in its text.\n\n## Details\n\nWhen a user selects a multiVariableText schema field that contains no `{variable}` placeholders, the property panel renders instructional text by concatenating i18n-translated strings directly into `innerHTML`.\n\n**Vulnerable sink** — `packages/schemas/src/multiVariableText/propPanel.ts:65-71`:\n\n```typescript\n// Use safe string concatenation for innerHTML\nconst typingInstructions = i18n('schemas.mvt.typingInstructions');\nconst sampleField = i18n('schemas.mvt.sampleField');\npara.innerHTML =\n  typingInstructions +\n  ` <code style=\"color:${safeColorValue}; font-weight:bold;\">{` +\n  sampleField +\n  '}</code>';\n```\n\nThe comment on line 64 claims \"safe string concatenation\" but the result is assigned to `innerHTML` with no HTML escaping applied to `typingInstructions` or `sampleField`.\n\n**i18n lookup has no escaping** — `packages/ui/src/i18n.ts:903`:\n\n```typescript\nexport const i18n = (key: keyof Dict, dict?: Dict) => (dict || getDict(DEFAULT_LANG))[key];\n```\n\nThis is a plain dictionary lookup — no HTML encoding or sanitization.\n\n**Label override via deep merge** — `packages/ui/src/components/AppContextProvider.tsx:57-63`:\n\n```typescript\nlet dict = getDict(lang);\nif (options.labels) {\n  dict = deepMerge(\n    dict as unknown as Record<string, unknown>,\n    options.labels as unknown as Record<string, unknown>,\n  ) as typeof dict;\n}\n```\n\nUser-supplied `options.labels` values are deep-merged into the i18n dictionary with no content sanitization. The Zod schema validates labels as `z.record(z.string(), z.string())` — enforcing type but not content safety.\n\n**Inconsistency:** The color value on lines 58-62 is explicitly validated with a regex allowlist, demonstrating security awareness. The i18n string values were simply overlooked.\n\n## PoC\n\n1. **Create a minimal app that passes attacker-controlled labels:**\n\n```html\n<html>\n<body>\n<div id=\"designer-container\" style=\"width:100%;height:700px;\"></div>\n<script type=\"module\">\nimport { Designer } from '@pdfme/ui';\nimport { multiVariableText } from '@pdfme/schemas';\n\nconst template = {\n  basePdf: { width: 210, height: 297, padding: [10, 10, 10, 10] },\n  schemas: [[{\n    type: 'multiVariableText',\n    name: 'field1',\n    text: 'plain text with no variables',\n    content: '{}',\n    variables: [],\n    position: { x: 20, y: 20 },\n    width: 100,\n    height: 20,\n    readOnly: true,\n  }]],\n};\n\nnew Designer({\n  domContainer: document.getElementById('designer-container'),\n  template,\n  plugins: { multiVariableText },\n  options: {\n    labels: {\n      'schemas.mvt.typingInstructions':\n        '<img src=x onerror=\"document.title=document.cookie\">Inject: ',\n      'schemas.mvt.sampleField': 'safe',\n    },\n  },\n});\n</script>\n</body>\n</html>\n```\n\n2. **Open the application in a browser.**\n\n3. **Click on the multiVariableText field** (`field1`) in the Designer canvas to select it.\n\n4. **Observe:** The property panel renders the injected HTML. The `onerror` handler executes, setting `document.title` to the page's cookies. In a real attack, this would exfiltrate session tokens to an attacker-controlled server.\n\n## Impact\n\n- **Session hijacking:** Attacker-injected JavaScript can steal authentication cookies and tokens from any user who opens the Designer.\n- **DOM manipulation:** The injected script runs in the application's origin, allowing phishing overlays, form hijacking, or data exfiltration.\n- **Stored XSS potential:** In multi-tenant applications where labels are stored in a database or fetched from an API, a single poisoned label entry affects all users who subsequently open the Designer.\n- **Scope change:** The XSS payload executes in the embedding application's browser context, escaping the pdfme component's security boundary.\n\n## Recommended Fix\n\nReplace `innerHTML` with safe DOM APIs in `packages/schemas/src/multiVariableText/propPanel.ts`:\n\n```typescript\n// BEFORE (vulnerable):\npara.innerHTML =\n  typingInstructions +\n  ` <code style=\"color:${safeColorValue}; font-weight:bold;\">{` +\n  sampleField +\n  '}</code>';\n\n// AFTER (safe):\npara.appendChild(document.createTextNode(typingInstructions + ' '));\nconst codeEl = document.createElement('code');\ncodeEl.style.color = safeColorValue;\ncodeEl.style.fontWeight = 'bold';\ncodeEl.textContent = `{${sampleField}}`;\npara.appendChild(codeEl);\n```\n\nThis ensures that i18n label values are always treated as text content, never parsed as HTML, regardless of their source.","published":"2026-08-31T08:46:35.945Z","modified":"2026-10-28T02:31:12.286679340Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"@pdfme/schemas","fixedVersion":"5.5.10"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/82xxx/CVE-2026-82865.json"},{"type":"ADVISORY","url":"https://github.com/pdfme/pdfme/security/advisories/GHSA-xgx4-2wgv-4jhm"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-82865"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/pdfme-schemas-before-5.5.10-cross-site-scripting-via-i18n-label"},{"type":"PACKAGE","url":"https://github.com/pdfme/pdfme"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:31:12.286679340Z"}}