{"id":"GHSA-hrmw-qprp-wgmc","summary":"PhpSpreadsheet has XSS via number format code with @ text placeholder bypasses htmlspecialchars in HTML writer","details":"It was discovered that there is a way to bypass HTML escaping in the HTML writer using custom number format codes.\n\n## The Problem\n\nIn `Writer/Html.php` around line 1592, the code checks if the formatted cell data equals the original data to decide whether to apply `htmlspecialchars()`:\n\n```php\nif ($cellData === $origData) {\n    $cellData = htmlspecialchars($cellData, ...);\n}\n```\n\nWhen a cell has a custom number format containing `@` (text placeholder) with any additional literal characters, the formatter replaces `@` with the cell value and adds the extra characters. This makes `$cellData !== $origData`, so `htmlspecialchars()` is **skipped entirely**.\n\nEven a single trailing space in the format (`@ `) is enough to bypass the escape.\n\n## Proof of Concept\n\n```php\nuse PhpOffice\\PhpSpreadsheet\\Spreadsheet;\nuse PhpOffice\\PhpSpreadsheet\\Writer\\Html;\nuse PhpOffice\\PhpSpreadsheet\\Cell\\DataType;\n\n$spreadsheet = new Spreadsheet();\n$sheet = $spreadsheet-\u003egetActiveSheet();\n\n// XSS payload with malicious number format\n$sheet-\u003esetCellValueExplicit('A1', '\u003cimg src=x onerror=alert(document.cookie)\u003e', DataType::TYPE_STRING);\n$sheet-\u003egetStyle('A1')-\u003egetNumberFormat()-\u003esetFormatCode('. @');\n\n$writer = new Html($spreadsheet);\n$writer-\u003esave('output.html');\n```\n\nThe generated HTML contains:\n```html\n\u003ctd\u003e. \u003cimg src=x onerror=alert(document.cookie)\u003e\u003c/td\u003e\n```\n\nThe XSS payload is **completely unescaped**.\n\n## Tested Bypass Formats\n\n| Format Code | Result | Escaped? |\n|---|---|---|\n| `General` (default) | Original value | YES (safe) |\n| `. @` | `. ` + value | **NO (XSS!)** |\n| `@ ` (trailing space) | value + ` ` | **NO (XSS!)** |\n| `x@` | `x` + value | **NO (XSS!)** |\n\nThis was tested with PhpSpreadsheet 4.5.0 and confirmed the XSS executes in the browser.\n\n## Impact\n\nAny application that:\n1. Accepts uploaded XLSX files from users\n2. Converts them to HTML using PhpSpreadsheet's HTML writer\n3. Displays the HTML to other users\n\n...is vulnerable to stored XSS. The attacker embeds the payload in a cell value and sets a custom number format in the XLSX file's `xl/styles.xml`.\n\n## Suggested Fix\n\nAlways apply `htmlspecialchars()` regardless of whether formatting changed the value:\n\n```php\n// Instead of conditional escaping:\n$cellData = htmlspecialchars($cellData, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');\n```\n\nOr escape AFTER formatting, not conditionally based on equality.\n\n## Reporter\nKeyvan Hardani","aliases":["CVE-2026-40296"],"modified":"2026-05-08T19:50:57.048795Z","published":"2026-04-28T22:57:13Z","database_specific":{"nvd_published_at":"2026-05-06T22:16:25Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-04-28T22:57:13Z"},"references":[{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-hrmw-qprp-wgmc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40296"},{"type":"PACKAGE","url":"https://github.com/PHPOffice/PhpSpreadsheet"}],"affected":[{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"5.7.0"}]}],"versions":["4.0.0","4.1.0","4.2.0","4.3.0","4.3.1","4.4.0","4.5.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.5.0","5.6.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-hrmw-qprp-wgmc/GHSA-hrmw-qprp-wgmc.json","last_known_affected_version_range":"\u003c= 5.6.0"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.3.0"},{"fixed":"3.10.5"}]}],"versions":["3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.3.0","3.4.0","3.5.0","3.6.0","3.7.0","3.8.0","3.9.0","3.9.1","3.9.2","3.9.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.10.4","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-hrmw-qprp-wgmc/GHSA-hrmw-qprp-wgmc.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.2.0"},{"fixed":"2.4.5"}]}],"versions":["2.2.0","2.2.1","2.2.2","2.3.0","2.3.10","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.3.7","2.3.8","2.3.9","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.4.4","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-hrmw-qprp-wgmc/GHSA-hrmw-qprp-wgmc.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.1.16"}]}],"versions":["2.0.0","2.1.0","2.1.1","2.1.10","2.1.11","2.1.12","2.1.13","2.1.14","2.1.15","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.1.15","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-hrmw-qprp-wgmc/GHSA-hrmw-qprp-wgmc.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.30.4"}]}],"versions":["1.0.0","1.0.0-beta","1.0.0-beta2","1.1.0","1.10.0","1.10.1","1.11.0","1.12.0","1.13.0","1.14.0","1.14.1","1.15.0","1.16.0","1.17.0","1.17.1","1.18.0","1.19.0","1.2.0","1.2.1","1.20.0","1.21.0","1.22.0","1.23.0","1.24.0","1.24.1","1.25.0","1.25.1","1.25.2","1.26.0","1.27.0","1.27.1","1.28.0","1.29.0","1.29.1","1.29.10","1.29.11","1.29.12","1.29.2","1.29.4","1.29.5","1.29.6","1.29.7","1.29.8","1.29.9","1.3.0","1.3.1","1.30.0","1.30.1","1.30.2","1.30.3","1.4.0","1.4.1","1.5.0","1.5.1","1.5.2","1.6.0","1.7.0","1.8.0","1.8.1","1.8.2","1.9.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-hrmw-qprp-wgmc/GHSA-hrmw-qprp-wgmc.json","last_known_affected_version_range":"\u003c= 1.30.3"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"}]}