Go · github.com/xuri/excelize/v2
Excelize: Streaming GetRows row-bound bypass causes attacker-controlled allocation
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).
github.com/xuri/excelize/v21213a8bd7c5ab360554603ac5c995ccaf6eb4314, and release v2.10.1An 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.
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.
<?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).
A minimal Go harness creates the XLSX in memory and calls GetRows("Sheet1"):
rows, err := f.GetRows("Sheet1")
fmt.Println("rows_len:", len(rows))
if len(rows) > 0 {
fmt.Println("last_row:", rows[len(rows)-1])
}
fmt.Printf("returned error: %T %v\n", err, err)
Observed output on current default branch commit 1213a8bd7c5ab360554603ac5c995ccaf6eb4314:
== streaming GetRows row r=2000000 cell without r ==
rows_len: 2000000
last_row: [ok]
returned error: <nil> <nil>
elapsed=21ms alloc_delta=46MB
Observed output on latest release tag v2.10.1:
== streaming GetRows row r=2000000 cell without r ==
rows_len: 2000000
last_row: [ok]
returned error: <nil> <nil>
elapsed=14ms alloc_delta=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.
Rows / GetRows should reject row numbers greater than TotalRows with the same error behavior as the checked parser path.
r attribute.GetRows() instead of silently continuing or returning only Rows.Close() errors.GetRows() on a worksheet containing row r="1048577" with a cell value but no cell r coordinate.Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.