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.
References
Summary
Any file whose first 8 bytes are the OLE compound-file signature (
D0 CF 11 E0 A1 B1 1A E1) is routed byOpenFile/OpenReader/OpenBytes→openReaderAt→Decrypt. The version dispatch only guaranteeslen(EncryptionInfo) >= 4before handing attacker-controlledEncryptionInfo/EncryptedPackagebuffers tostandardDecrypt/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-213mapsDecrypterrors toErrWorkbookFileFormat, but panics bypass that path and kill the process.len(EncryptionInfo)4–11[:12]crypt.go:238headerSizeuint32[12:12+headerSize]/ fixed-offset header readscrypt.go:238-249standardEncryptionVerifiercrypt.go:282-295header.KeySize= 0xFFFFFFFF[:536870911]with capacity 48crypt.go:321EncryptedPackagestream missing/short[8:0]crypt.go:268len(EncryptionInfo)4–7[8:4]crypt.go:407<keyEncryptors>[0]with length 0crypt.go:416,433saltValuedecoded length ≠ AES blockcipher.NewCBCDecrypter: IV length must equal block sizecrypt.go:425→512keyData blockSize="0"crypt.go:539Note 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<4bytes →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-panicbuilds seven malformed CFB containers and shows each panic escaping the publicDecryptAPI plus one end-to-endOpenReadercrash. 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 ownEncrypt()still opens through the same code path.(A separate advisory covers the unbounded/negative allocation in
extractPart.)Impact
Any service that calls
OpenFile/OpenReader/OpenByteson 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 inDecryptmapping any panic toErrWorkbookFileFormat(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.References