GHSA-q5j5-6p94-4gwc
Excelize: Streaming GetRows row-bound bypass causes attacker-controlled allocation
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Go | github.com/xuri/excelize/v2 | < 2.11.0 | 2.11.0 |
| Go | github.com/xuri/excelize | <= 1.4.0 | no fix listed |
Details
As written in the advisory (Markdown source, shown as plain text).
# 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("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`:
```text
== 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`:
```text
== 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.
## 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.
References
External links from the advisory. They lead to third-party sites.
- https://github.com/qax-os/excelize/security/advisories/GHSA-q5j5-6p94-4gwc
- https://nvd.nist.gov/vuln/detail/CVE-2026-59161
- https://github.com/qax-os/excelize/pull/2331
- https://github.com/qax-os/excelize/commit/93f0b3caed37f21ef5079e3259c6c21dcfe68453
- https://github.com/qax-os/excelize
- https://github.com/qax-os/excelize/releases/tag/v2.11.0