Summary
mz_zip_reader_entry_get_hash() validates a hash extra-field record's digest_size field against MZ_HASH_MAX_SIZE (256), but then uses a different, unvalidated value—the caller-supplied digest_size parameter—as the copy length when reading into a fixed 256-byte stack buffer.
By crafting a ZIP entry with two MZ_ZIP_EXTENSION_HASH (0x1a51) extra-field records—one oversized to poison reader->hash_digest_size and another with a digest_size small enough to pass validation—an attacker can trigger an attacker-controlled write of up to 65,535 bytes into the 256-byte stack buffer.
This stack-based buffer overflow can corrupt adjacent stack memory, causing a denial of service through process crashes and potentially enabling arbitrary code execution depending on the runtime environment and compiler protections.
Root cause
mz_zip_reader_entry_get_first_hash() reads the 16-bit digest_size field from the first MZ_ZIP_EXTENSION_HASH (0x1a51) extra-field record of a ZIP entry. Because this value is fully attacker-controlled and is not validated, it is stored directly in reader->hash_digest_size.
Later, mz_zip_reader_entry_close() calls:
mz_zip_reader_entry_get_hash(reader, reader->hash_algorithm,
expected_hash /* uint8_t[256] on the stack */,
reader->hash_digest_size /* attacker value */);
Inside mz_zip_reader_entry_get_hash(), the extra field is scanned again, and each hash record's own cur_digest_size is validated against MZ_HASH_MAX_SIZE (256). However, the subsequent mz_stream_read() call does not use the freshly validated cur_digest_size. Instead, it uses the caller-supplied digest_size parameter—the original, unchecked value:
if ((cur_algorithm == algorithm) && (cur_digest_size <= digest_size) &&
(cur_digest_size <= MZ_HASH_MAX_SIZE)) {
if (mz_stream_read(file_extra_stream, digest, digest_size) == cur_digest_size)
return_err = MZ_OK;
Reproduction
Create a ZIP archive with a single entry whose central directory extra field contains two consecutive MZ_ZIP_EXTENSION_HASH (0x1a51) records:
- The first record should specify algorithm=SHA1 (20) and digest_size=2000, followed by 2000 bytes of filler data. This value is stored in reader->hash_digest_size.
- The second record should specify the same algorithm=SHA1 and digest_size=20, followed by 20 bytes of filler data. This value passes the size validation check.
- Append approximately 2000 bytes of additional padding after the second record to ensure enough data is available for the oversized read.
Extract the archive using minizip -x.
Log
=================================================================
==4579==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7b6b81f00260 at pc 0x60cbf8c7e518 bp 0x7ffe5c219a80 sp 0x7ffe5c219240
WRITE of size 2000 at 0x7b6b81f00260 thread T0
#0 0x60cbf8c7e517 in __asan_memcpy (/root/minizip+0xc8517) (BuildId: 406e7b0b535e229af3760f62a3c7ce8cdc63f17a)
#1 0x60cbf8cfaf62 in mz_stream_mem_read /root/minizip-ng/mz_strm_mem.c:107:5
#2 0x60cbf8cf4445 in mz_stream_read /root/minizip-ng/mz_strm.c:40:12
#3 0x60cbf8cdc4d2 in mz_zip_reader_entry_get_hash /root/minizip-ng/mz_zip_rw.c:515:17
#4 0x60cbf8cdb737 in mz_zip_reader_entry_close /root/minizip-ng/mz_zip_rw.c:458:13
#5 0x60cbf8cdc775 in mz_zip_reader_entry_save_process /root/minizip-ng/mz_zip_rw.c:602:15
#6 0x60cbf8cdc9de in mz_zip_reader_entry_save /root/minizip-ng/mz_zip_rw.c:639:19
#7 0x60cbf8cdd99a in mz_zip_reader_entry_save_file /root/minizip-ng/mz_zip_rw.c:779:15
#8 0x60cbf8cde983 in mz_zip_reader_save_all /root/minizip-ng/mz_zip_rw.c:926:15
#9 0x60cbf8cc1186 in minizip_extract /root/minizip-ng/minizip.c:404:15
#10 0x60cbf8cc2955 in main /root/minizip-ng/minizip.c:712:15
#11 0x7b6b83b3b1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#12 0x7b6b83b3b28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#13 0x60cbf8be5784 in _start (/root/minizip+0x2f784) (BuildId: 406e7b0b535e229af3760f62a3c7ce8cdc63f17a)
Address 0x7b6b81f00260 is located in stack of thread T0 at offset 608 in frame
#0 0x60cbf8cdb3cf in mz_zip_reader_entry_close /root/minizip-ng/mz_zip_rw.c:440
This frame has 2 object(s):
[32, 288) 'computed_hash' (line 446)
[352, 608) 'expected_hash' (line 447) <== Memory access at offset 608 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/root/minizip+0xc8517) (BuildId: 406e7b0b535e229af3760f62a3c7ce8cdc63f17a) in __asan_memcpy
Shadow bytes around the buggy address:
0x7b6b81efff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00000: f1 f1 f1 f1 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00100: 00 00 00 00 f2 f2 f2 f2 f2 f2 f2 f2 00 00 00 00
0x7b6b81f00180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7b6b81f00200: 00 00 00 00 00 00 00 00 00 00 00 00[f3]f3 f3 f3
0x7b6b81f00280: f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7b6b81f00480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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 redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==4579==ABORTING
$ gdb ./minizip
$ r -x poc.zip
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
(gdb) info registers
rax 0x0 0
rbx 0x4141414141414141 4702111234474983745
rcx 0x4 4
rdx 0x6055c4a39968 105921487542632
rsi 0x6053c19f80b0 105912847007920
rdi 0x6053c19f5ba8 105912846998440
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7ffe2139a2b0 0x7ffe2139a2b0
r8 0x6053c19d4010 105912846860304
r9 0x7 7
r10 0x6053c19f80c0 105912847007936
r11 0x92d84bd62fc2558 661330669237708120
r12 0x6053c03f7850 105912823937104
r13 0x0 0
r14 0x4141414141414141 4702111234474983745
r15 0x6053c19d4751 105912846862161
rip 0x414141414141 0x414141414141
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
k0 0x2002221 33563169
k1 0xffff 65535
k2 0x100ffff 16842751
k3 0x0 0
k4 0x0 0
k5 0x0 0
k6 0x0 0
k7 0x0 0
fs_base 0x77af173d3080 131593892868224
gs_base 0x0 0
(gdb)
Summary
mz_zip_reader_entry_get_hash()validates a hash extra-field record'sdigest_sizefield againstMZ_HASH_MAX_SIZE(256), but then uses a different, unvalidated value—the caller-supplieddigest_sizeparameter—as the copy length when reading into a fixed 256-byte stack buffer.By crafting a ZIP entry with two
MZ_ZIP_EXTENSION_HASH(0x1a51) extra-field records—one oversized to poisonreader->hash_digest_sizeand another with adigest_sizesmall enough to pass validation—an attacker can trigger an attacker-controlled write of up to 65,535 bytes into the 256-byte stack buffer.This stack-based buffer overflow can corrupt adjacent stack memory, causing a denial of service through process crashes and potentially enabling arbitrary code execution depending on the runtime environment and compiler protections.
Root cause
mz_zip_reader_entry_get_first_hash()reads the 16-bitdigest_sizefield from the firstMZ_ZIP_EXTENSION_HASH(0x1a51) extra-field record of a ZIP entry. Because this value is fully attacker-controlled and is not validated, it is stored directly inreader->hash_digest_size.Later,
mz_zip_reader_entry_close()calls:Inside
mz_zip_reader_entry_get_hash(), the extra field is scanned again, and each hash record's owncur_digest_sizeis validated againstMZ_HASH_MAX_SIZE(256). However, the subsequentmz_stream_read()call does not use the freshly validatedcur_digest_size. Instead, it uses the caller-supplieddigest_sizeparameter—the original, unchecked value:Reproduction
Create a ZIP archive with a single entry whose central directory extra field contains two consecutive
MZ_ZIP_EXTENSION_HASH(0x1a51) records:Extract the archive using
minizip -x.Log