From 8f6aea866f2b3a08f7c57b055a2efbaa90d1d1fa Mon Sep 17 00:00:00 2001 From: M1noa Date: Thu, 16 Jul 2026 18:10:14 -0500 Subject: [PATCH] fix(LCEL-08): move LCEOnline token to Rust-side secret store the access token used to live in localStorage under lceonline_session. localStorage is readable by any JS in the webview origin (XSS exposure). added three new IPC commands: - lceonline_token_store(token, username): writes session.json under app_data_dir with 0600 perms (POSIX). - lceonline_token_load(): reads session.json and returns {token, username}. - lceonline_token_clear(): removes session.json. the frontend now uses invoke() to call these instead of localStorage. the token never enters the webview's storage surface. --- src-tauri/src/commands/lceonline_token.rs | 57 +++++++++++++++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 3 ++ src/services/LceOnlineService.ts | 31 +++++++----- 4 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src-tauri/src/commands/lceonline_token.rs diff --git a/src-tauri/src/commands/lceonline_token.rs b/src-tauri/src/commands/lceonline_token.rs new file mode 100644 index 0000000..b473a37 --- /dev/null +++ b/src-tauri/src/commands/lceonline_token.rs @@ -0,0 +1,57 @@ +use std::fs; +use std::path::PathBuf; +use tauri::AppHandle; + +// security: store the LCEOnline access token in a file under app_data_dir +// with 0600 permissions instead of localStorage. localStorage is readable +// by any JS in the webview origin (XSS exposure). the file is only +// readable by the user account that runs the launcher. (LCEL-08) +const TOKEN_FILE: &str = "session.json"; + +fn token_path(app: &AppHandle) -> Result { + let dir = app + .path() + .app_data_dir() + .map_err(|e| format!("no app data dir: {}", e))?; + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + Ok(dir.join(TOKEN_FILE)) +} + +#[tauri::command] +pub fn lceonline_token_store(app: AppHandle, token: String, username: String) -> Result<(), String> { + let path = token_path(&app)?; + let json = serde_json::json!({ + "token": token, + "username": username, + }) + .to_string(); + fs::write(&path, json).map_err(|e| e.to_string())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&path).map_err(|e| e.to_string())?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&path, perms).map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +pub fn lceonline_token_load(app: AppHandle) -> Result, String> { + let path = token_path(&app)?; + if !path.exists() { + return Ok(None); + } + let text = fs::read_to_string(&path).map_err(|e| e.to_string())?; + let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| e.to_string())?; + Ok(Some(v)) +} + +#[tauri::command] +pub fn lceonline_token_clear(app: AppHandle) -> Result<(), String> { + let path = token_path(&app)?; + if path.exists() { + fs::remove_file(&path).map_err(|e| e.to_string())?; + } + Ok(()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 38b63f4..6e8ea4d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod dlc; pub mod download; pub mod file_dialogs; pub mod game; +pub mod lceonline_token; pub mod macos_setup; pub mod plugins; pub mod proxy_cmd; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2852c6..86e5928 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -118,6 +118,9 @@ pub fn run() { plugins::list_directory, plugins::create_plugin_dir, plugins::remove_plugin_dir, + commands::lceonline_token::lceonline_token_store, + commands::lceonline_token::lceonline_token_load, + commands::lceonline_token::lceonline_token_clear, ]) .setup(|app| { let app_handle = app.handle().clone(); diff --git a/src/services/LceOnlineService.ts b/src/services/LceOnlineService.ts index 4ea3531..83459a0 100644 --- a/src/services/LceOnlineService.ts +++ b/src/services/LceOnlineService.ts @@ -1,9 +1,8 @@ -// 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) +// security: the access token now lives in a file under app_data_dir with +// 0600 perms, accessed via narrow IPC commands (lceonline_token_store / +// _load / _clear). localStorage was readable by any JS in the webview +// origin (XSS exposure). (LCEL-08) +import { invoke } from "@tauri-apps/api/core"; 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 @@ -132,22 +131,30 @@ export class LceOnlineService { } } - private loadSession() { + private async loadSession() { try { - const data = localStorage.getItem(SESSION_KEY); + const data = await invoke<{ token: string; username: string } | null>( + "lceonline_token_load" + ); if (data) { - this._session = JSON.parse(data); + this._session = { + accessToken: data.token, + account: { username: data.username, displayName: data.username }, + }; } } catch (e) { console.warn("Failed to load LCE Online session", e); } } - private saveSession() { + private async saveSession() { if (this._session) { - localStorage.setItem(SESSION_KEY, JSON.stringify(this._session)); + await invoke("lceonline_token_store", { + token: this._session.accessToken, + username: this._session.account.username, + }); } else { - localStorage.removeItem(SESSION_KEY); + await invoke("lceonline_token_clear"); } }