diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index 15627da..0f49608 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -87,6 +87,13 @@ pub async fn launch_game( } all_args.extend(args.clone()); all_args.push(game_exe.to_string_lossy().to_string()); + // security: extra_args from the frontend pass through to the + // spawned game verbatim. Wine/Proton honor many flags + // influencing DLL loading, debug ports, etc. an attacker + // controlling the webview (see LCEL-01) could craft args + // that influence the spawn. (LCEL-05) + // TODO: allowlist extra_args against a known safe set + // (e.g. --server, --port). reject unknown flags. all_args.extend(extra_args.clone()); let (final_prog, final_args) = apply_launch_prefix(prog, all_args, &config_val); let mut cmd = tokio::process::Command::new(&final_prog); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2852c6..e299e3c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -129,29 +129,39 @@ pub fn run() { } let args: Vec = std::env::args().collect(); - if args.len() > 1 && !args[1].starts_with('-') { - let first = &args[1]; - let is_deep_link = first.starts_with("emerald://") - || first.starts_with("emeraldlauncher://") - || first.starts_with("discord-1482504445152460871://"); - if !is_deep_link { - let instance_id = first.clone(); - let app_handle_clone = app.handle().clone(); - tauri::async_runtime::spawn(async move { - if let Some(window) = app_handle_clone.get_webview_window("main") { - let _ = window.hide(); - } - let state = app_handle_clone.state::(); - match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await { - Ok(_) => app_handle_clone.exit(0), - Err(e) => { - let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}")); - eprintln!("Auto-launch error: {}", e); - app_handle_clone.exit(1); - } - } - }); + // security: require an explicit --launch flag for silent auto-launch. + // the old code treated any positional argv[1] (that wasn't a flag + // or deep-link) as an instance_id, hid the main window, and spawned + // the game. a crafted shortcut, scheduled task, or shell-out from + // another program could stealth-launch any installed instance. + // (LCEL-06) + let mut launch_instance: Option = None; + let mut i = 1; + while i < args.len() { + let a = &args[i]; + if a == "--launch" && i + 1 < args.len() { + launch_instance = Some(args[i + 1].clone()); + i += 2; + continue; } + i += 1; + } + if let Some(instance_id) = launch_instance { + let app_handle_clone = app.handle().clone(); + tauri::async_runtime::spawn(async move { + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + } + let state = app_handle_clone.state::(); + match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await { + Ok(_) => app_handle_clone.exit(0), + Err(e) => { + let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}")); + eprintln!("Auto-launch error: {}", e); + app_handle_clone.exit(1); + } + } + }); } Ok(()) }) diff --git a/src-tauri/src/networking/relay.rs b/src-tauri/src/networking/relay.rs index f4773c5..878010f 100644 --- a/src-tauri/src/networking/relay.rs +++ b/src-tauri/src/networking/relay.rs @@ -6,6 +6,10 @@ use tokio::time::sleep; use tokio_util::sync::CancellationToken; use crate::state::ProxyGuard; const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it +// security: cap read_line at 8 KiB. relay control lines are tiny (HOST/JOIN +// tokens + small id). a malicious peer (relay or LAN) sending an unbounded +// line without \n used to OOM the launcher. (LCEL-04) +const MAX_RELAY_LINE_BYTES: usize = 8 * 1024; async fn read_line(stream: &mut TcpStream) -> Result { let mut buf = Vec::new(); let mut byte = [0u8; 1]; @@ -13,6 +17,9 @@ async fn read_line(stream: &mut TcpStream) -> Result { stream.read_exact(&mut byte).await.map_err(|e| e.to_string())?; if byte[0] == b'\n' { break; } if byte[0] != b'\r' { buf.push(byte[0]); } + if buf.len() > MAX_RELAY_LINE_BYTES { + return Err(format!("relay line exceeded {} bytes", MAX_RELAY_LINE_BYTES)); + } } String::from_utf8(buf).map_err(|e| e.to_string()) } @@ -111,7 +118,11 @@ async fn run_relay_proxy( .map_err(|e| format!("Proxy connect failed: {}", e))?; write_line(&mut stream, &format!("JOIN {} 0 {}", auth_token, target_session)).await?; - let listener = TcpListener::bind("0.0.0.0:61000") + // security: bind to loopback only. the game process connects on + // 127.0.0.1:61000; binding 0.0.0.0 was LAN-reachable so any host on + // the user's network could intercept/inject the relayed game traffic. + // (LCEL-04) + let listener = TcpListener::bind("127.0.0.1:61000") .await .map_err(|e| format!("Bind failed: {}", e))?; let local_port = listener.local_addr().map_err(|e| e.to_string())?.port(); diff --git a/src-tauri/src/workshop_server.rs b/src-tauri/src/workshop_server.rs index c381c53..df76fd8 100644 --- a/src-tauri/src/workshop_server.rs +++ b/src-tauri/src/workshop_server.rs @@ -92,7 +92,10 @@ async fn handle(stream: tokio::net::TcpStream) { "application/octet-stream" }; let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n", + // security: scope CORS to the tauri webview origin. was + // "*" which let any website the user visits fetch the + // workshop server. (LCEL-07) + "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: http://localhost:1420\r\nConnection: close\r\n\r\n", content_type, body.len(), ); diff --git a/src/services/LceOnlineService.ts b/src/services/LceOnlineService.ts index 6a2d3c3..4ea3531 100644 --- a/src/services/LceOnlineService.ts +++ b/src/services/LceOnlineService.ts @@ -1,3 +1,9 @@ +// security: the access token currently lives in localStorage under +// SESSION_KEY, which is readable by any JS in the webview origin +// (XSS exposure). the proper fix is to move it to a Rust-side secret +// store accessed via a narrow IPC that returns only scoped capability, +// or to use an httpOnly cookie + same-site strict backend auth flow. +// (LCEL-08) const SESSION_KEY = "lceonline_session"; const SOCIAL_BASE_URL = "https://social.mclegacyedition.xyz"; const AUTH_BASE_URL = "https://auth.mclegacyedition.xyz"; //neo: yeah bro im hardcoding all three