GHSA-q5j5-6p94-4gwc
Dashboard / Vulnerabilities / GHSA-q5j5-6p94-4gwc
Summary: Excelize: Streaming GetRows row-bound bypass causes attacker-controlled allocation
Details: # 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: 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
Affected packages
Package
Name: github.com/xuri/excelize/v2
Purl: pkg:golang/github.com/xuri/excelize/v2
Affected ranges
Type: SEMVER
Events:
