From 52a938297bd526294ed7576e811e263cd87f2d6f Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:36:38 +0200 Subject: [PATCH] IMAP: Discard oversized non-synchronizing literals (fixes #2768) --- CHANGELOG.md | 1 + crates/imap-proto/src/receiver.rs | 81 ++++++++++++++++++++++++++++--- crates/imap/src/op/capability.rs | 3 +- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9924a20c..8873bf90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - `SearchSnippet/get`: incorrect response structure. - `Thread/changes`: emit a container delete when a thread becomes empty. - `VacationResponse/set`: incorrect singleton handling. +- IMAP: Discard oversized non-synchronizing literals (#2768). - DANE: Improper `TLSA` record validation (#2328). - OIDC: Add default domain name to groups that are not email addresses. - RocksDB: Enable blob garbage collection to reclaim disk space from deleted blobs. diff --git a/crates/imap-proto/src/receiver.rs b/crates/imap-proto/src/receiver.rs index 0e34bca5..945003a5 100644 --- a/crates/imap-proto/src/receiver.rs +++ b/crates/imap-proto/src/receiver.rs @@ -60,6 +60,7 @@ pub enum State { Literal { non_sync: bool }, LiteralSeek { size: u32, non_sync: bool }, LiteralData { remaining: u32 }, + LiteralDiscard { remaining: u32 }, } pub struct Receiver { @@ -315,15 +316,18 @@ impl Receiver { let size = self.buf.as_str().parse::().map_err(|_| { self.error_reset("Literal size is not a valid number.") })?; - if self.current_request_size + size as usize > self.max_request_size - { + let oversize = self.current_request_size + size as usize + > self.max_request_size; + if oversize && !non_sync { return Err(self.error_reset(format_compact!( "Literal exceeds the maximum request size of {} bytes.", self.max_request_size ))); } self.state = State::LiteralSeek { size, non_sync }; - self.buf.resize_buffer(size as usize); + if !oversize { + self.buf.resize_buffer(size as usize); + } self.buf.clear(); } else { return Err(self.error_reset("Invalid empty literal.")); @@ -356,14 +360,21 @@ impl Receiver { } State::LiteralSeek { size, non_sync } => { if ch == b'\n' { - if size > 0 { + if non_sync + && self.current_request_size + size as usize > self.max_request_size + { + self.state = State::LiteralDiscard { remaining: size }; + } else if size > 0 { self.state = State::LiteralData { remaining: size }; + if !non_sync { + return Err(Error::NeedsLiteral { size }); + } } else { self.state = State::Argument { last_ch: b' ' }; self.push_token(Token::Nil)?; - } - if !non_sync { - return Err(Error::NeedsLiteral { size }); + if !non_sync { + return Err(Error::NeedsLiteral { size }); + } } } else if !ch.is_ascii_whitespace() { return Err( @@ -371,6 +382,18 @@ impl Receiver { ); } } + State::LiteralDiscard { remaining } => { + if remaining > 1 { + self.state = State::LiteralDiscard { + remaining: remaining - 1, + }; + } else { + return Err(self.error_reset(format_compact!( + "Literal exceeds the maximum request size of {} bytes.", + self.max_request_size + ))); + } + } State::LiteralData { remaining } => { // SAFETY: We checked the size before entering this state self.buf.push_unchecked(ch); @@ -1103,4 +1126,48 @@ mod tests { } } } + + #[test] + fn receiver_discard_oversized_non_sync_literal() { + for frames in [ + vec![ + "a1 APPEND inbox (\\Draft) {40+}\r\n", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n", + "b2 NOOP\r\n", + ], + vec![ + "a1 APPEND inbox (\\Draft) {40+}\r\nAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAA\r\nb2 NOOP\r\n", + ], + vec![ + "a1 APPEND inbox (\\Draft) {40+}\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\nb2 NOOP\r\n", + ], + ] { + let mut receiver = Receiver::::with_max_request_size(30); + let mut requests = Vec::new(); + let mut errors = 0; + for frame in &frames { + let mut bytes = frame.as_bytes().iter(); + loop { + match receiver.parse(&mut bytes) { + Ok(request) => requests.push(request), + Err(Error::NeedsMoreData | Error::NeedsLiteral { .. }) => break, + Err(Error::Error { .. }) => errors += 1, + } + } + } + + assert_eq!(errors, 1, "expected a single error for {:#?}", frames); + assert_eq!( + requests, + vec![Request { + tag: "b2".into(), + command: Command::Noop, + tokens: vec![], + }], + "connection did not resync for {:#?}", + frames + ); + } + } } diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index 2952747c..92aab03c 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -11,8 +11,9 @@ use common::network::SessionStream; use imap_proto::{ Command, StatusResponse, protocol::{ - ImapResponse, quoted_string, + ImapResponse, capability::{Capability, Response}, + quoted_string, }, receiver::Request, };