mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(relay): cap HTTP typing indicator size at 1 KiB
POST /events skips the 256 KB ingest content cap for typing indicators, so a member could post up to the HTTP body limit and have it fanned out to every subscriber. Real indicators carry empty content and at most three short tags, so reject content plus tag values above 1 KiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -202,3 +202,22 @@ async fn typing_without_channel_is_rejected() {
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres and Redis"]
|
||||
async fn oversized_typing_is_rejected() {
|
||||
let f = Fixture::new().await;
|
||||
let event = EventBuilder::new(Kind::Custom(20002), "x".repeat(2048))
|
||||
.tags([Tag::parse(["h".to_string(), f.channel.to_string()]).unwrap()])
|
||||
.sign_with_keys(&f.member)
|
||||
.unwrap();
|
||||
let (status, body) = f.post(&f.member, &event).await;
|
||||
assert_eq!(status, axum::http::StatusCode::BAD_REQUEST, "{body}");
|
||||
assert!(
|
||||
body["error"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("too large"),
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1056,6 +1056,12 @@ async fn publish_channel_ephemeral(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upper bound on content plus tag values for an HTTP typing indicator.
|
||||
/// Real indicators carry empty content and at most three short tags (`h`,
|
||||
/// root `e`, reply `e`, roughly 250 bytes). Without a cap a member could post
|
||||
/// up to the HTTP body limit and have it fanned out to every subscriber.
|
||||
const MAX_HTTP_TYPING_BYTES: usize = 1024;
|
||||
|
||||
/// Publishes a typing indicator (kind:20002) submitted through HTTP
|
||||
/// `POST /events`, for clients that sign but hold no WebSocket, such as
|
||||
/// app-hosted agents. Applies the gates the WebSocket path applies to
|
||||
@@ -1075,6 +1081,18 @@ pub(crate) async fn publish_http_typing(
|
||||
let ch_id = super::ingest::extract_channel_id(&event).ok_or_else(|| {
|
||||
IngestError::Rejected("invalid: typing indicator needs a channel UUID h tag".into())
|
||||
})?;
|
||||
let size = event.content.len()
|
||||
+ event
|
||||
.tags
|
||||
.iter()
|
||||
.flat_map(|tag| tag.as_slice())
|
||||
.map(String::len)
|
||||
.sum::<usize>();
|
||||
if size > MAX_HTTP_TYPING_BYTES {
|
||||
return Err(IngestError::Rejected(format!(
|
||||
"invalid: typing indicator too large (max {MAX_HTTP_TYPING_BYTES} bytes of content and tags)"
|
||||
)));
|
||||
}
|
||||
let event_clone = event.clone();
|
||||
match tokio::task::spawn_blocking(move || verify_event(&event_clone)).await {
|
||||
Ok(Ok(())) => {}
|
||||
|
||||
Reference in New Issue
Block a user