CVE-2026-59161: Excelize GetRows Streaming Row-Bound Bypass Leads to Attacker-Controlled Memory Allocation
A crafted XLSX file can set a row number far above Excel's legal limit in the streaming reader, causing GetRows to silently allocate millions of empty rows and exhaust server memory.

The problem
Excelize's GetRows and Rows APIs use a streaming XML decoder that reads the r attribute of each <row> element directly, without calling checkRowNum. This means the 1,048,576-row limit enforced by the non-streaming parser is simply absent on the hot path most applications use.
An attacker who can supply an XLSX file can set <row r="2000000"> (or higher) and omit any cell coordinate. GetRows then appends empty row slices all the way up to that index, returning a 2-million-element slice, allocating ~46 MB, with no error, no panic, and no log entry.
The allocation scales linearly with the attacker-chosen row number.
Proof of concept
A working proof-of-concept for CVE-2026-59161 in github.com/xuri/excelize/v2, with the exact payload below.
<!-- xl/worksheets/sheet1.xml inside a valid XLSX archive -->
<?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>
<!-- xl/sharedStrings.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
<si><t>ok</t></si>
</sst>The non-streaming parser calls checkRowNum(r.R) before any allocation, rejecting rows above TotalRows (1,048,576). The streaming path in rows.go skips that check entirely: Rows.Next() and Rows.Columns() both assign the raw r attribute to iterator state and GetRows uses it as the loop bound, appending one empty slice per missing row with no guard.
Because the cell inside the malicious row omits its own r coordinate attribute, the per-cell coordinate check is also bypassed, so the only place the row number could have been caught (the non-streaming parser) is never reached. The patch (PR #2331, commit 93f0b3ca) adds the same checkRowNum validation to the streaming handler in rows.go, making both paths consistent (CWE-400, CWE-770).
This is an incomplete-fix variant of CVE-2026-54063 / GHSA-h69g: the earlier fix only hardened the checked parser, leaving the streaming reader open to the same class of attack.
The fix
Upgrade to github.com/xuri/excelize/v2 v2.11.0. The fix (PR #2331, commit 93f0b3caed37f21ef5079e3259c6c21dcfe68453) adds row-number validation via checkRowNum inside the streaming XML handler in rows.go, so both Rows and GetRows now reject any r attribute above TotalRows with the same error returned by the non-streaming parser.
Related research
- high · 7.5CVE-2026-54063CVE-2026-54063: Excelize Unbounded Row Allocation DoS in Worksheet Parser
- high · 7.5CVE-2026-67446CVE-2026-67446: Mailpit Thumbnail Handler Uncontrolled Memory Allocation via Oversized Image Dimensions
- high · 7.5CVE-2026-64868CVE-2026-64868: new-api Unauthenticated Webhook DoS via Unbounded Body Read
- high · 7.5CVE-2026-50274CVE-2026-50274: dd-trace-go Unbounded W3C Baggage Header Parsing DoS