Excelize Decrypt: unrecoverable panics on malformed OLE/CFB encrypted workbooksGHSA-2j4c-ffch-9f23
HIGHFix: qax-os/excelize#2395GHSA-2j4c-ffch-9f23 is a high-severity (CVSS 7.5) CWE-248 vulnerability in github.com/xuri/excelize/v2. A fix is available for github.com/xuri/excelize/v2 — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-2j4c-ffch-9f23.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-2j4c-ffch-9f23 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
github.com/xuri/excelize/v2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Any file whose first 8 bytes are the OLE compound-file signature (D0 CF 11 E0 A1 B1 1A E1) is routed by OpenFile/OpenReader/OpenBytes → openReaderAt → Decrypt. The version dispatch only guarantees len(EncryptionInfo) >= 4 before handing attacker-controlled EncryptionInfo/EncryptedPackage buffers to standardDecrypt/agileDecrypt, and no callee validates structure. Malformed but version-valid content therefore fails as an unrecovered runtime panic instead of an error, terminating the calling process.
Details
All panic classes below were execution-confirmed against pristine master (ecd99d761fe0, 2026-09-08). excelize.go:211-213 maps Decrypt errors to ErrWorkbookFileFormat, but panics bypass that path and kill the process.
| # | Malformed input | Panic | Site |
|---|---|---|---|
| 1 | standard, len(EncryptionInfo) 4–11 | slice bounds [:12] | crypt.go:238 |
| 2 | standard, attacker-controlled headerSize uint32 | slice bounds [12:12+headerSize] / fixed-offset header reads | crypt.go:238-249 |
| 3 | standard, verifier remainder < 72 (AES) / 60 (RC4) bytes | slice bounds in standardEncryptionVerifier | crypt.go:282-295 |
| 4 | standard, header.KeySize = 0xFFFFFFFF | slice bounds [:536870911] with capacity 48 | crypt.go:321 |
| 5 | standard, EncryptedPackage stream missing/short | slice bounds [8:0] | crypt.go:268 |
| 6 | agile, len(EncryptionInfo) 4–7 | slice bounds [8:4] | crypt.go:407 |
| 7 | agile, valid XML without <keyEncryptors> | index out of range [0] with length 0 | crypt.go:416, 433 |
| 8 | agile, saltValue decoded length ≠ AES block | cipher.NewCBCDecrypter: IV length must equal block size | crypt.go:425 → 512 |
| 9 | any, keyData blockSize="0" | integer divide by zero | crypt.go:539 |
Note the asymmetry pinpointing the missing constraint: the agile path already checks len(EncryptedPackage) >= 8 (crypt.go:520-523) but the standard path does not (#5). Existing tests only cover the error paths (short <4 bytes → ErrUnknownEncryptMechanism, bad XML, base64 errors), never these panic paths.
PoC
Standalone programs (public API only, inputs built in memory) were provided to the maintainer by email: 1-decrypt-panic builds seven malformed CFB containers and shows each panic escaping the public Decrypt API plus one end-to-end OpenReader crash. All cases print PANIC on master and BLOCKED with the proposed patch. A regression guard proves legitimate decryption is unaffected: a workbook encrypted with the package's own Encrypt() still opens through the same code path.
(A separate advisory covers the unbounded/negative allocation in extractPart.)
Impact
Any service that calls OpenFile/OpenReader/OpenBytes on untrusted input (upload processing, mail scanning, spreadsheet conversion) can be killed remotely and without authentication by a file of ~100 bytes to ~3 KB. No password is required — panics occur during structural/parameter handling before successful decryption. Site variety means filtering one pattern does not help.
Proposed fix
A recover() boundary in Decrypt mapping any panic to ErrWorkbookFileFormat (restores the documented error-routing contract; legitimate standard/agile decryption unaffected). A complete patch has been provided to the maintainer; per-site length validation is recommended as defense in depth.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/xuri/excelize/v2 | ≥ 2.3.1&&< 2.11.1-0.20260915055537-22f76f9acb94 | 2.11.1-0.20260915055537-22f76f9acb94go get github.com/xuri/excelize/v2@v2.11.1-0.20260915055537-22f76f9acb94 |
Affected Products
excelizeexcelizeDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/xuri/excelize/v2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/xuri/excelize/v2 to 2.11.1-0.20260915055537-22f76f9acb94 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-2j4c-ffch-9f23 is resolved across your whole dependency graph.
Workarounds
Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.
Frequently Asked Questions
Is GHSA-2j4c-ffch-9f23 in your dependencies?
Find it across Go, including transitive dependencies.