February 10, 2024·6 min read·engineeringperlsecuritycpanspreadsheets

Spreadsheet parsing crosses more trust boundaries than file uploads suggest

CPANSec's spreadsheet report connects format-string code execution, merged-cell memory exhaustion and XML entity expansion with the work required to release fixes.

A spreadsheet importer can execute attacker-controlled code before its caller reads a cell. The dangerous operation in Spreadsheet::ParseExcel was part of number formatting. A condition embedded in a workbook's format string reached Perl's string eval, which interpreted it as program text. Removing macros from an upload policy would not address that path, because the application introduced execution while parsing ordinary workbook data.

The CPANSec report published on 10 February brings this vulnerability together with two problems in Spreadsheet::ParseXLSX. One expands a merged-cell range into excessive memory use. The other lets XML processing load external entities. These are separate failures with separate fixes, even though an application may encounter all of them through the same spreadsheet import operation.

I value CPAN's long history of distributing reusable, documented modules. That history also makes the details of module maintenance consequential. A package that performs a useful task for years can become part of an upload service whose trust assumptions differ from those of its original callers.

Number formatting became program execution

Mandiant's disclosure identifies CVE-2023-7101 in Spreadsheet::ParseExcel 0.65. It specifically distinguishes Excel number format strings from printf format strings. The attack does not depend on a programmer passing a malicious format to an ordinary printing function. It depends on the library interpreting workbook-controlled formatting conditions through string evaluation.

The researcher's analysis follows that value through the implementation. A cell refers to a format, and the format refers to a format string. For a numeric or date cell, the formatting code passes that string into ExcelFmt. A bracketed conditional at the start of the string becomes part of the text supplied to eval.

The intended operation is a comparison used to choose a formatting section. The implementation asks the Perl interpreter to evaluate a constructed expression containing data from the document. That gives the document author influence over executable source. Checking that the cell's numeric value looks like a number does not establish that the separate formatting expression is safe.

This distinction explains why the vulnerability can affect a server that only intends to extract values. The researcher's examples trigger the vulnerable path during the call to parse. The application does not have to ask for a dangerous operation after receiving the workbook object. Its first parsing step has already interpreted the formatting metadata.

The same analysis traces an XLSX route into Spreadsheet::ParseExcel's formatting machinery. The XML workbook supplies format definitions and cell references, and Spreadsheet::ParseXLSX uses the shared formatting classes. Mandiant consequently includes dependent products and Spreadsheet::ParseXLSX among affected consumers. The file extension alone does not identify the implementation that eventually formats the cell.

The practical dependency question is therefore which version of the formatting library the importer actually loads. Updating an XLSX-facing component while leaving a vulnerable ParseExcel dependency available can leave the relevant execution path in place. CPANSec identifies Spreadsheet::ParseExcel 0.66 as the release containing Daniel Ruoso's fix.

A range description expanded into a large allocation

CVE-2024-22368 concerns a different operation. Spreadsheet::ParseXLSX 0.27 and earlier track merged cells in a way that can consume excessive memory for a crafted range. The researcher's separate analysis shows the relevant nested iteration.

The parser reads the top-left and bottom-right coordinates of a merged area. It then visits each row and each column in the resulting rectangle, creating an entry to mark each cell as merged. A short description of a rectangle can therefore require an amount of work proportional to its area. The input need not contain a separately encoded value for every location the parser records.

This is why a file-size limit does not directly bound the allocation involved. The coordinates are a compact description of work. A small document can describe a large rectangle, and the vulnerable parser expands that description into individual records. The researcher's proposed remedies distinguish restricting the range from changing the representation so that it does not precompute every membership entry.

Those approaches have different implications. A range limit constrains which documents the parser will accept. A representation change can preserve a description of the area without materialising a record for every coordinate. Either way, the implementation must account for the cost of what a range denotes, beyond the number of bytes used to write it.

The January fix for this issue arrived in Spreadsheet::ParseXLSX 0.28. CPANSec does not claim that attackers used this particular denial-of-service vulnerability against a production server. That uncertainty should remain separate from the demonstrated allocation problem. The mechanism is enough to explain why an importer accepting untrusted files needs the corrected parser.

XML processing could read outside the workbook

CVE-2024-23525 affects Spreadsheet::ParseXLSX 0.29 and earlier. An Pham's disclosure traces the parsing path into XML::Twig and identifies the missing no_xxe setting. The outer spreadsheet API did not require the caller to opt into external entity processing.

An XML entity can cause the XML processor to obtain content outside the document. In a server-side importer, that creates a dependency on resources available to the parsing process. The disclosure demonstrates local file content entering the parsed spreadsheet data. It also describes a denial-of-service example.

Reading a file during parsing and disclosing it to an attacker are distinct steps. The disclosure gives an application-level scenario in which imported values enter a database and a subsequent read function exposes them. A specific service's exposure depends on what it does with the parsed result. The parser's ability to incorporate outside data is the initial failure that makes that scenario possible.

The corrective decision belongs at the parser configuration boundary. The disclosure proposes enabling no_xxe by default, so ordinary spreadsheet parsing does not inherit that additional capability. It also discusses an explicit option for users who require entity processing. Such an option would make the caller responsible for choosing a broader interpretation of the document.

Spreadsheet::ParseXLSX 0.30 contains the XXE fix. Version 0.31 subsequently records the CVE reference. This means the release that fixes the merged-cell allocation issue is still within the affected range for the XML issue. A single statement that the spreadsheet library received a January security update is insufficient to identify which vulnerabilities an installed version addresses.

I find the XXE case particularly useful when assessing abstractions around file formats. A convenient parse method can invoke another parser with capabilities that the application never considered. The required restriction has to reach that lower-level parser, even when the application never calls it directly.

Releasing a patch required reachable authority

The CPANSec account also explains why a written patch is only part of a security response. Attempts to contact previous maintainers did not initially reach the people needed to release corrected modules. For ParseExcel, CPANSec contacted John McNamara, who retained the authority to publish a release. For ParseXLSX, Michael Daum took over maintenance and released the corrections.

That process matters to consumers because the ordinary dependency update path relies on a published distribution. A patch in an issue or a repository can describe a repair without making it available through the package version a deployment consumes. Maintainer permissions and contact information become part of the path between a report and an installable correction.

There is a useful distinction between quiet maintenance and unreachable maintenance. A module that has not needed frequent changes can still have a responsive owner. Publication age alone cannot establish that nobody is responsible for it. The spreadsheet response illustrates the more concrete question of whether someone can receive a report and exercise release authority when necessary.

The contrasting mechanisms also change what a useful regression case needs to establish. A formatting case concerns whether workbook text can become executable Perl. A merged-range case concerns how much state the parser creates from coordinates. An XML case concerns whether parsing can obtain data outside the supplied workbook. Checking only that a well-formed spreadsheet still imports cannot establish any of those properties. Each regression needs to exercise the operation whose interpretation crossed the intended boundary.

For an importing service, the corrected dependency set needs to cover both the ParseExcel formatting implementation and the ParseXLSX-specific parsing behaviour. The fixes address different operations, so neither the absence of macros nor a limit on upload bytes substitutes for them. Format metadata remains untrusted input, and the importer must preserve that boundary while interpreting it into values.