From 74c818bef9fe9ad8518d035499075f4c2249368e Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 14 Sep 2026 10:50:08 +0800 Subject: [PATCH] Validate cursor source metadata before UI dispatch --- flutter/lib/models/model.dart | 13 +++++++------ src/client/io_loop.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ed4f00ead..0447a454d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -3494,21 +3494,22 @@ class CursorModel with ChangeNotifier { final hoty = double.parse(evt['hoty']); final width = int.parse(evt['width']); final height = int.parse(evt['height']); - // Bound physical allocation before density normalization or image decoding. - if (!_validCursorRasterSize(width.toDouble(), height.toDouble())) { + // Rust validates native packets; Web receives them directly from JavaScript. + if (isWeb && !_validCursorRasterSize(width.toDouble(), height.toDouble())) { debugPrint('Rejected cursor $id: invalid source size ${width}x$height'); return; } final pixelRatio = double.tryParse(evt['scale'] ?? '0'); - if (pixelRatio == null || !pixelRatio.isFinite || pixelRatio < 0 || - (pixelRatio > 0 && - !_validCursorRasterSize(width / pixelRatio, height / pixelRatio))) { + if (pixelRatio == null || + (isWeb && (!pixelRatio.isFinite || pixelRatio < 0 || + (pixelRatio > 0 && + !_validCursorRasterSize(width / pixelRatio, height / pixelRatio))))) { debugPrint('Rejected cursor $id: invalid pixel ratio ${evt['scale']}'); return; } List colors = json.decode(evt['colors']); const bytesPerPixel = 4; - if (colors.length != width * height * bytesPerPixel) { + if (isWeb && colors.length != width * height * bytesPerPixel) { debugPrint('Rejected cursor $id: invalid RGBA length ${colors.length}'); return; } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d4c31453e..89d3f31c3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2562,6 +2562,22 @@ fn decode_cursor_data(data: CursorData) -> hbb_common::ResultType { if !(1..=MAX_CURSOR_SIZE).contains(&cd.width) || !(1..=MAX_CURSOR_SIZE).contains(&cd.height) { bail!("invalid source size {}x{}", cd.width, cd.height); } + if !(0..cd.width).contains(&cd.hotx) || !(0..cd.height).contains(&cd.hoty) { + bail!( + "hotspot ({},{}) is outside the cursor image", + cd.hotx, + cd.hoty + ); + } + // Zero preserves legacy sizing; positive density must bound the logical image too. + if !cd.scale.is_finite() + || cd.scale < 0.0 + || (cd.scale > 0.0 + && (f64::from(cd.width) / cd.scale > f64::from(MAX_CURSOR_SIZE) + || f64::from(cd.height) / cd.scale > f64::from(MAX_CURSOR_SIZE))) + { + bail!("invalid cursor density {}", cd.scale); + } let expected = (cd.width as usize) .checked_mul(cd.height as usize) .and_then(|pixels| pixels.checked_mul(RGBA_CHANNELS))