fix(LCEL-05): document extra_args passed verbatim to game

This commit is contained in:
M1noa 2026-07-16 20:27:34 -05:00
parent 8da6a217bc
commit 543a5890c3
5 changed files with 61 additions and 24 deletions

View file

@ -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);

View file

@ -129,29 +129,39 @@ pub fn run() {
}
let args: Vec<String> = 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::<GameState>();
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<String> = 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::<GameState>();
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(())
})

View file

@ -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<String, String> {
let mut buf = Vec::new();
let mut byte = [0u8; 1];
@ -13,6 +17,9 @@ async fn read_line(stream: &mut TcpStream) -> Result<String, String> {
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();

View file

@ -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(),
);

View file

@ -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