GitHub Advisoryadvisory · Go
GHSA-q5j5-6p94-4gwc Excelize: Streaming GetRows row-bound bypass causes attacker-controlled allocation
HIGH
Official page ↗
- Aliases
- CVE-2026-59161
- Ecosystem
- Go
- Packages
- github.com/xuri/excelize/v2, github.com/xuri/excelize
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
- Weaknesses
- CWE-400, CWE-770
- Reviewed
- 2026-09-10
Affected packages
• Go github.com/xuri/excelize/v2: < 2.11.0 → fixed in 2.11.0
• Go github.com/xuri/excelize: <= 1.4.0 (no fix yet)
Description
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
0
```
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:
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:
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.
Related
Alias · CVE (1)
- CVE-2026-59161 Excelize is a Go language library for reading and writing Microsoft Excel spreadsheets. vulnerability
References
- https://github.com/qax-os/excelize/security/advisories/GHSA-q5j5-6p94-4gwc web
- https://nvd.nist.gov/vuln/detail/CVE-2026-59161 advisory
- https://github.com/qax-os/excelize/pull/2331 web
- https://github.com/qax-os/excelize/commit/93f0b3caed37f21ef5079e3259c6c21dcfe68453 web
- https://github.com/qax-os/excelize package
- https://github.com/qax-os/excelize/releases/tag/v2.11.0 web