GHSA-hrmw-qprp-wgmc — phpoffice/phpspreadsheet
MEDIUMGHSA-hrmw-qprp-wgmc is a medium-severity (CVSS 5.4) Cross-site Scripting (XSS) vulnerability in phpoffice/phpspreadsheet. A fix is available for phpoffice/phpspreadsheet — see the affected versions and patch details below.
PhpSpreadsheet has XSS via number format code with @ text placeholder bypasses htmlspecialchars in HTML writer
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-hrmw-qprp-wgmc.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-hrmw-qprp-wgmc plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
phpoffice/phpspreadsheet🐘phpoffice/phpspreadsheet🐘phpoffice/phpspreadsheet🐘phpoffice/phpspreadsheet🐘phpoffice/phpspreadsheetReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
It was discovered that there is a way to bypass HTML escaping in the HTML writer using custom number format codes.
The Problem
In Writer/Html.php around line 1592, the code checks if the formatted cell data equals the original data to decide whether to apply htmlspecialchars():
if ($cellData === $origData) {
$cellData = htmlspecialchars($cellData, ...);
}
When 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.
Even a single trailing space in the format (@ ) is enough to bypass the escape.
Proof of Concept
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// XSS payload with malicious number format
$sheet->setCellValueExplicit('A1', '<img src=x onerror=alert(document.cookie)>', DataType::TYPE_STRING);
$sheet->getStyle('A1')->getNumberFormat()->setFormatCode('. @');
$writer = new Html($spreadsheet);
$writer->save('output.html');
The generated HTML contains:
<td>. <img src=x onerror=alert(document.cookie)></td>
The XSS payload is completely unescaped.
Tested Bypass Formats
| Format Code | Result | Escaped? |
|---|---|---|
General (default) | Original value | YES (safe) |
. @ | . + value | NO (XSS!) |
@ (trailing space) | value + | NO (XSS!) |
x@ | x + value | NO (XSS!) |
This was tested with PhpSpreadsheet 4.5.0 and confirmed the XSS executes in the browser.
Impact
Any application that:
- Accepts uploaded XLSX files from users
- Converts them to HTML using PhpSpreadsheet's HTML writer
- Displays the HTML to other users
...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.
Suggested Fix
Always apply htmlspecialchars() regardless of whether formatting changed the value:
// Instead of conditional escaping:
$cellData = htmlspecialchars($cellData, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Or escape AFTER formatting, not conditionally based on equality.
Reporter
Keyvan Hardani
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | phpoffice/phpspreadsheet | ≥ 4.0.0&&< 5.7.0 | 5.7.0composer require phpoffice/phpspreadsheet:^5.7.0 |
| 🐘Packagist | phpoffice/phpspreadsheet | ≥ 3.3.0&&< 3.10.5 | 3.10.5composer require phpoffice/phpspreadsheet:^3.10.5 |
| 🐘Packagist | phpoffice/phpspreadsheet | ≥ 2.2.0&&< 2.4.5 | 2.4.5composer require phpoffice/phpspreadsheet:^2.4.5 |
| 🐘Packagist | phpoffice/phpspreadsheet | ≥ 2.0.0&&< 2.1.16 | 2.1.16composer require phpoffice/phpspreadsheet:^2.1.16 |
| 🐘Packagist | phpoffice/phpspreadsheet | all versions | 1.30.4composer require phpoffice/phpspreadsheet:^1.30.4 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for phpoffice/phpspreadsheet, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update phpoffice/phpspreadsheet to 5.7.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-hrmw-qprp-wgmc is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-hrmw-qprp-wgmc can be triaged on real exposure rather than presence alone.
Tailored to GHSA-hrmw-qprp-wgmc. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-hrmw-qprp-wgmc in your dependencies?
O3 Security finds GHSA-hrmw-qprp-wgmc across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.