GHSA-QQ9G-96V4-M3CJ
Vulnerability from github – Published: 2026-03-18 16:10 – Updated: 2026-03-18 16:10Summary
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:
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:
{
"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:
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:
const escape = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.5.8"
},
"package": {
"ecosystem": "npm",
"name": "@pdfme/schemas"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.5.9"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-18T16:10:16Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nThe 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.\n\n## Details\n\nIn `packages/schemas/src/select/index.ts`, lines 159-164, the Select schema\u0027s `ui` renderer builds `\u003coption\u003e` elements by directly interpolating option values from the template into an HTML string:\n\n```typescript\nconst options = Array.isArray(schema.options) ? schema.options : [];\nselectElement.innerHTML = options\n .map(\n (option) =\u003e\n `\u003coption value=\"${option}\" ${option === value ? \u0027selected\u0027 : \u0027\u0027}\u003e${option}\u003c/option\u003e`,\n )\n .join(\u0027\u0027);\n```\n\nThe `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 `\u003c`, `\u003e`, `\"`, `\u0026`, or other HTML-special characters. An option value containing `\"\u003e` breaks out of the `value` attribute and allows injection of arbitrary HTML elements and event handlers.\n\n## Proof of Concept\n\nLoading the following template into a pdfme Form or Designer component triggers JavaScript execution:\n\n```json\n{\n \"basePdf\": { \"width\": 210, \"height\": 297, \"padding\": [20, 20, 20, 20] },\n \"schemas\": [[\n {\n \"name\": \"malicious_select\",\n \"type\": \"select\",\n \"content\": \"Normal\",\n \"options\": [\n \"Normal\",\n \"\\\"\u003e\u003c/option\u003e\u003cimg src=x onerror=\\\"alert(document.domain)\\\"\u003e\"\n ],\n \"position\": { \"x\": 20, \"y\": 20 },\n \"width\": 80,\n \"height\": 10\n }\n ]]\n}\n```\n\nThe injected `\u003cimg onerror\u003e` element executes JavaScript because it is parsed as HTML when assigned to `selectElement.innerHTML`.\n\n## Attack Vectors\n\nThe `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:\n1. File upload (e.g., \"Load Template\" functionality in applications)\n2. Shared/imported templates in multi-tenant applications\n3. Templates stored in databases without content sanitization\n4. The `updateTemplate()` API being called with untrusted data\n\nThis vulnerability is triggered in Form mode (for non-readOnly select fields) and Designer mode when the select element is rendered.\n\n## Impact\n\nAn 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:\n- Session hijacking via cookie/token theft\n- Keylogging of form input data\n- Phishing and page modification\n- Data exfiltration\n\n## Suggested Fix\n\nUse DOM APIs to create option elements safely instead of string interpolation:\n\n```typescript\noptions.forEach((option) =\u003e {\n const optionEl = document.createElement(\u0027option\u0027);\n optionEl.value = option;\n optionEl.textContent = option;\n if (option === value) optionEl.selected = true;\n selectElement.appendChild(optionEl);\n});\n```\n\nAlternatively, HTML-encode option values before interpolation:\n```typescript\nconst escape = (s) =\u003e s.replace(/\u0026/g, \u0027\u0026amp;\u0027).replace(/\"/g, \u0027\u0026quot;\u0027).replace(/\u003c/g, \u0027\u0026lt;\u0027).replace(/\u003e/g, \u0027\u0026gt;\u0027);\n```",
"id": "GHSA-qq9g-96v4-m3cj",
"modified": "2026-03-18T16:10:16Z",
"published": "2026-03-18T16:10:16Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pdfme/pdfme/security/advisories/GHSA-qq9g-96v4-m3cj"
},
{
"type": "PACKAGE",
"url": "https://github.com/pdfme/pdfme"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Cross-Site Scripting (XSS) via Select Schema Option Value Injection in @pdfme/schemas"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.