From d950ac1239817d17324c035e5c4769ee71fc197d Mon Sep 17 00:00:00 2001 From: Tillmann <28728469+tweidinger@users.noreply.github.com> Date: Thu, 23 May 2024 05:49:08 +0900 Subject: [PATCH] Merge pull request from GHSA-57fm-592m-34r7 * only allow tauri-initialized frames to call the ipc * dont regenerate the invoke key * return early if the invoke key does not match * add change file * wry 0.40 * update change file * trigger ci --------- Co-authored-by: Chip Reed Co-authored-by: Lucas Nogueira --- .changes/ipc-only-main-frame.md | 6 ++++ core/tauri/scripts/ipc-protocol.js | 15 +++++++-- core/tauri/src/app.rs | 8 +++++ core/tauri/src/error.rs | 13 +++++++- core/tauri/src/ipc/protocol.rs | 16 ++++++++++ core/tauri/src/lib.rs | 49 ++++++++++++++++++++++++++++++ core/tauri/src/manager/mod.rs | 10 ++++++ core/tauri/src/manager/webview.rs | 10 ++++++ core/tauri/src/webview/mod.rs | 19 ++++++++++++ 9 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 .changes/ipc-only-main-frame.md diff --git a/.changes/ipc-only-main-frame.md b/.changes/ipc-only-main-frame.md new file mode 100644 index 000000000..52e04c4eb --- /dev/null +++ b/.changes/ipc-only-main-frame.md @@ -0,0 +1,6 @@ +--- +'tauri': patch:sec +'tauri-runtime-wry': patch:sec +--- + +Only process IPC commands from the main frame. diff --git a/core/tauri/scripts/ipc-protocol.js b/core/tauri/scripts/ipc-protocol.js index 7c18b124d..ce89b92e0 100644 --- a/core/tauri/scripts/ipc-protocol.js +++ b/core/tauri/scripts/ipc-protocol.js @@ -2,7 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -;(function () { +;(function() { + /** + * A runtime generated key to ensure an IPC call comes from an initialized frame. + * + * This is declared outside the `window.__TAURI_INVOKE__` definition to prevent + * the key from being leaked by `window.__TAURI_INVOKE__.toString()`. + * @var {string} __TEMPLATE_invoke_key__ + */ + const __TAURI_INVOKE_KEY__ = __TEMPLATE_invoke_key__ + const processIpcMessage = __RAW_process_ipc_message_fn__ const osName = __TEMPLATE_os_name__ const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__ @@ -29,6 +38,7 @@ 'Content-Type': contentType, 'Tauri-Callback': callback, 'Tauri-Error': error, + 'Tauri-Invoke-Key': __TAURI_INVOKE_KEY__, ...((options && options.headers) || {}) } }) @@ -66,7 +76,8 @@ callback, error, options, - payload + payload, + __TAURI_INVOKE_KEY__ }) window.ipc.postMessage(data) } diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index f4cb654f7..39aec0cf0 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -1097,6 +1097,8 @@ pub struct Builder { /// The device event filter. device_event_filter: DeviceEventFilter, + + invoke_key: String, } #[derive(Template)] @@ -1108,6 +1110,7 @@ struct InvokeInitializationScript<'a> { os_name: &'a str, fetch_channel_data_command: &'a str, linux_ipc_protocol_enabled: bool, + invoke_key: &'a str, } /// Make `Wry` the default `Runtime` for `Builder` @@ -1130,6 +1133,8 @@ impl Default for Builder { impl Builder { /// Creates a new App builder. pub fn new() -> Self { + let invoke_key = crate::generate_invoke_key().unwrap(); + Self { #[cfg(any(windows, target_os = "linux"))] runtime_any_thread: false, @@ -1141,6 +1146,7 @@ impl Builder { os_name: std::env::consts::OS, fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, linux_ipc_protocol_enabled: cfg!(feature = "linux-ipc-protocol"), + invoke_key: &invoke_key.clone(), } .render_default(&Default::default()) .unwrap() @@ -1155,6 +1161,7 @@ impl Builder { window_event_listeners: Vec::new(), webview_event_listeners: Vec::new(), device_event_filter: Default::default(), + invoke_key, } } } @@ -1622,6 +1629,7 @@ tauri::Builder::default() #[cfg(desktop)] HashMap::new(), (self.invoke_responder, self.invoke_initialization_script), + self.invoke_key, )); let runtime_args = RuntimeInitArgs { diff --git a/core/tauri/src/error.rs b/core/tauri/src/error.rs index 3779076f0..0943c3908 100644 --- a/core/tauri/src/error.rs +++ b/core/tauri/src/error.rs @@ -151,10 +151,21 @@ pub enum Error { /// Failed to deserialize scope object. #[error("error deserializing scope: {0}")] CannotDeserializeScope(Box), - /// Failed to get a raw handle. #[error(transparent)] RawHandleError(#[from] raw_window_handle::HandleError), + /// Something went wrong with the CSPRNG. + #[error("unable to generate random bytes from the operating system: {0}")] + Csprng(getrandom::Error), + /// Bad `__TAURI_INVOKE_KEY__` value received in ipc message. + #[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")] + InvokeKey, +} + +impl From for Error { + fn from(value: getrandom::Error) -> Self { + Self::Csprng(value) + } } /// `Result` diff --git a/core/tauri/src/ipc/protocol.rs b/core/tauri/src/ipc/protocol.rs index 17ca5ca4c..3d06ecd44 100644 --- a/core/tauri/src/ipc/protocol.rs +++ b/core/tauri/src/ipc/protocol.rs @@ -19,6 +19,7 @@ use super::{CallbackFn, InvokeBody, InvokeResponse}; const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; +const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key"; pub fn message_handler( manager: Arc>, @@ -210,6 +211,8 @@ fn handle_ipc_message(request: Request, manager: &AppManager error: CallbackFn, payload: serde_json::Value, options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, } #[allow(unused_mut)] @@ -224,6 +227,8 @@ fn handle_ipc_message(request: Request, manager: &AppManager error: CallbackFn, payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>, options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, } if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern { @@ -240,6 +245,7 @@ fn handle_ipc_message(request: Request, manager: &AppManager error: message.error, payload: serde_json::from_slice(&crypto_keys.decrypt(message.payload)?)?, options: message.options, + invoke_key: message.invoke_key, }) }), ); @@ -261,6 +267,7 @@ fn handle_ipc_message(request: Request, manager: &AppManager url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"), body: message.payload.into(), headers: message.options.map(|o| o.headers.0).unwrap_or_default(), + invoke_key: message.invoke_key, }; #[cfg(feature = "tracing")] @@ -394,6 +401,14 @@ fn parse_invoke_request( } } + let invoke_key = parts + .headers + .get(TAURI_INVOKE_KEY_HEADER_NAME) + .ok_or("missing Tauri-Invoke-Key header")? + .to_str() + .map_err(|_| "Tauri invoke key header value must be a string")? + .to_owned(); + let url = Url::parse( parts .headers @@ -461,6 +476,7 @@ fn parse_invoke_request( url, body, headers: parts.headers, + invoke_key, }; Ok(payload) diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index 43145590b..cc77c9ae2 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -1092,3 +1092,52 @@ mod test_utils { } } } + +/// Simple dependency-free string encoder using [Z85]. +mod z85 { + const TABLE: &[u8; 85] = + b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#"; + + /// Encode bytes with [Z85]. + /// + /// # Panics + /// + /// Will panic if the input bytes are not a multiple of 4. + pub fn encode(bytes: &[u8]) -> String { + assert_eq!(bytes.len() % 4, 0); + + let mut buf = String::with_capacity(bytes.len() * 5 / 4); + for chunk in bytes.chunks_exact(4) { + let mut chars = [0u8; 5]; + let mut chunk = u32::from_be_bytes(chunk.try_into().unwrap()) as usize; + for byte in chars.iter_mut().rev() { + *byte = TABLE[chunk % 85]; + chunk /= 85; + } + + buf.push_str(std::str::from_utf8(&chars).unwrap()); + } + + buf + } + + #[cfg(test)] + mod tests { + #[test] + fn encode() { + assert_eq!( + super::encode(&[0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B]), + "HelloWorld" + ); + } + } +} + +/// Generate a random 128-bit [Z85] encoded [`String`]. +/// +/// [Z85]: https://rfc.zeromq.org/spec/32/ +pub(crate) fn generate_invoke_key() -> Result { + let mut bytes = [0u8; 16]; + getrandom::getrandom(&mut bytes)?; + Ok(z85::encode(&bytes)) +} diff --git a/core/tauri/src/manager/mod.rs b/core/tauri/src/manager/mod.rs index 283656826..d6335c70d 100644 --- a/core/tauri/src/manager/mod.rs +++ b/core/tauri/src/manager/mod.rs @@ -193,6 +193,9 @@ pub struct AppManager { /// Application Resources Table pub(crate) resources_table: Arc>, + + /// Runtime-generated invoke key. + pub(crate) invoke_key: String, } impl fmt::Debug for AppManager { @@ -232,6 +235,7 @@ impl AppManager { crate::app::GlobalMenuEventListener>, >, (invoke_responder, invoke_initialization_script): (Option>>, String), + invoke_key: String, ) -> Self { // generate a random isolation key at runtime #[cfg(feature = "isolation")] @@ -254,6 +258,7 @@ impl AppManager { event_listeners: Arc::new(webiew_event_listeners), invoke_responder, invoke_initialization_script, + invoke_key: invoke_key.clone(), }, #[cfg(all(desktop, feature = "tray-icon"))] tray: tray::TrayManager { @@ -279,6 +284,7 @@ impl AppManager { pattern: Arc::new(context.pattern), plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts), resources_table: Arc::default(), + invoke_key, } } @@ -570,6 +576,10 @@ impl AppManager { .lock() .expect("poisoned window manager") } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } } #[cfg(desktop)] diff --git a/core/tauri/src/manager/webview.rs b/core/tauri/src/manager/webview.rs index f1bd79033..5f138c0f6 100644 --- a/core/tauri/src/manager/webview.rs +++ b/core/tauri/src/manager/webview.rs @@ -89,6 +89,9 @@ pub struct WebviewManager { pub invoke_responder: Option>>, /// The script that initializes the invoke system. pub invoke_initialization_script: String, + + /// A runtime generated invoke key. + pub(crate) invoke_key: String, } impl fmt::Debug for WebviewManager { @@ -98,6 +101,7 @@ impl fmt::Debug for WebviewManager { "invoke_initialization_script", &self.invoke_initialization_script, ) + .field("invoke_key", &self.invoke_key) .finish() } } @@ -371,6 +375,7 @@ impl WebviewManager { #[default_template("../../scripts/core.js")] struct CoreJavascript<'a> { os_name: &'a str, + invoke_key: &'a str, } let bundle_script = if with_global_tauri { @@ -391,6 +396,7 @@ impl WebviewManager { bundle_script, core_script: &CoreJavascript { os_name: std::env::consts::OS, + invoke_key: self.invoke_key(), } .render_default(&Default::default())? .into_string(), @@ -660,6 +666,10 @@ impl WebviewManager { pub fn labels(&self) -> HashSet { self.webviews_lock().keys().cloned().collect() } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } } impl Webview { diff --git a/core/tauri/src/webview/mod.rs b/core/tauri/src/webview/mod.rs index f5289d219..1a3791a89 100644 --- a/core/tauri/src/webview/mod.rs +++ b/core/tauri/src/webview/mod.rs @@ -124,6 +124,7 @@ pub struct InvokeRequest { pub body: InvokeBody, /// The request headers. pub headers: HeaderMap, + pub(crate) invoke_key: String, } /// The platform webview handle. Accessed with [`Webview#method.with_webview`]; @@ -1132,6 +1133,24 @@ fn main() { let manager = self.manager_owned(); let is_local = self.is_local_url(&request.url); + // ensure the passed key matches what our manager should have injected + let expected = manager.invoke_key(); + if request.invoke_key != expected { + #[cfg(feature = "tracing")] + tracing::error!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + #[cfg(not(feature = "tracing"))] + eprintln!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + return; + } + let custom_responder = self.manager().webview.invoke_responder.clone(); let resolver = InvokeResolver::new(