mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
security(desktop): IPC serial-command-injection + over-broad shell capability + ADR-178 (#1100)
* fix(security): desktop IPC serial-command-injection + over-broad shell capability (ADR-178) Beyond-SOTA security review of wifi-densepose-desktop (Tauri v2). Two real findings, each MEASURED on Windows (crate builds + tests under --no-default-features): WDP-DESK-01 (MODERATE) — serial command injection via configure_esp32_wifi. The #[tauri::command] handler concatenated webview-supplied ssid/password into newline-terminated serial commands with no validation; a \r\n let a compromised webview inject an arbitrary follow-up firmware command (reboot/erase). Added validate_wifi_credentials() enforcing WPA2 length bounds and rejecting all control characters, called fail-closed before any serial write. Pinned by 3 new tests (rejects \r\n / \n / NUL injection, rejects out-of-range, accepts valid boundaries). WDP-DESK-02 (MODERATE) — removed unused shell:allow-execute / shell:allow-open from capabilities/default.json. The Rust backend spawns processes via std::process::Command (bypassing the allowlist) and the UI only uses dialog.open; the shell perms were unused privilege granting the webview arbitrary host command execution on compromise. Regenerated capabilities.json confirms only core:default + dialog perms remain. lib tests 18 -> 21 (+3 pins), integration 21 -> 21, 0 failed. Python deterministic proof unchanged (f8e76f21...46f7a; desktop off the signal path). Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr): ADR-178 — desktop IPC injection fix + capability least-privilege Records the 2 MEASURED MODERATE fixes in feddcde9d: WDP-DESK-01 (webview ssid/password \r\n-injected arbitrary firmware serial commands → validated fail-closed) and WDP-DESK-02 (unused shell:allow-execute/open capability granted to the webview → removed). 30-command IPC surface + capability scope audited; 6 dimensions clean-with-evidence. desktop 18→21. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -4,8 +4,6 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save"
|
||||
]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"RuView default capability set","local":true,"windows":["main"],"permissions":["core:default","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save"]}}
|
||||
{"default":{"identifier":"default","description":"RuView default capability set","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open","dialog:allow-save"]}}
|
||||
@@ -430,6 +430,35 @@ fn is_esp32_compatible(vid: u16, pid: u16) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Validate WiFi credentials before they are interpolated into a
|
||||
/// newline-delimited serial command protocol.
|
||||
///
|
||||
/// The ESP32 firmware accepts line-oriented commands such as
|
||||
/// `wifi_config <ssid> <password>\r\n`. Because the SSID and password
|
||||
/// arrive from the webview (untrusted) and are concatenated directly into
|
||||
/// those command strings, a control character (`\r`, `\n`, or NUL) embedded
|
||||
/// in either field would let a malicious caller terminate the current line
|
||||
/// early and inject an arbitrary follow-up command (e.g. `reboot`, `erase`).
|
||||
///
|
||||
/// Enforce the IEEE 802.11 / WPA2 bounds and reject any control characters:
|
||||
/// - SSID: 1-32 bytes, no control characters
|
||||
/// - Password: 8-63 bytes (WPA2 PSK ASCII range), no control characters
|
||||
fn validate_wifi_credentials(ssid: &str, password: &str) -> Result<(), String> {
|
||||
if ssid.is_empty() || ssid.len() > 32 {
|
||||
return Err("SSID must be 1-32 characters".into());
|
||||
}
|
||||
if password.len() < 8 || password.len() > 63 {
|
||||
return Err("WiFi password must be 8-63 characters".into());
|
||||
}
|
||||
if ssid.chars().any(|c| c.is_control()) {
|
||||
return Err("SSID must not contain control characters".into());
|
||||
}
|
||||
if password.chars().any(|c| c.is_control()) {
|
||||
return Err("WiFi password must not contain control characters".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure WiFi credentials on an ESP32 via serial port.
|
||||
///
|
||||
/// Sends WiFi credentials to the ESP32 using a simple serial protocol.
|
||||
@@ -443,6 +472,10 @@ pub async fn configure_esp32_wifi(
|
||||
use std::io::{Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
// Reject control characters / out-of-range lengths before the credentials
|
||||
// are spliced into the line-oriented serial command protocol below.
|
||||
validate_wifi_credentials(&ssid, &password)?;
|
||||
|
||||
tracing::info!("Configuring WiFi on port: {}", port);
|
||||
|
||||
// Open serial port
|
||||
@@ -549,6 +582,37 @@ mod tests {
|
||||
assert_eq!(node.tdm_total, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_wifi_credentials_accepts_valid() {
|
||||
assert!(validate_wifi_credentials("MyNetwork", "password123").is_ok());
|
||||
// Boundary: 32-char SSID, 63-char password are allowed.
|
||||
assert!(validate_wifi_credentials(&"A".repeat(32), &"B".repeat(63)).is_ok());
|
||||
// Boundary: 8-char password (WPA2 minimum) is allowed.
|
||||
assert!(validate_wifi_credentials("net", "12345678").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_wifi_credentials_rejects_injection() {
|
||||
// Newline/CR in SSID would terminate the serial command line early and
|
||||
// let the caller inject a follow-up firmware command. Must be rejected.
|
||||
assert!(validate_wifi_credentials("net\r\nreboot", "password123").is_err());
|
||||
assert!(validate_wifi_credentials("net\ninjected", "password123").is_err());
|
||||
// Same vector via the password field.
|
||||
assert!(validate_wifi_credentials("net", "pass\r\nerase_nvs").is_err());
|
||||
// Embedded NUL.
|
||||
assert!(validate_wifi_credentials("net", "pass\0word1").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_wifi_credentials_rejects_out_of_range() {
|
||||
// Empty / over-length SSID.
|
||||
assert!(validate_wifi_credentials("", "password123").is_err());
|
||||
assert!(validate_wifi_credentials(&"A".repeat(33), "password123").is_err());
|
||||
// Too-short / too-long password (WPA2 PSK bounds).
|
||||
assert!(validate_wifi_credentials("net", "short").is_err());
|
||||
assert!(validate_wifi_credentials("net", &"B".repeat(64)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_esp32_compatible() {
|
||||
// CP2102
|
||||
|
||||
Reference in New Issue
Block a user