IMAP: Discard oversized non-synchronizing literals (fixes #2768)

This commit is contained in:
Maurus Decimus
2026-06-19 18:36:38 +02:00
parent 4fd86298fa
commit 52a938297b
3 changed files with 77 additions and 8 deletions

View File

@@ -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.

View File

@@ -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<T: CommandParser> {
@@ -315,15 +316,18 @@ impl<T: CommandParser> Receiver<T> {
let size = self.buf.as_str().parse::<u32>().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<T: CommandParser> Receiver<T> {
}
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<T: CommandParser> Receiver<T> {
);
}
}
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::<Command>::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
);
}
}
}

View File

@@ -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,
};