From b6c567b2c6f246eff03995ded0829df0c4485aa3 Mon Sep 17 00:00:00 2001 From: DeNdlerX <254494963+DeNdlerX@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:05:02 +0200 Subject: [PATCH] Check length of WinZip AES AE-2 entries. AE-2 entries have no CRC, so zip_source_zip_file_create didn't add the CRC layer, and nothing compared the length of the decrypted data with the uncompressed size from the archive. Also add the layer when the whole entry is decrypted. In validation mode, it now reports a CRC in its stat only if the lower layer had one, so no CRC is checked or reported for AE-2 entries. AE-1 entries are unaffected. Reading an AE-2 entry whose data doesn't match its size now fails with ZIP_ER_INCONS (ZIP_ER_DETAIL_INVALID_FILE_LENGTH). For compressed AE-2 entries, this also needs the decompression layer to keep the size from the archive. Assisted-by: Claude Code (Opus 5.5) --- lib/zip_source_crc.c | 8 ++++++-- lib/zip_source_zip_new.c | 4 ++-- regress/data/incons-aes-ae2-data-longer.zip | Bin 0 -> 320 bytes regress/data/incons-aes-ae2-data-shorter.zip | Bin 0 -> 320 bytes regress/read_incons_aes_ae2_longer.test | 11 +++++++++++ regress/read_incons_aes_ae2_shorter.test | 11 +++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 regress/data/incons-aes-ae2-data-longer.zip create mode 100644 regress/data/incons-aes-ae2-data-shorter.zip create mode 100644 regress/read_incons_aes_ae2_longer.test create mode 100644 regress/read_incons_aes_ae2_shorter.test diff --git a/lib/zip_source_crc.c b/lib/zip_source_crc.c index 9933dae4d..34966c2d5 100644 --- a/lib/zip_source_crc.c +++ b/lib/zip_source_crc.c @@ -152,11 +152,15 @@ static zip_int64_t crc_read(zip_source_t *src, void *_ctx, void *data, zip_uint6 /* TODO: Set comp_size, comp_method, encryption_method? After all, this only works for uncompressed data. */ st->size = ctx->size; - st->crc = ctx->crc; st->comp_size = ctx->size; st->comp_method = ZIP_CM_STORE; st->encryption_method = ZIP_EM_NONE; - st->valid |= ZIP_STAT_SIZE | ZIP_STAT_CRC | ZIP_STAT_COMP_SIZE | ZIP_STAT_COMP_METHOD | ZIP_STAT_ENCRYPTION_METHOD; + st->valid |= ZIP_STAT_SIZE | ZIP_STAT_COMP_SIZE | ZIP_STAT_COMP_METHOD | ZIP_STAT_ENCRYPTION_METHOD; + /* When validating, don't report a CRC for data that has none (WinZip AES AE-2). */ + if (!ctx->validate || (st->valid & ZIP_STAT_CRC)) { + st->crc = ctx->crc; + st->valid |= ZIP_STAT_CRC; + } } return 0; } diff --git a/lib/zip_source_zip_new.c b/lib/zip_source_zip_new.c index 8ed76964c..f36be6859 100644 --- a/lib/zip_source_zip_new.c +++ b/lib/zip_source_zip_new.c @@ -141,8 +141,8 @@ ZIP_EXTERN zip_source_t *zip_source_zip_file_create(zip_t *srcza, zip_uint64_t s needs_decrypt = ((flags & ZIP_FL_ENCRYPTED) == 0) && encrypted; compressed = (st.valid & ZIP_STAT_COMP_METHOD) && (st.comp_method != ZIP_CM_STORE); needs_decompress = ((flags & ZIP_FL_COMPRESSED) == 0) && compressed; - /* when reading the whole file, check for CRC errors */ - needs_crc = ((flags & ZIP_FL_COMPRESSED) == 0 || !compressed) && !partial_data && (st.valid & ZIP_STAT_CRC) != 0; + /* when reading the whole file, check for CRC errors; for decrypted data without CRC (WinZip AES AE-2), still check the length */ + needs_crc = ((flags & ZIP_FL_COMPRESSED) == 0 || !compressed) && !partial_data && ((st.valid & ZIP_STAT_CRC) != 0 || needs_decrypt); if (needs_decrypt) { if (password == NULL) { diff --git a/regress/data/incons-aes-ae2-data-longer.zip b/regress/data/incons-aes-ae2-data-longer.zip new file mode 100644 index 0000000000000000000000000000000000000000..10cf9b5a633aa7adaae984961f09de087482f23d GIT binary patch literal 320 zcmWIWW@h1H0D+v|QZ$-un2 zvnm3F!3wwnfQE=LFft@F*qbK4;*Lj-`5`C&B!FjjLUfvAd7&u zGB9ju1hK$w2@PR|xDL(LAfrT>aXS)Z00?YrbOn;woE+fI3JMmW511Hwf%IGuhXDXY C*g^RK literal 0 HcmV?d00001 diff --git a/regress/data/incons-aes-ae2-data-shorter.zip b/regress/data/incons-aes-ae2-data-shorter.zip new file mode 100644 index 0000000000000000000000000000000000000000..c996c74da520d81ee95e814df20f4e240cd3fc20 GIT binary patch literal 320 zcmWIWW@h1H0D+v|QZ$-un2 zvnm3F!3wwnfQE=LFft@F*qbK4;*Lj-`5`C&B!FjjLUfvAd7&u zGB9ju1hK$w2@PR|xDL(LAfrT>aXS)Z00?YrbOn;woE+fI3JMmW511Hwf%IGuhXDXr CC_)(k literal 0 HcmV?d00001 diff --git a/regress/read_incons_aes_ae2_longer.test b/regress/read_incons_aes_ae2_longer.test new file mode 100644 index 000000000..a5595e6d0 --- /dev/null +++ b/regress/read_incons_aes_ae2_longer.test @@ -0,0 +1,11 @@ +# WinZip AES AE-2 entry (no CRC) decrypts to more data than the uncompressed size in its header +features HAVE_CRYPTO +arguments test.zip set_password foofoofoo cat 1 +return 1 +file test.zip incons-aes-ae2-data-longer.zip +stdout +encrypted +end-of-inline-data +stderr +can't read file at index '1': Zip archive inconsistent: file length in header doesn't match actual file length +end-of-inline-data diff --git a/regress/read_incons_aes_ae2_shorter.test b/regress/read_incons_aes_ae2_shorter.test new file mode 100644 index 000000000..0fb84b447 --- /dev/null +++ b/regress/read_incons_aes_ae2_shorter.test @@ -0,0 +1,11 @@ +# WinZip AES AE-2 entry (no CRC) decrypts to less data than the uncompressed size in its header +features HAVE_CRYPTO +arguments test.zip set_password foofoofoo cat 1 +return 1 +file test.zip incons-aes-ae2-data-shorter.zip +stdout +encrypted +end-of-inline-data +stderr +can't read file at index '1': Zip archive inconsistent: file length in header doesn't match actual file length +end-of-inline-data