Skip to content

Commit 6841aa3

Browse files
committed
v0.6.1: Harden remote clipboard file transfers
Remote clipboard file downloads now validate every server response against both the requested chunk size and the remaining advertised file size before writing data to disk. Oversized or inconsistent responses abort the transfer instead of being accepted. Regression tests cover oversized responses as well as valid full-size and final short chunks. The application version and README badge are updated to 0.6.1.
1 parent 1583945 commit 6841aa3

4 files changed

Lines changed: 50 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.6.0"
10+
version = "0.6.1"
1111
edition = "2021"
1212
rust-version = "1.89"
1313
license = "AGPL-3.0-only"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</p>
1010

1111
<p align="center">
12-
<img alt="Version 0.6.0" src="https://img.shields.io/badge/version-0.6.0-2f81f7">
12+
<img alt="Version 0.6.1" src="https://img.shields.io/badge/version-0.6.1-2f81f7">
1313
<a href="LICENSE"><img alt="GNU AGPL v3.0" src="https://img.shields.io/badge/license-AGPLv3-3da639"></a>
1414
<img alt="macOS 11 or newer" src="https://img.shields.io/badge/macOS-11%2B-black">
1515
<img alt="Rust 1.89 or newer" src="https://img.shields.io/badge/Rust-1.89%2B-b7410e">

crates/rdp123-core/src/session.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
22752302
mod 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

Comments
 (0)