@@ -4,6 +4,23 @@ use crate::msg;
44
55use crate :: map_err;
66
7+ /// Require a decoded known-message payload to be fully consumed.
8+ ///
9+ /// SSH RFCs and implemented OpenSSH extensions define exact field layouts for
10+ /// known message types. Callers use this after decoding those fields so
11+ /// malformed packets with trailing payload bytes are rejected instead of being
12+ /// treated as canonical messages.
13+ pub ( crate ) fn ensure_end ( reader : & impl Reader ) -> Result < ( ) , crate :: Error > {
14+ if reader. is_finished ( ) {
15+ Ok ( ( ) )
16+ } else {
17+ Err ( ssh_encoding:: Error :: TrailingData {
18+ remaining : reader. remaining_len ( ) ,
19+ }
20+ . into ( ) )
21+ }
22+ }
23+
724#[ derive( Debug ) ]
825pub struct OpenChannelMessage {
926 pub typ : ChannelType ,
@@ -13,6 +30,12 @@ pub struct OpenChannelMessage {
1330}
1431
1532impl OpenChannelMessage {
33+ /// Parse an SSH `CHANNEL_OPEN` payload.
34+ ///
35+ /// Known channel types are parsed according to their fixed layouts and must
36+ /// not contain trailing bytes. Unknown extension channel types remain
37+ /// intentionally opaque so applications can implement extension-specific
38+ /// parsing and compatibility behavior.
1639 pub fn parse < R : Reader > ( r : & mut R ) -> Result < Self , crate :: Error > {
1740 // https://tools.ietf.org/html/rfc4254#section-5.1
1841 let typ = map_err ! ( String :: decode( r) ) ?;
@@ -21,24 +44,46 @@ impl OpenChannelMessage {
2144 let maxpacket = map_err ! ( u32 :: decode( r) ) ?;
2245
2346 let typ = match typ. as_str ( ) {
24- "session" => ChannelType :: Session ,
47+ "session" => {
48+ ensure_end ( r) ?;
49+ ChannelType :: Session
50+ }
2551 "x11" => {
2652 let originator_address = map_err ! ( String :: decode( r) ) ?;
2753 let originator_port = map_err ! ( u32 :: decode( r) ) ?;
54+ ensure_end ( r) ?;
2855 ChannelType :: X11 {
2956 originator_address,
3057 originator_port,
3158 }
3259 }
33- "direct-tcpip" => ChannelType :: DirectTcpip ( TcpChannelInfo :: decode ( r) ?) ,
60+ "direct-tcpip" => {
61+ let info = TcpChannelInfo :: decode ( r) ?;
62+ ensure_end ( r) ?;
63+ ChannelType :: DirectTcpip ( info)
64+ }
3465 "direct-streamlocal@openssh.com" => {
35- ChannelType :: DirectStreamLocal ( StreamLocalChannelInfo :: decode ( r) ?)
66+ let info = StreamLocalChannelInfo :: decode ( r) ?;
67+ String :: decode ( r) ?; // originator address/reserved
68+ u32:: decode ( r) ?; // originator port/reserved
69+ ensure_end ( r) ?;
70+ ChannelType :: DirectStreamLocal ( info)
71+ }
72+ "forwarded-tcpip" => {
73+ let info = TcpChannelInfo :: decode ( r) ?;
74+ ensure_end ( r) ?;
75+ ChannelType :: ForwardedTcpIp ( info)
3676 }
37- "forwarded-tcpip" => ChannelType :: ForwardedTcpIp ( TcpChannelInfo :: decode ( r) ?) ,
3877 "forwarded-streamlocal@openssh.com" => {
39- ChannelType :: ForwardedStreamLocal ( StreamLocalChannelInfo :: decode ( r) ?)
78+ let info = StreamLocalChannelInfo :: decode ( r) ?;
79+ String :: decode ( r) ?; // reserved
80+ ensure_end ( r) ?;
81+ ChannelType :: ForwardedStreamLocal ( info)
82+ }
83+ "auth-agent@openssh.com" => {
84+ ensure_end ( r) ?;
85+ ChannelType :: AgentForward
4086 }
41- "auth-agent@openssh.com" => ChannelType :: AgentForward ,
4287 _ => ChannelType :: Unknown { typ } ,
4388 } ;
4489
@@ -177,3 +222,51 @@ impl Decode for ChannelOpenConfirmation {
177222 } )
178223 }
179224}
225+
226+ #[ cfg( test) ]
227+ mod tests {
228+ use super :: { ChannelType , OpenChannelMessage } ;
229+ use crate :: tests:: raw_no_crypto:: { channel_open_payload, encode_string, push_u32} ;
230+
231+ #[ test]
232+ fn known_channel_open_with_trailing_bytes_is_rejected ( ) {
233+ let mut payload = channel_open_payload ( b"session" ) ;
234+ payload. push ( 0 ) ;
235+
236+ assert ! (
237+ OpenChannelMessage :: parse( & mut payload. as_slice( ) ) . is_err( ) ,
238+ "known channel-open type accepted trailing bytes"
239+ ) ;
240+ }
241+
242+ #[ test]
243+ fn unknown_channel_open_with_extra_payload_stays_permissive ( ) {
244+ let mut payload = channel_open_payload ( b"unknown@example.com" ) ;
245+ payload. extend_from_slice ( b"opaque" ) ;
246+
247+ let parsed = OpenChannelMessage :: parse ( & mut payload. as_slice ( ) )
248+ . expect ( "unknown channel-open payload should remain opaque" ) ;
249+
250+ assert ! ( matches!( parsed. typ, ChannelType :: Unknown { .. } ) ) ;
251+ }
252+
253+ #[ test]
254+ fn openssh_streamlocal_channel_open_reserved_fields_are_consumed ( ) {
255+ let mut direct = channel_open_payload ( b"direct-streamlocal@openssh.com" ) ;
256+ encode_string ( & mut direct, b"/tmp/socket" ) ;
257+ encode_string ( & mut direct, b"" ) ;
258+ push_u32 ( & mut direct, 0 ) ;
259+
260+ let parsed = OpenChannelMessage :: parse ( & mut direct. as_slice ( ) )
261+ . expect ( "direct streamlocal reserved fields should be consumed" ) ;
262+ assert ! ( matches!( parsed. typ, ChannelType :: DirectStreamLocal ( _) ) ) ;
263+
264+ let mut forwarded = channel_open_payload ( b"forwarded-streamlocal@openssh.com" ) ;
265+ encode_string ( & mut forwarded, b"/tmp/socket" ) ;
266+ encode_string ( & mut forwarded, b"" ) ;
267+
268+ let parsed = OpenChannelMessage :: parse ( & mut forwarded. as_slice ( ) )
269+ . expect ( "forwarded streamlocal reserved field should be consumed" ) ;
270+ assert ! ( matches!( parsed. typ, ChannelType :: ForwardedStreamLocal ( _) ) ) ;
271+ }
272+ }
0 commit comments