diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index dd2fa6e93e0..b90006487b2 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -1973,6 +1973,21 @@ fn validate_not_before(tag_value: &str) -> Result { Ok(value) } +/// Postgres cannot store NUL in `text`/`jsonb` columns, so an event carrying +/// `\u0000` in its content or tag values would otherwise pass validation and +/// then fail at insert time as an internal database error. +fn validate_no_nul_bytes(event: &Event) -> Result<(), &'static str> { + if event.content.contains('\0') { + return Err("content contains a NUL character"); + } + for tag in event.tags.iter() { + if tag.as_slice().iter().any(|value| value.contains('\0')) { + return Err("tag value contains a NUL character"); + } + } + Ok(()) +} + /// Validate the public tag envelope of a NIP-ER `kind:30300` event before it /// reaches NIP-33 parameterized replacement. /// @@ -2239,6 +2254,10 @@ async fn ingest_event_inner( ))); } + if let Err(e) = validate_no_nul_bytes(&event) { + return Err(IngestError::Rejected(format!("invalid: {e}"))); + } + let is_gift_wrap = kind_u32 == KIND_GIFT_WRAP; if event.pubkey != *auth.pubkey() && !is_gift_wrap { return Err(IngestError::AuthFailed( @@ -4233,6 +4252,43 @@ mod tests { } } + #[test] + fn nul_validation_rejects_content_with_nul() { + let event = make_event_with_tags(KIND_STREAM_MESSAGE, "hello\0world", &[]); + assert_eq!( + validate_no_nul_bytes(&event), + Err("content contains a NUL character") + ); + } + + #[test] + fn nul_validation_rejects_tag_value_with_nul() { + let event = make_event_with_tags(KIND_STREAM_MESSAGE, "hello", &[&["title", "a\0b"]]); + assert_eq!( + validate_no_nul_bytes(&event), + Err("tag value contains a NUL character") + ); + } + + #[test] + fn nul_validation_rejects_tag_name_with_nul() { + let event = make_event_with_tags(KIND_STREAM_MESSAGE, "hello", &[&["ti\0tle", "a"]]); + assert_eq!( + validate_no_nul_bytes(&event), + Err("tag value contains a NUL character") + ); + } + + #[test] + fn nul_validation_accepts_clean_event() { + let event = make_event_with_tags( + KIND_STREAM_MESSAGE, + "hello world", + &[&["title", "greeting"]], + ); + assert!(validate_no_nul_bytes(&event).is_ok()); + } + fn make_dummy_event() -> Event { let keys = nostr::Keys::generate(); nostr::EventBuilder::new(nostr::Kind::Custom(9), "")