{"id":"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\n\n## Summary\n\nExcelize'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.\n\nThis was reproduced on the current default branch commit `1213a8bd7c5ab360554603ac5c995ccaf6eb4314` and the latest release tag `v2.10.1` (`5ad5ab3af0054c55bdce09f1530085600e9f2e45`).\n\n## Affected package\n\n- Package: `github.com/xuri/excelize/v2`\n- Tested affected versions: current default branch at `1213a8bd7c5ab360554603ac5c995ccaf6eb4314`, and release `v2.10.1`\n- Fixed version: none known at the time of this report\n\n## Impact\n\nAn 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.\n\nIn 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.\n\n## Root cause\n\nThe checked parser path validates row numbers:\n\n- `excelize.go`: `checkRowNum(r int)` rejects negative rows and rows greater than `TotalRows`.\n- `excelize.go`: `checkSheet()` calls `checkRowNum(r.R)` before allocating sheet rows.\n- `workSheetReader()` invokes `checkSheet()` / `checkRow()` before returning a cached worksheet.\n\nThe streaming path does not use that checked parser:\n\n- `rows.go`: `Rows(sheet)` opens an XML decoder directly.\n- `Rows.Next()` accepts the row `r` attribute and assigns it to the iterator's current row without applying `checkRowNum()`.\n- `Rows.Columns()` also assigns row `r` to the iterator state without applying `checkRowNum()`.\n- `GetRows()` appends empty row slices for the gap between the previous row and the current row.\n\nBecause 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.\n\n## Minimal worksheet payload\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cworksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"\u003e\n  \u003csheetData\u003e\n    \u003crow r=\"2000000\"\u003e\u003cc t=\"s\"\u003e\u003cv\u003e0\u003c/v\u003e\u003c/c\u003e\u003c/row\u003e\n  \u003c/sheetData\u003e\n\u003c/worksheet\u003e\n```\n\nThe workbook also contains a normal `sharedStrings.xml` with one string (`ok`).\n\n## Reproduction\n\nA minimal Go harness creates the XLSX in memory and calls `GetRows(\"Sheet1\")`:\n\n```go\nrows, err := f.GetRows(\"Sheet1\")\nfmt.Println(\"rows_len:\", len(rows))\nif len(rows) \u003e 0 {\n    fmt.Println(\"last_row:\", rows[len(rows)-1])\n}\nfmt.Printf(\"returned error: %T %v\\n\", err, err)\n```\n\nObserved output on current default branch commit `1213a8bd7c5ab360554603ac5c995ccaf6eb4314`:\n\n```text\n== streaming GetRows row r=2000000 cell without r ==\nrows_len: 2000000\nlast_row: [ok]\nreturned error: \u003cnil\u003e \u003cnil\u003e\nelapsed=21ms alloc_delta=46MB\n```\n\nObserved output on latest release tag `v2.10.1`:\n\n```text\n== streaming GetRows row r=2000000 cell without r ==\nrows_len: 2000000\nlast_row: [ok]\nreturned error: \u003cnil\u003e \u003cnil\u003e\nelapsed=14ms alloc_delta=46MB\n```\n\nA 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.\n\n## Expected behavior\n\n`Rows` / `GetRows` should reject row numbers greater than `TotalRows` with the same error behavior as the checked parser path.\n\n## Suggested remediation\n\n- Apply the same row-bound validation in the streaming reader immediately after parsing a row `r` attribute.\n- Preserve and return row parsing errors from `GetRows()` instead of silently continuing or returning only `Rows.Close()` errors.\n- Add regression tests for `GetRows()` on a worksheet containing `row r=\"1048577\"` with a cell value but no cell `r` coordinate.","aliases":["CVE-2026-59161","GO-2026-6453"],"modified":"2026-09-17T17:40:38.684695894Z","published":"2026-09-10T15:09:09Z","database_specific":{"nvd_published_at":"2026-07-10T17:17:02Z","cwe_ids":["CWE-400","CWE-770"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-09-10T15:09:09Z"},"references":[{"type":"WEB","url":"https://github.com/qax-os/excelize/security/advisories/GHSA-q5j5-6p94-4gwc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59161"},{"type":"WEB","url":"https://github.com/qax-os/excelize/pull/2331"},{"type":"WEB","url":"https://github.com/qax-os/excelize/commit/93f0b3caed37f21ef5079e3259c6c21dcfe68453"},{"type":"PACKAGE","url":"https://github.com/qax-os/excelize"},{"type":"WEB","url":"https://github.com/qax-os/excelize/releases/tag/v2.11.0"}],"affected":[{"package":{"name":"github.com/xuri/excelize/v2","ecosystem":"Go","purl":"pkg:golang/github.com/xuri/excelize/v2"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.11.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-q5j5-6p94-4gwc/GHSA-q5j5-6p94-4gwc.json"}},{"package":{"name":"github.com/xuri/excelize","ecosystem":"Go","purl":"pkg:golang/github.com/xuri/excelize"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"1.4.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-q5j5-6p94-4gwc/GHSA-q5j5-6p94-4gwc.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"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"}]}