From e2819b9ac4f6580f0d27f87adad4b6264759d8bc Mon Sep 17 00:00:00 2001 From: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co> Date: Wed, 22 Jul 2026 16:11:57 -0700 Subject: [PATCH] fix(cli): allow generic file uploads Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz> --- crates/buzz-cli/src/client.rs | 60 ++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/crates/buzz-cli/src/client.rs b/crates/buzz-cli/src/client.rs index d0dd2677a..a4b2c4cc8 100644 --- a/crates/buzz-cli/src/client.rs +++ b/crates/buzz-cli/src/client.rs @@ -75,6 +75,29 @@ const MAX_IMAGE_BYTES: u64 = 50 * 1024 * 1024; /// Maximum file size for video uploads (500 MB). const MAX_VIDEO_BYTES: u64 = 500 * 1024 * 1024; +/// Maximum file size for generic attachment uploads (100 MB). +const MAX_FILE_BYTES: u64 = 100 * 1024 * 1024; + +/// Return the client-side size cap for an upload MIME type. +/// +/// Known media must use an explicitly supported image/video format. Other +/// content is a generic attachment and is validated again by the relay. +fn upload_size_limit(mime: &str) -> Result { + let is_media = + mime.starts_with("image/") || mime.starts_with("video/") || mime.starts_with("audio/"); + if is_media && !ALLOWED_MIMES.contains(&mime) { + return Err(CliError::Usage(format!("unsupported file type: {mime}"))); + } + + if mime.starts_with("video/") { + Ok(MAX_VIDEO_BYTES) + } else if mime.starts_with("image/") { + Ok(MAX_IMAGE_BYTES) + } else { + Ok(MAX_FILE_BYTES) + } +} + /// Sign a NIP-98 HTTP auth event (kind:27235) and return the Authorization header value. /// /// The event includes: @@ -1113,16 +1136,9 @@ impl BuzzClient { .map(|t| t.mime_type().to_string()) .unwrap_or_else(|| "application/octet-stream".to_string()); - if !ALLOWED_MIMES.contains(&mime.as_str()) { - return Err(CliError::Usage(format!("unsupported file type: {mime}"))); - } - - // 3. Size check - let max = if mime.starts_with("video/") { - MAX_VIDEO_BYTES - } else { - MAX_IMAGE_BYTES - }; + // 3. Enforce media allowlist and the matching client-side size cap. + // Generic attachments are deny-listed and re-sniffed by the relay. + let max = upload_size_limit(&mime)?; if bytes.len() as u64 > max { return Err(CliError::Usage(format!( "file too large: {} bytes (max {})", @@ -2297,10 +2313,32 @@ mod retry_policy_tests { #[cfg(test)] mod tests { use super::{ - advance_query_cursor, create_response_with_id, extract_relay_response_field, BuzzClient, + advance_query_cursor, create_response_with_id, extract_relay_response_field, + upload_size_limit, BuzzClient, MAX_FILE_BYTES, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, }; use nostr::{EventBuilder, Keys, Kind, Tag}; + #[test] + fn upload_size_limit_accepts_plain_text_as_generic_attachment() { + let mime = infer::get(b"# Markdown attachment\n") + .map(|kind| kind.mime_type()) + .unwrap_or("application/octet-stream"); + assert_eq!(mime, "application/octet-stream"); + assert_eq!(upload_size_limit(mime).unwrap(), MAX_FILE_BYTES); + assert_eq!( + upload_size_limit("application/pdf").unwrap(), + MAX_FILE_BYTES + ); + } + + #[test] + fn upload_size_limit_preserves_media_allowlist_and_caps() { + assert_eq!(upload_size_limit("image/png").unwrap(), MAX_IMAGE_BYTES); + assert_eq!(upload_size_limit("video/mp4").unwrap(), MAX_VIDEO_BYTES); + assert!(upload_size_limit("image/svg+xml").is_err()); + assert!(upload_size_limit("audio/mpeg").is_err()); + } + #[test] fn query_cursor_uses_last_events_composite_sort_key() { let mut filter = serde_json::json!({"kinds": [39000], "limit": 500});