From b5b33b5040b14ebdea551db4c16d253e961fe521 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 02:08:25 +0000 Subject: [PATCH] Fix publish/play/SO auth bypass when only on_frame_cb is configured When a server registered only on_frame_cb (a common frame-observation pattern), publish and play commands were accepted without on_publish_cb or on_play_cb, and AMF3 shared objects reached on_shared_object_cb without on_shared_object_auth_cb. Extend the same cross-callback guards already used for on_media_cb. Co-authored-by: Alexander Wagner --- src/session/conn.rs | 98 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/src/session/conn.rs b/src/session/conn.rs index f78c450..39d8169 100644 --- a/src/session/conn.rs +++ b/src/session/conn.rs @@ -1219,7 +1219,8 @@ impl Conn { Some(auth_cb) if !auth_cb(self.conn_id, &so) => return Ok(()), None if self.on_publish_cb.is_some() || self.on_play_cb.is_some() - || self.on_media_cb.is_some() => + || self.on_media_cb.is_some() + || self.on_frame_cb.is_some() => { return Ok(()); } @@ -1710,7 +1711,9 @@ impl Conn { ); } if self.on_publish_cb.is_none() - && (self.on_play_cb.is_some() || self.on_media_cb.is_some()) + && (self.on_play_cb.is_some() + || self.on_media_cb.is_some() + || self.on_frame_cb.is_some()) { return self.send_onstatus( 0, @@ -1822,7 +1825,9 @@ impl Conn { ); } if self.on_play_cb.is_none() - && (self.on_publish_cb.is_some() || self.on_media_cb.is_some()) + && (self.on_publish_cb.is_some() + || self.on_media_cb.is_some() + || self.on_frame_cb.is_some()) { return self.send_onstatus( 0, @@ -2742,6 +2747,46 @@ mod tests { ); } + #[test] + fn publish_rejects_frame_only_connections_when_publish_cb_missing() { + let mut conn = Conn::new(); + conn.app = "live".to_string(); + conn.current_stream = Some(Box::new(Stream::new(1))); + conn.on_frame_cb = Some(|_| {}); + + let mut publish = Buffer::with_capacity(128); + command::build_publish(&mut publish, "inject", "live").unwrap(); + conn.handle_command(publish.as_slice()).unwrap(); + assert!( + !conn.current_stream.as_ref().unwrap().is_publishing, + "frame-observing servers must not accept publish without on_publish_cb" + ); + assert!( + !conn.relay_enabled, + "relay must stay disabled when publish is rejected on a frame-only server" + ); + } + + #[test] + fn play_rejects_frame_only_connections_when_play_cb_missing() { + let mut conn = Conn::new(); + conn.app = "live".to_string(); + conn.current_stream = Some(Box::new(Stream::new(1))); + conn.on_frame_cb = Some(|_| {}); + + let mut play = Buffer::with_capacity(128); + command::build_play(&mut play, "viewer").unwrap(); + conn.handle_command(play.as_slice()).unwrap(); + assert!( + !conn.current_stream.as_ref().unwrap().is_playing, + "frame-observing servers must not accept play without on_play_cb" + ); + assert!( + !conn.relay_enabled, + "relay must stay disabled when play is rejected on a frame-only server" + ); + } + #[test] fn defer_media_relay_keeps_relay_disabled_without_publish_cb() { let mut conn = Conn::new(); @@ -4647,6 +4692,53 @@ mod tests { ); } + #[test] + fn amf3_shared_object_dropped_when_only_on_frame_cb_configured() { + use crate::message::shared_object::{SharedObjectEvent, SharedObjectEventType}; + use std::sync::{LazyLock, Mutex}; + + static SEEN: LazyLock> = LazyLock::new(|| Mutex::new(false)); + + fn record_so(_conn_id: u64, _so: &SharedObjectMessage) { + *SEEN.lock().unwrap() = true; + } + + *SEEN.lock().unwrap() = false; + let mut conn = Conn::new(); + conn.conn_id = 42; + conn.state = ConnState::AppConnected; + conn.on_frame_cb = Some(|_| {}); + conn.on_shared_object_cb = Some(record_so); + + let so = SharedObjectMessage { + name: "chat".to_string(), + version: 1, + flags: 0, + events: vec![SharedObjectEvent { + event_type: SharedObjectEventType::Change, + data: b"evil".to_vec(), + }], + }; + let mut amf_buf = Buffer::with_capacity(64); + shared_object::write(&so, &mut amf_buf).unwrap(); + let mut payload = vec![0x00]; + payload.extend_from_slice(amf_buf.as_slice()); + let msg = ChunkMessage { + csid: 3, + fmt: 0, + timestamp: 0, + msg_length: payload.len() as u32, + msg_type_id: msg_dispatch::RTMP_MSG_AMF3_SHARED_OBJECT, + msg_stream_id: 0, + is_complete: true, + }; + conn.handle_message_for_test(&msg, &payload).unwrap(); + assert!( + !*SEEN.lock().unwrap(), + "shared objects must not bypass frame-only auth configuration" + ); + } + #[test] fn amf3_shared_object_honors_on_shared_object_auth_cb() { use crate::message::shared_object::{SharedObjectEvent, SharedObjectEventType};