GHSA-q5j5-6p94-4gwc: Go/github.com/xuri/excelize vulnerability
Streaming GetRows row-bound bypass causes attacker-controlled allocation
Summary
Excelize's prior row-bound fix for GHSA-h69g / CVE-2026-54063 protects the checked worksheet parser, but the streaming worksheet reader used by Rows and GetRows does not enforce the same TotalRows bound on the row r attribute. A small XLSX file can set a row number above Excelize's maximum row (1048576) and omit the cell coordinate. GetRows then appends empty rows up to the attacker-controlled row index and returns success.
This was reproduced on the current default branch commit 1213a8bd7c5ab360554603ac5c995ccaf6eb4314 and the latest release tag v2.10.1 (5ad5ab3af0054c55bdce09f1530085600e9f2e45).
Affected package
- Package: github.com/xuri/excelize/v2 - Tested affected versions: current default branch at 1213a8bd7c5ab360554603ac5c995ccaf6eb4314, and release v2.10.1 - Fixed version: none known at the time of this report
Impact
An attacker who can provide an XLSX file to an application that calls GetRows can cause memory and CPU usage to scale with an attacker-controlled row number, even though the file itself is tiny. This is an availability issue and appears to be an incomplete coverage variant of the GHSA-h69g row-index allocation class.
In the conservative PoC, row r="2000000" returned a [][]string with length 2,000,000 and allocated about 46 MB. Larger row numbers scale the allocation further.
Root cause
The checked parser path validates row numbers:
- excelize.go: checkRowNum(r int) rejects negative rows and rows greater than TotalRows. - excelize.go: checkSheet() calls checkRowNum(r.R) before allocating sheet rows. - workSheetReader() invokes checkSheet() / checkRow() before returning a cached worksheet.
The streaming path does not use that checked parser:
- rows.go: Rows(sheet) opens an XML decoder directly. - Rows.Next() accepts the row r attribute and assigns it to the iterator's current row without applying checkRowNum(). - Rows.Columns() also assigns row r to the iterator state without applying checkRowNum(). - GetRows() appends empty row slices for the gap between the previous row and the current row.
Because a cell without an r coordinate can still contain a value, the worksheet can avoid cell-coordinate row validation while still causing GetRows() to materialize rows up to the out-of-range row number.
Minimal worksheet payload
xml <?xml version="1.0" encoding="UTF-8"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <sheetData> <row r="2000000"><c t="s"><v>0</v></c></row> </sheetData> </worksheet>
The workbook also contains a normal sharedStrings.xml with one string (ok).
Reproduction
A minimal Go harness creates the XLSX in memory and calls GetRows("Sheet1"):
go rows, err := f.GetRows("Sheet1") fmt.Println("rowslen:", len(rows)) if len(rows) > 0 { fmt.Println("lastrow:", rows[len(rows)-1]) } fmt.Printf("returned error: %T %v\n", err, err)
Observed output on current default branch commit 1213a8bd7c5ab360554603ac5c995ccaf6eb4314:
text == streaming GetRows row r=2000000 cell without r == rowslen: 2000000 lastrow: [ok] returned error: <nil> <nil> elapsed=21ms allocdelta=46MB
Observed output on latest release tag v2.10.1:
text == streaming GetRows row r=2000000 cell without r == rowslen: 2000000 lastrow: [ok] returned error: <nil> <nil> elapsed=14ms allocdelta=46MB
A control using the checked parser with row r="1048577" and c r="A1048577" correctly returns row number exceeds maximum limit, confirming this report is about inconsistent enforcement in the streaming path rather than a missing global constant.
Expected behavior
Rows / GetRows should reject row numbers greater than TotalRows with the same error behavior as the checked parser path.
Suggested remediation
- Apply the same row-bound validation in the streaming reader immediately after parsing a row r attribute. - Preserve and return row parsing errors from GetRows() instead of silently continuing or returning only Rows.Close() errors. - Add regression tests for GetRows() on a worksheet containing row r="1048577" with a cell value but no cell r coordinate.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/xuri/excelize/v2to a version that resolves this vulnerability.Fixed in 2.11.0 - Upgrade
Upgrade
github.com/xuri/excelize/v2to a version that resolves this vulnerability.Fixed in v2.10.1 - Configuration
In the streaming worksheet reader used by `Rows`/`GetRows`, enforce the same `TotalRows` bound as `checkRowNum(r int)` does in the checked parser path: after parsing each `<row r="...">`, call `checkRowNum(parsedR)` before accepting/assigning the iterator current row or appending empty rows up to that index.
excelize streaming reader (Rows/GetRows path) row r attribute validation via checkRowNum = Apply checkRowNum(r) immediately after parsing a row's `r` attribute; reject rows where r is negative or greater than TotalRows (1048576) with the same error behavior as the checked parser. - Configuration
Modify `GetRows()` (and/or shared `Rows` implementation used by it) so that if row parsing fails during the streaming path, the function returns that specific row parsing error (the same style of error used by the checked parser), rather than continuing and returning only errors from `Rows.Close()`.
excelize GetRows/Rows streaming iteration error handling behavior for row parsing failures = Return and preserve row parsing errors from `GetRows()` instead of silently continuing or only returning `Rows.Close()` errors. - Compensating control
At the application layer, treat user-supplied XLSX files as untrusted: validate/scan XLSX before calling `GetRows()` (e.g., reject documents whose sheet `row` attributes exceed 1048576) to mitigate attacker-controlled allocation/CPU scaling in the availability path.
Event History
Frequently Asked Questions
Which applications are exposed to this issue?
Applications using github.com/xuri/excelize/v2 are exposed when they process attacker-provided XLSX files through the streaming worksheet reader used by Rows or GetRows. The reported impact specifically occurs when an application calls GetRows.
What does an attacker need to include in an XLSX file?
The file can be small but contain a worksheet row with an r attribute above Excelize's maximum row count of 1048576 and omit the cell coordinate. This causes GetRows to append empty rows up to the supplied row number.
How can I determine whether my deployment is affected?
Check whether your application uses GetRows or Rows on untrusted XLSX input and whether it uses v2.10.1 or the reported default-branch commit 1213a8bd7c5ab360554603ac5c995ccaf6eb4314. Those versions were tested as affected.
Is a fixed version available?
No fixed version was known at the time of the report.