highCVE-2026-59161Sep 10, 2026

CVE-2026-59161: Excelize GetRows Streaming Row-Bound Bypass Leads to Attacker-Controlled Memory Allocation

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packagegithub.com/xuri/excelize/v2
Ecosystemgo
Affected< 2.11.0
Fixed in2.11.0
CVE-2026-59161: Excelize GetRows Streaming Row-Bound Bypass Leads to Attacker-Controlled Memory Allocation

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.

text
<!-- 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.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research