-
-
Notifications
You must be signed in to change notification settings - Fork 219
Description
Expected Behavior
When MessageStream::aggregate_continuations() is used, empty continuation frames (0-byte payload) should not be
stored in the aggregation buffer. A remote client should not be able to cause unbounded memory growth (DoS) by sending
an unbounded stream of empty continuations.
Current Behavior
When MessageStream::aggregate_continuations() is used, AggregatedMessageStream stores each continuation payload in
continuations: Vec<Bytes>, while the size check is based on summing bytes.len() into current_size.
Since 0-byte continuation frames are accepted and passed through as empty Bytes, a client can send an unbounded
stream of empty continuation frames (FIN=0) without increasing current_size. As a result, max_continuation_size is
never hit and the continuations Vec grows until the connection closes.
This happens even though the total aggregated payload size remains 0 bytes: each empty continuation still results in a
Bytes being pushed into the Vec, so memory grows due to Vec element metadata and reallocations.
If the client never sends the terminating continuation frame (FIN=1 / Item::Last), the aggregated message is never
completed and continuations is never cleared, so growth continues for the lifetime of the connection.
Possible Solution
There are a few ways to address this (count empty frames toward the size limit, add a fragment/empty-frame limit, add
an assembly timeout, etc.).
To avoid changing the public API, I suggest the smallest change first: do not store 0-byte continuation payloads in the aggregation buffer, since they add no message content.
actix-ws/src/aggregated.rs:120-176(AggregatedMessageStream::poll_next)- Apply to the
this.continuations.push(bytes)call in:Item::FirstText,Item::FirstBinary,Item::Continue,
Item::Last
- Apply to the
Item::FirstText(bytes) => {
this.continuation_kind = ContinuationKind::Text;
this.current_size += bytes.len();
if this.current_size > this.max_size {
this.continuations.clear();
return size_error();
}
// Empty continuation frames add no content.
if !bytes.is_empty() {
this.continuations.push(bytes);
}
continue;
}This should prevent unbounded memory growth from empty continuations while keeping the aggregated message content the
same
(0-byte frames contribute no bytes). It does not address per-frame CPU overhead from extremely high frame counts.
Steps to Reproduce (for bugs)
-
Run a minimal server that enables continuation aggregation (and keep the returned
Sessionalive):async fn ws(req: HttpRequest, body: web::Payload) -> Result<HttpResponse, Error> { let (res, session, msg_stream) = actix_ws::handle(&req, body)?; let mut agg_stream = msg_stream.aggregate_continuations(); actix_web::rt::spawn(async move { let _session = session; // keep connection alive while let Some(_item) = agg_stream.recv().await {} }); Ok(res) }
-
Connect using a custom WebSocket client (not a browser client).
-
Send a fragmented message start frame with FIN=0 and payload length 0, then send an unbounded stream of
continuation frames with FIN=0 and payload length 0, and never send the terminating continuation (FIN=1 /
Last).Example PoC snippet (tungstenite / tokio-tungstenite):
let (mut ws, _resp) = connect_async("ws://127.0.0.1:8080/ws").await?; let first = Frame::message(vec![b'A'], OpCode::Data(Data::Text), false); ws.send(Message::Frame(first)).await?; loop { let cont = Frame::message(Vec::new(), OpCode::Data(Data::Continue), false); ws.send(Message::Frame(cont)).await?; }
-
Observe the server process memory increasing over time while the connection stays open.
Context
This is remotely triggerable when an application enables MessageStream::aggregate_continuations(). A client that can
send raw WebSocket frames can keep a fragmented message open indefinitely (by never sending FIN=1 / Last) while
sending empty continuation frames that cause the server to grow memory over the lifetime of the connection. With enough
connections, this can lead to a DoS.
Browsers typically won’t generate continuation frames, but a custom client (or proxy/fuzzer) can.
The suggested fix is intentionally small and avoids public API changes: ignore empty continuation payloads when
building
the aggregation buffer. This prevents memory growth from empty frames (though very high frame counts can still cause
per-frame CPU overhead).
Your Environment
- rustc 1.91.1
- actix-web 4.12.1
- actix-ws 0.3.1