| Title | xlnt-community xlnt 5606f5b Heap-based Buffer Overflow |
|---|
| Description | ### Description
We discovered a Heap-buffer-overflow vulnerability in xlnt. The crash occurs in the xlnt::detail::decode_base64 function when parsing an encrypted XLSX file (specifically involving Agile Encryption info).
The ASAN report indicates a WRITE of size 1 occurring exactly 0 bytes after the allocated region. This suggests an Off-by-one error where the Base64 decoder writes one byte past the end of the destination vector.
Vendor confirmed and fixed this vulnerability in commit [f2d7bf4](https://github.com/xiaohunqupo/xlnt/commit/f2d7bf494e5c52706843cf7eb9892821bffb0734).
### Environment
- OS: Linux x86_64
- Complier: Clang
- Build Configuration: Release mode with ASan enabled.
### Vulnerability Details
- Target: xlnt
- Vulnerability Type: CWE-193: Off-by-one Error / CWE-122: Heap-based Buffer Overflow
- Function: xlnt::detail::decode_base64
- Location: source/detail/cryptography/base64.cpp:176
- Caller: read_agile_encryption_info at source/detail/cryptography/xlsx_crypto_consumer.cpp:208
- Root Cause Analysis: The function decode_base64 calculates the expected output size and allocates a std::vector (in this case, 70 bytes) at line 106. During the decoding loop, logic related to padding or index calculation appears to be slightly incorrect for certain malformed or edge-case Base64 input strings. At line 176, the code attempts to write the decoded byte into the vector, but the index exceeds the allocated size by exactly 1 byte.
### Reproduce
1. Build xlnt and harness with Release optimization and ASAN enabled.
<details>
<summary>harness.c</summary>
```
#include <xlnt/xlnt.hpp>
#include <iostream>
#include <string>
#include <vector>
int main(int argc, char **argv) {
if (argc < 2) {
return 0;
}
std::string filepath = argv[1];
try {
xlnt::workbook wb;
wb.load(filepath);
if (wb.sheet_count() > 0) {
auto ws = wb.active_sheet();
for (auto row : ws.rows(false)) {
for (auto cell : row) {
(void)cell.to_string();
}
}
}
} catch (const xlnt::exception& e) {
} catch (const std::exception& e) {
} catch (...) {
}
return 0;
}
```
</details>
2. Run with the crashing [file](https://github.com/oneafter/0128/blob/main/xl1/repro):
```
./harness repro
```
<details>
<summary>ASAN report</summary>
```
==44083==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x507000002056 at pc 0x7fb0d851e7a9 bp 0x7ffe97816c30 sp 0x7ffe97816c28
WRITE of size 1 at 0x507000002056 thread T0
#0 0x7fb0d851e7a8 in xlnt::detail::decode_base64(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/base64.cpp:176:32
#1 0x7fb0d85753ce in (anonymous namespace)::read_agile_encryption_info(std::istream&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:208:31
#2 0x7fb0d85753ce in (anonymous namespace)::read_encryption_info(std::istream&, std::__cxx11::basic_string<char16_t, std::char_traits<char16_t>, std::allocator<char16_t>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:278:22
#3 0x7fb0d856ff58 in (anonymous namespace)::decrypt_xlsx(std::vector<unsigned char, std::allocator<unsigned char>> const&, std::__cxx11::basic_string<char16_t, std::char_traits<char16_t>, std::allocator<char16_t>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:323:28
#4 0x7fb0d856ff58 in xlnt::detail::decrypt_xlsx(std::vector<unsigned char, std::allocator<unsigned char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:339:12
#5 0x7fb0d8572ed2 in xlnt::detail::xlsx_consumer::read(std::istream&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:345:28
#6 0x7fb0d83e76c9 in xlnt::workbook::load(std::istream&) /src/xlnt/source/workbook/workbook.cpp:906:22
#7 0x7fb0d83e5f77 in xlnt::workbook::load(xlnt::path const&) /src/xlnt/source/workbook/workbook.cpp:942:5
#8 0x7fb0d840d018 in xlnt::workbook::load(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/workbook/workbook.cpp:929:12
#9 0x5645520ee27e in main /src/xlnt/fuzz_xlnt.cpp:18:12
#10 0x7fb0d7c201c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e)
#11 0x7fb0d7c2028a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e)
#12 0x56455200a594 in _start (/src/xlnt/fuzz_xlnt+0x2d594) (BuildId: 9402de847aab9fb45990c4e2333074ba128dc968)
0x507000002056 is located 0 bytes after 70-byte region [0x507000002010,0x507000002056)
allocated by thread T0 here:
#0 0x5645520eba41 in operator new(unsigned long) (/src/xlnt/fuzz_xlnt+0x10ea41) (BuildId: 9402de847aab9fb45990c4e2333074ba128dc968)
#1 0x7fb0d851d498 in std::__new_allocator<unsigned char>::allocate(unsigned long, void const*) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/new_allocator.h:151:27
#2 0x7fb0d851d498 in std::allocator_traits<std::allocator<unsigned char>>::allocate(std::allocator<unsigned char>&, unsigned long) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/alloc_traits.h:482:20
#3 0x7fb0d851d498 in std::_Vector_base<unsigned char, std::allocator<unsigned char>>::_M_allocate(unsigned long) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:381:20
#4 0x7fb0d851d498 in std::_Vector_base<unsigned char, std::allocator<unsigned char>>::_M_create_storage(unsigned long) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:398:33
#5 0x7fb0d851d498 in std::_Vector_base<unsigned char, std::allocator<unsigned char>>::_Vector_base(unsigned long, std::allocator<unsigned char> const&) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:335:9
#6 0x7fb0d851d498 in std::vector<unsigned char, std::allocator<unsigned char>>::vector(unsigned long, std::allocator<unsigned char> const&) /usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:557:9
#7 0x7fb0d851d498 in xlnt::detail::decode_base64(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/base64.cpp:106:19
#8 0x7fb0d85753ce in (anonymous namespace)::read_agile_encryption_info(std::istream&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:208:31
#9 0x7fb0d85753ce in (anonymous namespace)::read_encryption_info(std::istream&, std::__cxx11::basic_string<char16_t, std::char_traits<char16_t>, std::allocator<char16_t>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:278:22
#10 0x7fb0d856ff58 in (anonymous namespace)::decrypt_xlsx(std::vector<unsigned char, std::allocator<unsigned char>> const&, std::__cxx11::basic_string<char16_t, std::char_traits<char16_t>, std::allocator<char16_t>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:323:28
#11 0x7fb0d856ff58 in xlnt::detail::decrypt_xlsx(std::vector<unsigned char, std::allocator<unsigned char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:339:12
#12 0x7fb0d8572ed2 in xlnt::detail::xlsx_consumer::read(std::istream&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/detail/cryptography/xlsx_crypto_consumer.cpp:345:28
#13 0x7fb0d83e76c9 in xlnt::workbook::load(std::istream&) /src/xlnt/source/workbook/workbook.cpp:906:22
#14 0x7fb0d83e5f77 in xlnt::workbook::load(xlnt::path const&) /src/xlnt/source/workbook/workbook.cpp:942:5
#15 0x7fb0d840d018 in xlnt::workbook::load(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /src/xlnt/source/workbook/workbook.cpp:929:12
#16 0x5645520ee27e in main /src/xlnt/fuzz_xlnt.cpp:18:12
#17 0x7fb0d7c201c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e)
#18 0x7fb0d7c2028a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e)
#19 0x56455200a594 in _start (/src/xlnt/fuzz_xlnt+0x2d594) (BuildId: 9402de847aab9fb45990c4e2333074ba128dc968)
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/xlnt/source/detail/cryptography/base64.cpp:176:32 in xlnt::detail::decode_base64(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)
Shadow bytes around the buggy address:
0x507000001d80: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
0x507000001e00: 00 00 00 00 04 fa fa fa fa fa 00 00 00 00 00 00
0x507000001e80: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
0x507000001f00: 00 04 fa fa fa fa 00 00 00 00 00 00 00 00 00 fa
0x507000001f80: fa fa fa fa fd fd fd fd fd fd fd fd fd fa fa fa
=>0x507000002000: fa fa 00 00 00 00 00 00 00 00[06]fa fa fa fa fa
0x507000002080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x507000002100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x507000002180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x507000002200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x507000002280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzon |
|---|
| Source | ⚠️ https://github.com/xlnt-community/xlnt/issues/137 |
|---|
| User | Oneafter (UID 92781) |
|---|
| Submission | 02/09/2026 02:10 AM (2 months ago) |
|---|
| Moderation | 02/18/2026 06:59 PM (10 days later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 346649 [xlnt-community xlnt up to 1.6.1 Encrypted XLSX File Parser base64.cpp decode_base64 off-by-one] |
|---|
| Points | 20 |
|---|