GitHub Advisoryadvisory · npm
GHSA-xgx4-2wgv-4jhm PDFME has XSS via Unsanitized i18n Label Injection into innerHTML in multiVariableText propPanel
MEDIUM
Official page ↗
- Ecosystem
- npm
- Packages
- @pdfme/schemas
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:N
- Weaknesses
- CWE-79
- Reviewed
- 2026-03-20
Affected packages
• npm @pdfme/schemas: < 5.5.10 → fixed in 5.5.10
Description
Summary
The 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.
Details
When 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`.
Vulnerable sink — `packages/schemas/src/multiVariableText/propPanel.ts:65-71`:
```typescript
// Use safe string concatenation for innerHTML
const typingInstructions = i18n('schemas.mvt.typingInstructions');
const sampleField = i18n('schemas.mvt.sampleField');
para.innerHTML =
typingInstructions +
` {` +
sampleField +
'}';
```
The comment on line 64 claims "safe string concatenation" but the result is assigned to `innerHTML` with no HTML escaping applied to `typingInstructions` or `sampleField`.
i18n lookup has no escaping — `packages/ui/src/i18n.ts:903`:
```typescript
export const i18n = (key: keyof Dict, dict?: Dict) => (dict || getDict(DEFAULT_LANG))[key];
```
This is a plain dictionary lookup — no HTML encoding or sanitization.
Label override via deep merge — `packages/ui/src/components/AppContextProvider.tsx:57-63`:
```typescript
let dict = getDict(lang);
if (options.labels) {
dict = deepMerge(
dict as unknown as Record,
options.labels as unknown as Record,
) as typeof dict;
}
```
User-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.
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.
PoC
1. Create a minimal app that passes attacker-controlled labels:
```html
import { Designer } from '@pdfme/ui';
import { multiVariableText } from '@pdfme/schemas';
const template = {
basePdf: { width: 210, height: 297, padding: [10, 10, 10, 10] },
schemas: [[{
type: 'multiVariableText',
name: 'field1',
text: 'plain text with no variables',
content: '{}',
variables: [],
position: { x: 20, y: 20 },
width: 100,
height: 20,
readOnly: true,
}]],
};
new Designer({
domContainer: document.getElementById('designer-container'),
template,
plugins: { multiVariableText },
options: {
labels: {
'schemas.mvt.typingInstructions':
'Inject: ',
'schemas.mvt.sampleField': 'safe',
},
},
});
```
2. Open the application in a browser.
3. Click on the multiVariableText field (`field1`) in the Designer canvas to select it.
4. 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.
Impact
• Session hijacking: Attacker-injected JavaScript can steal authentication cookies and tokens from any user who opens the Designer.
• DOM manipulation: The injected script runs in the application's origin, allowing phishing overlays, form hijacking, or data exfiltration.
• 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.
• Scope change: The XSS payload executes in the embedding application's browser context, escaping the pdfme component's security boundary.
Recommended Fix
Replace `innerHTML` with safe DOM APIs in `packages/schemas/src/multiVariableText/propPanel.ts`:
```typescript
// BEFORE (vulnerable):
para.innerHTML =
typingInstructions +
` {` +
sampleField +
'}';
// AFTER (safe):
para.appendChild(document.createTextNode(typingInstructions + ' '));
const codeEl = document.createElement('code');
codeEl.style.color = safeColorValue;
codeEl.style.fontWeight = 'bold';
codeEl.textContent = `{${sampleField}}`;
para.appendChild(codeEl);
```
This ensures that i18n label values are always treated as text content, never parsed as HTML, regardless of their source.
Related
Weakness · CWE (1)
- CWE-79 Improper Neutralization of Input During Web Page Generation weakness