@@ -248,8 +248,30 @@ struct RemoteClipboard {
248248 data_id : Option < u32 > ,
249249 jobs : std:: collections:: VecDeque < FetchJob > ,
250250 next_stream_id : u32 ,
251- /// Outstanding request: (stream id, was a SIZE request).
252- outstanding : Option < ( u32 , bool ) > ,
251+ /// Outstanding request: (stream id, was a SIZE request, requested bytes).
252+ outstanding : Option < ( u32 , bool , u32 ) > ,
253+ }
254+
255+ /// Reject a server response that exceeds either the requested range or the
256+ /// remaining size advertised for the remote file.
257+ fn validate_remote_file_range (
258+ requested_size : u32 ,
259+ remaining_size : u64 ,
260+ response_len : usize ,
261+ ) -> Result < ( ) , String > {
262+ let response_len = u64:: try_from ( response_len)
263+ . map_err ( |_| "remote file response length does not fit in u64" . to_string ( ) ) ?;
264+ if response_len > u64:: from ( requested_size) {
265+ return Err ( format ! (
266+ "remote returned {response_len} bytes for a {requested_size}-byte request"
267+ ) ) ;
268+ }
269+ if response_len > remaining_size {
270+ return Err ( format ! (
271+ "remote returned {response_len} bytes with only {remaining_size} bytes remaining"
272+ ) ) ;
273+ }
274+ Ok ( ( ) )
253275}
254276
255277/// Plan the entries for one pasted top-level item. Wire names containing
@@ -346,7 +368,7 @@ async fn advance_remote_fetch(
346368 requested_size : requested,
347369 data_id,
348370 } ;
349- remote. outstanding = Some ( ( stream_id, was_size) ) ;
371+ remote. outstanding = Some ( ( stream_id, was_size, requested ) ) ;
350372 return send_cliprdr ( active_stage, out_tx, |c| c. request_file_contents ( request) ) . await ;
351373 }
352374
@@ -404,7 +426,7 @@ async fn handle_remote_file_contents(
404426) -> Result < ( ) > {
405427 use std:: io:: Write as _;
406428
407- let Some ( ( expected_id, was_size) ) = remote. outstanding else {
429+ let Some ( ( expected_id, was_size, requested_size ) ) = remote. outstanding else {
408430 return Ok ( ( ) ) ; // stale response after a failed/cancelled job
409431 } ;
410432 if stream_id != expected_id {
@@ -430,6 +452,11 @@ async fn handle_remote_file_contents(
430452 if bytes. is_empty ( ) {
431453 return Err ( "transfer ended early" . to_string ( ) ) ;
432454 }
455+ validate_remote_file_range (
456+ requested_size,
457+ current. size . saturating_sub ( current. offset ) ,
458+ bytes. len ( ) ,
459+ ) ?;
433460 current
434461 . file
435462 . write_all ( & bytes)
@@ -2275,7 +2302,8 @@ fn normalize_clipboard_to_crlf(text: &str) -> String {
22752302mod tests {
22762303 use super :: {
22772304 build_config, mdns_fallback_hostname, normalize_clipboard_to_crlf, resolve_pending_command,
2278- update_keys_down, InputEvent , PendingCommands , SessionCommand , SessionConfig ,
2305+ update_keys_down, validate_remote_file_range, InputEvent , PendingCommands , SessionCommand ,
2306+ SessionConfig ,
22792307 } ;
22802308 use crate :: profile:: { AudioMode , AuthenticationMode , ClipboardMode , GraphicsMode } ;
22812309 use ironrdp:: pdu:: rdp:: client_info:: CompressionType ;
@@ -2319,6 +2347,18 @@ mod tests {
23192347 ) ;
23202348 }
23212349
2350+ #[ test]
2351+ fn remote_file_range_rejects_more_data_than_requested_or_remaining ( ) {
2352+ assert ! ( validate_remote_file_range( 1_048_576 , 2_000_000 , 1_048_577 ) . is_err( ) ) ;
2353+ assert ! ( validate_remote_file_range( 1_048_576 , 12 , 13 ) . is_err( ) ) ;
2354+ }
2355+
2356+ #[ test]
2357+ fn remote_file_range_accepts_full_and_short_final_chunks ( ) {
2358+ assert ! ( validate_remote_file_range( 1_048_576 , 2_000_000 , 1_048_576 ) . is_ok( ) ) ;
2359+ assert ! ( validate_remote_file_range( 1_048_576 , 12 , 12 ) . is_ok( ) ) ;
2360+ }
2361+
23222362 #[ test]
23232363 fn single_label_hosts_get_an_mdns_fallback ( ) {
23242364 assert_eq ! (
0 commit comments