KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 18:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-115906

zip: AES Entry HMAC Authentication Bypass on Premature EOF

An AES-encrypted (AE-x) entry's authentication code is verified only when the reader's byte counter lands exactly on zero.

At a glance

Exploited
Not known
Severity
6.5 Medium (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: zip: 9.0.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-8hxf-qr8p-62x4 (opens a new tab)

What happened

Summary

An AES-encrypted (AE-x) entry's authentication code is verified only when the reader's byte counter lands exactly on zero. If the bounded entry reader reaches the archive's actual EOF before the attacker-controlled, header-declared compressed_size, the counter remains positive, the HMAC is never read or compared, and the truncated, unauthenticated plaintext is returned as a clean end of stream.

A zero-payload entry also returns before authentication. An attacker who does not know the password can modify an archive so chosen or modified plaintext is accepted with no error.

Details

Continue reading

[src/aes.rs:246-292](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/aes.rs#L246-L292):

fn read(&mut self, buf: &mut [u8]) -> io::Result {
 if self.data_remaining == 0 { // @audit insufficient check
 return Ok(0);
 }

 // get the number of bytes to read, compare as u64 to make sure we can read more than
 // 2^32 bytes even on 32 bit systems.
 let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize;
 let read = self.reader.read(&mut buf[0..bytes_to_read])?;
 self.data_remaining -= read as u64;

 // Update the hmac with the encrypted data
 self.hmac.update(&buf[0..read]);

 // decrypt the data
 self.cipher.crypt_in_place(&mut buf[0..read]);

 // if there is no data left to read, check the integrity of the data
 if self.data_remaining == 0 {
 debug_assert!(
 !self.finalized,
 "Tried to use an already finalized HMAC. This is a bug!"
 );
 if self.finalized {
 return Err(Error::new(
 ErrorKind::InvalidData,
 "Tried to use an already finalized HMAC",
 ));
 }
 self.finalized = true;

 // Zip uses HMAC-Sha1-80, which only uses the first half of the hash
 // see https://www.winzip.com/win/en/aes_info.html#auth-faq
 let mut read_auth_code = [0; AUTH_CODE_LENGTH];
 self.reader.read_exact(&mut read_auth_code)?;
 let computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];

 // use constant time comparison to mitigate timing attacks
 if !constant_time_eq(computed_auth_code, &read_auth_code) {
 return Err(Error::new(
 ErrorKind::InvalidData,
 "Invalid authentication code, this could be due to an invalid password or errors in the data",
 ));
 }
 }

 Ok(read)
}

data_remaining starts at compressed_size - (2 + 10 + salt_length) ([src/aes.rs:144-145](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/aes.rs#L144-L145)), where compressed_size comes straight from the attacker-controlled central directory. Nothing cross-checks the declared data span against the archive's structural boundaries: ZipFileData::find_content() is just reader.take(self.

compressed_size) ([src/types.rs:214-223](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/types.rs#L214-L223)), and make_crypto_reader() passes the declared value into AesReader::new() ([src/read/readers.rs](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/read/readers.rs#L219-L223)).

When the entry's bounded reader reaches true EOF before the declared size, the inner reader returns Ok(0), the subtraction takes away nothing, and the finalisation block — the only place the 10-byte HMAC-SHA1-80 tag is read and compared — never runs. If bytes such as the central directory follow the entry, they are consumed as payload until the Take limit is reached; the HMAC then usually fails.

The bypass therefore requires true EOF at the premature end (as in the PoC) or the zero-payload case. The early return at the top of read() means an entry whose declared size leaves zero payload bytes is never authenticated either, even though its tag is present in the file.

This defeats the guarantee the type documents: *"After the data has been read and decrypted an HMAC will be checked and provide a final means to check if either the password is invalid or if the data has been changed."*

AE-2 entries skip the CRC-32 check by design ([src/read/readers.rs](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/read/readers.rs#L248-L255) → Crc32Reader::new(.., should_disable = true)), so an AE-2 entry on this path has no integrity check of any kind remaining.

AES-CTR is malleable, so an attacker who can modify an archive in transit — without the password — can XOR chosen plaintext onto the ciphertext and arrange for the entry to end at true EOF. The password-bearing read paths are affected, including by_index_decrypt(), by_name_decrypt(), by_path_decrypt(), and by_index_with_options() when a password is supplied.

Bare extract() calls by_index() without a password and therefore rejects AES-encrypted entries; an application that decrypts entries through these APIs before writing or extracting them is affected.

PoC

tests/aes_hmac_bypass.rs below; Cargo.toml needs only zip = { git = "https://github.com/zip-rs/zip2", rev = "6f09b3064ec87c3a69ea9f3413f9f9638b4aa446" }, or zip = "8.6.0" to run it against the published 8.6.0 release unchanged. All three tests pass in debug and release against the reviewed commit.

use std::io::{Cursor, Read, Write};
use zip::write::SimpleFileOptions;
use zip::{AesMode, CompressionMethod, ZipArchive, ZipWriter};

const PASSWORD: &str = "correct horse battery staple";
const GENUINE: &[u8] = b"balance: 100 USD\n";
const FORGED: &[u8] = b"balance: 999 USD\n";
const AES256_OVERHEAD: usize = 28; // 16-byte salt + 2-byte verifier + 10-byte tag

fn genuine_archive(plaintext: &[u8]) -> Vec {
 let mut w = ZipWriter::new(Cursor::new(Vec::new()));
 let opts = SimpleFileOptions::default()
 .compression_method(CompressionMethod::Stored)
 .with_aes_encryption(AesMode::Aes256, PASSWORD);
 w.start_file("secret.txt", opts).unwrap();
 w.write_all(plaintext).unwrap();
 w.finish().unwrap().into_inner()
}

/// What a victim does: open with the correct password and read the entry.
fn victim_reads(archive: &[u8]) -> std::io::R

Affected package ranges reported by GitHub:
zip: >= 0.6.0, < 9.0.0

Scores and how they were measured

CVSS base severity

6.5

Version 3.1As of

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-8hxf-qr8p-62x4 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-8hxf-qr8p-62x4, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 29). zip: AES Entry HMAC Authentication Bypass on Premature EOF (CVE-2026-115906). https://kevwire.com/record/CVE-2026-115906

MLA

"zip: AES Entry HMAC Authentication Bypass on Premature EOF (CVE-2026-115906)." KEVWire, 29 Oct. 2026, kevwire.com/record/CVE-2026-115906.

Chicago

KEVWire. "zip: AES Entry HMAC Authentication Bypass on Premature EOF (CVE-2026-115906)." Last modified October 29, 2026. https://kevwire.com/record/CVE-2026-115906.

Evidence summary (plain text)

zip: AES Entry HMAC Authentication Bypass on Premature EOF (Oct 29, 2026).
An AES-encrypted (AE-x) entry's authentication code is verified only when the reader's byte counter lands exactly on zero. If the bounded entry reader reaches the archive's actual EOF before the attacker-controlled, header-declared compressed_size, the counter remains positive, the HMAC is never read or compared, and the truncated, unauthenticated plaintext is returned as a clean end of stream. A zero-payload entry also returns before authentication. An attacker who does not know the password can modify an archive so chosen or modified plaintext is accepted with no error.
CVSS 6.5 (version 3.1; evidence as of Oct 29, 2026; https://github.com/advisories/GHSA-8hxf-qr8p-62x4).
Sources:
GitHub-reviewed advisory GHSA-8hxf-qr8p-62x4: https://github.com/advisories/GHSA-8hxf-qr8p-62x4
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-115906.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 18:26 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail