{"id":"GHSA-fw94-4wwp-w8pw","summary":"Excelize: A Zip64 uncompressed-size of 2^63 panics OpenFile/OpenReader","details":"### Summary\n\nA Zip64 uncompressed-size of 2^63 casts to a negative int64 that bypasses the unzip-size guard and reaches make([]byte, 0, negativeCap)\n\nA Zip64 uncompressed-size in the range [2^63, 2^64) casts to a negative `int64` in `zip.File.FileInfo().Size()`, and `ReadZipReader` does signed arithmetic on that value, so the total-decompression guard is bypassed and the negative size reaches `make([]byte, 0, size)` in `readFile`. A 159-byte crafted file panics `OpenFile` and `OpenReader` with `runtime error: makeslice: cap out of range`.\n\nThe guard, at `lib.go:44-46` on HEAD `ae2113b`:\n\n```go\nfileSize := v.FileInfo().Size()\nunzipSize += fileSize\nif unzipSize \u003e f.options.UnzipSizeLimit {\n\treturn fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit)\n}\n```\n\n`FileInfo().Size()` returns `int64(UncompressedSize64)`. `UncompressedSize64` is read from the Zip64 extended-information extra record in the central directory and is fully attacker controlled, so any value with the top bit set arrives as a negative `int64`. `unzipSize` then goes negative and the comparison against `UnzipSizeLimit` (default `1000 \u003c\u003c 24`, `templates.go:193`) is false no matter how many such entries the archive contains.\n\nThe same negative value also fails the two stream-to-temp checks at `lib.go:53` and `lib.go:64` (`fileSize \u003e f.options.UnzipXMLSizeLimit`), so the entry is not diverted to a temp file and control reaches `readFile`:\n\n```go\n// lib.go:150\ndat := make([]byte, 0, file.FileInfo().Size())\n```\n\n`make` with a negative capacity panics. There is no `recover()` in any non-test file in the library, so the panic leaves the public API and takes down the calling goroutine.\n\nThe asymmetry inside this same function is the clearest way to see it: `unzipToTemp` (`lib.go:83`) handles an entry with a plain `io.Copy` and never pre-allocates from the declared size, so it is indifferent to whatever the header claims. Only the pre-allocating branch trusts that number, and it trusts it after a signed comparison that a wrapped value walks straight through.\n\n## Reproduction, executed against HEAD `ae2113b` (go1.26.5)\n\nA 159-byte archive was built with one stored entry named `xl/worksheets/sheet1.xml`: truthful 1-byte local header, central-directory 32-bit uncompressed size set to the `0xFFFFFFFF` Zip64 sentinel, and a Zip64 extra record (tag `0x0001`, data size 8) declaring `UncompressedSize64 = 0x8000000000000000`.\n\n```\nentry \"xl/worksheets/sheet1.xml\" FileInfo().Size() = -9223372036854775808\nexcelize.OpenReader(bytes.NewReader(raw))  -\u003e  panic: runtime error: makeslice: cap out of range\n```\n\nControl, the identical archive with `UncompressedSize64 = 0x7FFFFFFFFFFFFFFF`:\n\n```\nentry \"xl/worksheets/sheet1.xml\" FileInfo().Size() = 9223372036854775807\nexcelize.OpenReader(bytes.NewReader(raw))  -\u003e  err = \"unzip size exceeds the 16777216000 bytes limit\"\n```\n\nThe control isolates the defect to the sign flip rather than to size magnitude: the guard behaves correctly for any positive declared size and fails only once the value wraps. `archive/zip` itself accepts the archive without complaint, so nothing upstream of excelize rejects it.\n\n## What I verified and what I did not\n\nI ran both cases above myself and confirmed each cited line at HEAD. I caught the panic with a deliberate `recover` in my harness so I could print it; nothing in excelize recovers it, which I checked by grepping every non-test `.go` file. I did not separately run the `OpenFile` variant, since `OpenFile` reaches the same `ReadZipReader` loop, and I did not attempt to turn the bypassed size cap into a separate decompression-bomb primitive.\n\n## Attacker model\n\nAny service that opens a spreadsheet it did not produce. The payload is 159 bytes, needs no authentication, and is deterministic.\n\n## Suggested fix\n\nCompare against the limit in unsigned space, and bound the allocation independently rather than trusting the header:\n\n```go\nif v.UncompressedSize64 \u003e uint64(f.options.UnzipSizeLimit) {\n\treturn fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit)\n}\n```\n\nand in `readFile`, reject or clamp a declared size that is negative or larger than the limit before calling `make`. The same unsigned treatment applies to the two `UnzipXMLSizeLimit` comparisons, which currently route a wrapped size away from the safe streaming path and into the pre-allocating one.","aliases":["CVE-2026-107224"],"modified":"2026-10-07T20:30:07.330722837Z","published":"2026-10-07T20:22:45Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-190","CWE-681"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-07T20:22:45Z"},"references":[{"type":"WEB","url":"https://github.com/qax-os/excelize/security/advisories/GHSA-fw94-4wwp-w8pw"},{"type":"WEB","url":"https://github.com/qax-os/excelize/pull/2369"},{"type":"WEB","url":"https://github.com/qax-os/excelize/commit/db93f8d89de71589069a54d1b998af02e3c5f7cd"},{"type":"PACKAGE","url":"https://github.com/qax-os/excelize"}],"affected":[{"package":{"name":"github.com/xuri/excelize/v2","ecosystem":"Go","purl":"pkg:golang/github.com/xuri/excelize/v2"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.1.0"},{"fixed":"2.11.1-0.20260805032953-db93f8d89de7"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-fw94-4wwp-w8pw/GHSA-fw94-4wwp-w8pw.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H"}]}