| Affected_by_vulnerabilities |
| 0 |
| url |
VCID-kxsk-fpuj-5uaw |
| vulnerability_id |
VCID-kxsk-fpuj-5uaw |
| summary |
PDFME has XSS via Unsanitized i18n Label Injection into innerHTML in multiVariableText propPanel
## 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 src=x onerror="document.title=document.cookie">Inject: ',
'schemas.mvt.sampleField': 'safe',
},
},
});
</script>
</body>
</html>
```
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 +
` <code style="color:${safeColorValue}; font-weight:bold;">{` +
sampleField +
'}</code>';
// 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. |
| references |
|
| fixed_packages |
|
| aliases |
GHSA-xgx4-2wgv-4jhm
|
| risk_score |
null |
| exploitability |
null |
| weighted_severity |
null |
| resource_url |
http://public2.vulnerablecode.io/vulnerabilities/VCID-kxsk-fpuj-5uaw |
|
| 1 |
| url |
VCID-p7h9-8dum-cybm |
| vulnerability_id |
VCID-p7h9-8dum-cybm |
| summary |
Cross-Site Scripting (XSS) via Select Schema Option Value Injection in @pdfme/schemas
## Summary
The Select schema plugin in `@pdfme/schemas` constructs HTML from template-defined option values using unsanitized string interpolation and sets it via `innerHTML`, enabling arbitrary JavaScript execution.
## Details
In `packages/schemas/src/select/index.ts`, lines 159-164, the Select schema's `ui` renderer builds `<option>` elements by directly interpolating option values from the template into an HTML string:
```typescript
const options = Array.isArray(schema.options) ? schema.options : [];
selectElement.innerHTML = options
.map(
(option) =>
`<option value="${option}" ${option === value ? 'selected' : ''}>${option}</option>`,
)
.join('');
```
The `option` values come from `schema.options`, which is an array of strings defined in the template JSON. These values are interpolated directly into the HTML string without any escaping of `<`, `>`, `"`, `&`, or other HTML-special characters. An option value containing `">` breaks out of the `value` attribute and allows injection of arbitrary HTML elements and event handlers.
## Proof of Concept
Loading the following template into a pdfme Form or Designer component triggers JavaScript execution:
```json
{
"basePdf": { "width": 210, "height": 297, "padding": [20, 20, 20, 20] },
"schemas": [[
{
"name": "malicious_select",
"type": "select",
"content": "Normal",
"options": [
"Normal",
"\"></option><img src=x onerror=\"alert(document.domain)\">"
],
"position": { "x": 20, "y": 20 },
"width": 80,
"height": 10
}
]]
}
```
The injected `<img onerror>` element executes JavaScript because it is parsed as HTML when assigned to `selectElement.innerHTML`.
## Attack Vectors
The `options` array is defined in the template (not by form-filling end users). The attack requires a malicious template to be loaded, which can happen via:
1. File upload (e.g., "Load Template" functionality in applications)
2. Shared/imported templates in multi-tenant applications
3. Templates stored in databases without content sanitization
4. The `updateTemplate()` API being called with untrusted data
This vulnerability is triggered in Form mode (for non-readOnly select fields) and Designer mode when the select element is rendered.
## Impact
An attacker who can supply a malicious template can execute arbitrary JavaScript in the browser of any user who views or interacts with the template. This enables:
- Session hijacking via cookie/token theft
- Keylogging of form input data
- Phishing and page modification
- Data exfiltration
## Suggested Fix
Use DOM APIs to create option elements safely instead of string interpolation:
```typescript
options.forEach((option) => {
const optionEl = document.createElement('option');
optionEl.value = option;
optionEl.textContent = option;
if (option === value) optionEl.selected = true;
selectElement.appendChild(optionEl);
});
```
Alternatively, HTML-encode option values before interpolation:
```typescript
const escape = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
``` |
| references |
|
| fixed_packages |
|
| aliases |
GHSA-qq9g-96v4-m3cj
|
| risk_score |
null |
| exploitability |
null |
| weighted_severity |
null |
| resource_url |
http://public2.vulnerablecode.io/vulnerabilities/VCID-p7h9-8dum-cybm |
|
| 2 |
| url |
VCID-r9pa-fc4s-p3a2 |
| vulnerability_id |
VCID-r9pa-fc4s-p3a2 |
| summary |
Cross-Site Scripting (XSS) via SVG Schema innerHTML Injection in @pdfme/schemas
## Summary
The SVG schema plugin in `@pdfme/schemas` renders user-supplied SVG content using `container.innerHTML = value` without any sanitization, enabling arbitrary JavaScript execution in the user's browser.
## Details
In `packages/schemas/src/graphics/svg.ts`, line 87, the SVG schema's `ui` renderer assigns raw SVG markup directly to `innerHTML` when in viewer mode or form mode with `readOnly: true`:
```typescript
// svg.ts, line 81-94 (non-editable rendering path)
} else {
if (!value) return;
if (!isValidSVG(value)) {
rootElement.appendChild(createErrorElm());
return;
}
container.innerHTML = value; // <-- VULNERABLE: unsanitized SVG injected into DOM
const svgElement = container.childNodes[0];
if (svgElement instanceof SVGElement) {
svgElement.setAttribute('width', '100%');
svgElement.setAttribute('height', '100%');
rootElement.appendChild(container);
}
}
```
The `isValidSVG()` function (lines 11-37) only validates that the string contains `<svg` and `</svg>` tags and passes `DOMParser` well-formedness checks. It does NOT strip or block:
- `<script>` tags embedded in SVG
- Event handler attributes (`onload`, `onerror`, `onclick`, etc.)
- `<foreignObject>` elements containing HTML with event handlers
- `<animate>` / `<set>` elements with `onbegin` / `onend` handlers
- SVG `<use>` elements referencing malicious external resources
All of these are valid SVG and pass `isValidSVG()`, but execute JavaScript when inserted via `innerHTML`.
## Attack Vectors
### 1. Malicious Template (readOnly SVG schema)
An attacker crafts a template JSON with a readOnly SVG schema containing a malicious `content` value. When loaded into the pdfme Form or Viewer component, the SVG executes JavaScript.
### 2. Application-Supplied Inputs + Viewer
If an application uses the pdfme Viewer component and passes user-controlled data as inputs for a non-readOnly SVG schema, the attacker's SVG flows directly to `innerHTML`.
## Proof of Concept
Loading the following template into a pdfme Form or Viewer component triggers JavaScript execution:
```json
{
"basePdf": { "width": 210, "height": 297, "padding": [20, 20, 20, 20] },
"schemas": [[
{
"name": "malicious_svg",
"type": "svg",
"content": "<svg xmlns='http://www.w3.org/2000/svg' onload='alert(document.domain)'><rect width='100' height='100' fill='red'/></svg>",
"readOnly": true,
"position": { "x": 20, "y": 20 },
"width": 80,
"height": 40
}
]]
}
```
Additional payloads that bypass `isValidSVG()` and execute JavaScript:
```svg
<!-- Via foreignObject -->
<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="200" height="60"><body xmlns="http://www.w3.org/1999/xhtml"><img src="x" onerror="alert(1)"/></body></foreignObject></svg>
<!-- Via animate onbegin -->
<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"><animate attributeName="x" values="0" dur="0.001s" onbegin="alert(1)"/></rect></svg>
```
## Impact
An attacker who can supply a malicious template (via file upload, shared template URL, multi-tenant template storage, or `updateTemplate()` API) can execute arbitrary JavaScript in the context of any user who views or fills the template. This enables:
- Session hijacking via cookie/token theft
- Keylogging of form inputs (including sensitive data being entered into PDF forms)
- Phishing attacks by modifying the rendered page
- Data exfiltration from the application
The attack is particularly concerning for multi-tenant SaaS applications using pdfme where templates may be user-supplied.
## Suggested Fix
Sanitize SVG content before DOM insertion using DOMPurify or a similar library:
```typescript
import DOMPurify from 'dompurify';
// Replace line 87:
container.innerHTML = DOMPurify.sanitize(value, { USE_PROFILES: { svg: true } });
```
Alternatively, parse the SVG via `DOMParser`, strip all script elements and event handler attributes, then append the sanitized DOM nodes. |
| references |
|
| fixed_packages |
|
| aliases |
GHSA-87v3-4cfp-cm76
|
| risk_score |
null |
| exploitability |
null |
| weighted_severity |
null |
| resource_url |
http://public2.vulnerablecode.io/vulnerabilities/VCID-r9pa-fc4s-p3a2 |
|
|