CVE-2024-35222
Description
Tauri is a framework for building binaries for all major desktop platforms. Remote origin iFrames in Tauri applications can access the Tauri IPC endpoints without being explicitly allowed in the dangerousRemoteDomainIpcAccess in v1 and in the capabilities in v2. Valid commands with potentially unwanted consequences ("delete project", "transfer credits", etc.) could be invoked by an attacker that controls the content of an iframe running inside a Tauri app. This vulnerability has been patched in versions 1.6.7 and 2.0.0-beta.19.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tauricrates.io | < 1.6.7 | 1.6.7 |
tauricrates.io | >= 2.0.0-beta.0, < 2.0.0-beta.20 | 2.0.0-beta.20 |
Patches
2f6d81dfe0871Merge pull request from GHSA-57fm-592m-34r7
14 files changed · +227 −96
Cargo.lock+3 −2 modified@@ -4036,6 +4036,7 @@ dependencies = [ "encoding_rs", "flate2", "futures-util", + "getrandom 0.2.14", "glib", "glob", "gtk", @@ -5515,9 +5516,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.24.8" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04e72739ee84a218e3dbf8625888eadc874285637003ed21ab96a1bbbb538ec" +checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45" dependencies = [ "base64 0.13.1", "block",
.changes/ipc-only-main-frame.md+6 −0 added@@ -0,0 +1,6 @@ +--- +"tauri": patch:sec +"tauri-runtime-wry": patch:sec +--- + +Only process IPC commands from the main frame.
core/tauri/Cargo.toml+1 −0 modified@@ -96,6 +96,7 @@ encoding_rs = "0.8.31" sys-locale = { version = "0.2.3", optional = true } tracing = { version = "0.1", optional = true } indexmap = { version = "1", features = [ "std", "serde" ], optional = true } +getrandom = { version = "0.2", features = [ "std" ] } [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] rfd = { version = "0.10", optional = true, features = [ "gtk3", "common-controls-v6" ] }
core/tauri-runtime-wry/Cargo.toml+1 −1 modified@@ -13,7 +13,7 @@ exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" [dependencies] -wry = { version = "0.24.6", default-features = false, features = [ "file-drop", "protocol" ] } +wry = { version = "0.24.10", default-features = false, features = [ "file-drop", "protocol" ] } tauri-runtime = { version = "0.14.3", path = "../tauri-runtime" } tauri-utils = { version = "1.5.4", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] }
core/tauri/scripts/init.js+8 −14 modified@@ -2,31 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -; (function () { +;(function() { __RAW_freeze_prototype__ - ; (function () { - __RAW_hotkeys__ - })() + ;(function() { + __RAW_hotkeys__ + })() __RAW_pattern_script__ __RAW_ipc_script__ - ; (function () { - __RAW_bundle_script__ - })() + ;(function() { + __RAW_bundle_script__ + })() __RAW_listen_function__ __RAW_core_script__ __RAW_event_initialization_script__ - if (window.ipc) { - window.__TAURI_INVOKE__('__initialized', { url: window.location.href }) - } else { - window.addEventListener('DOMContentLoaded', function () { - window.__TAURI_INVOKE__('__initialized', { url: window.location.href }) - }) - } + window.__TAURI_INVOKE__('__initialized', { url: window.location.href }) })()
core/tauri/scripts/ipc.js+11 −1 modified@@ -7,7 +7,7 @@ */ ; -(function () { +(function() { /** * @type {string} */ @@ -18,6 +18,15 @@ */ const isolationOrigin = __TEMPLATE_isolation_origin__ + /** + * 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__ + /** * @type {{queue: object[], ready: boolean, frame: HTMLElement | null}} */ @@ -85,6 +94,7 @@ Object.defineProperty(window, '__TAURI_IPC__', { // todo: JSDoc this function value: Object.freeze((message) => { + message.__TAURI_INVOKE_KEY__ = __TAURI_INVOKE_KEY__ switch (pattern) { case 'brownfield': window.__TAURI_POST_MESSAGE__(message)
core/tauri/src/app.rs+1 −0 modified@@ -1569,6 +1569,7 @@ impl<R: Runtime> Builder<R> { self.window_event_listeners, (self.menu, self.menu_event_listeners), (self.invoke_responder, self.invoke_initialization_script), + crate::generate_invoke_key()?, ); let http_scheme = manager.config().tauri.security.dangerous_use_http_scheme;
core/tauri/src/error.rs+6 −0 modified@@ -131,6 +131,12 @@ pub enum Error { /// The Window's raw handle is invalid for the platform. #[error("Unexpected `raw_window_handle` for the current platform")] InvalidWindowHandle, + /// Something went wrong with the CSPRNG. + #[error("unable to generate random bytes from the operating system: {0}")] + Csprng(#[from] getrandom::Error), + /// Bad `__TAURI_INVOKE_KEY__` value received in ipc message. + #[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")] + InvokeKey, } pub(crate) fn into_anyhow<T: std::fmt::Display>(err: T) -> anyhow::Error {
core/tauri/src/hooks.rs+5 −0 modified@@ -35,6 +35,7 @@ pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'sta #[default_template("../scripts/ipc.js")] pub(crate) struct IpcJavascript<'a> { pub(crate) isolation_origin: &'a str, + pub(crate) invoke_key: &'a str, } #[cfg(feature = "isolation")] @@ -66,6 +67,10 @@ pub struct InvokePayload { #[serde(rename = "__tauriModule")] #[doc(hidden)] pub tauri_module: Option<String>, + /// A secret key that only Tauri initialized frames have. + #[serde(rename = "__TAURI_INVOKE_KEY__")] + #[doc(hidden)] + pub invoke_key: Option<String>, /// The success callback. pub callback: CallbackFn, /// The error callback.
core/tauri/src/lib.rs+49 −0 modified@@ -1091,3 +1091,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<String> { + let mut bytes = [0u8; 16]; + getrandom::getrandom(&mut bytes)?; + Ok(z85::encode(&bytes)) +}
core/tauri/src/manager.rs+11 −0 modified@@ -238,6 +238,8 @@ pub struct InnerWindowManager<R: Runtime> { invoke_initialization_script: String, /// Application pattern. pub(crate) pattern: Pattern, + /// A runtime generated key to ensure an IPC call comes from an initialized frame. + invoke_key: String, } impl<R: Runtime> fmt::Debug for InnerWindowManager<R> { @@ -252,6 +254,7 @@ impl<R: Runtime> fmt::Debug for InnerWindowManager<R> { .field("package_info", &self.package_info) .field("menu", &self.menu) .field("pattern", &self.pattern) + .field("invoke_key", &self.invoke_key) .finish() } } @@ -303,6 +306,7 @@ impl<R: Runtime> WindowManager<R> { window_event_listeners: Vec<GlobalWindowEventListener<R>>, (menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>), (invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String), + invoke_key: String, ) -> Self { // generate a random isolation key at runtime #[cfg(feature = "isolation")] @@ -333,6 +337,7 @@ impl<R: Runtime> WindowManager<R> { window_event_listeners: Arc::new(window_event_listeners), invoke_responder, invoke_initialization_script, + invoke_key, }), } } @@ -449,6 +454,7 @@ impl<R: Runtime> WindowManager<R> { } _ => "".to_string(), }, + invoke_key: self.invoke_key(), } .render_default(&Default::default())?; @@ -896,6 +902,10 @@ impl<R: Runtime> WindowManager<R> { listeners = self.event_listeners_object_name() ) } + + pub(crate) fn invoke_key(&self) -> &str { + &self.inner.invoke_key + } } #[cfg(test)] @@ -917,6 +927,7 @@ mod test { Default::default(), Default::default(), (std::sync::Arc::new(|_, _, _, _| ()), "".into()), + crate::generate_invoke_key().unwrap(), ); #[cfg(custom_protocol)]
core/tauri/src/scope/ipc.rs+34 −19 modified@@ -171,6 +171,7 @@ impl Scope { #[cfg(test)] mod tests { use super::RemoteDomainAccessScope; + use crate::sealed::ManagerBase; use crate::{ api::ipc::CallbackFn, test::{assert_ipc_response, mock_app, MockRuntime}, @@ -190,7 +191,7 @@ mod tests { (app, window) } - fn app_version_payload() -> InvokePayload { + fn app_version_payload(invoke_key: &str) -> InvokePayload { let callback = CallbackFn(0); let error = CallbackFn(1); @@ -208,10 +209,11 @@ mod tests { callback, error, inner: serde_json::Value::Object(payload), + invoke_key: Some(invoke_key.into()), } } - fn plugin_test_payload() -> InvokePayload { + fn plugin_test_payload(invoke_key: &str) -> InvokePayload { let callback = CallbackFn(0); let error = CallbackFn(1); @@ -221,19 +223,25 @@ mod tests { callback, error, inner: Default::default(), + invoke_key: Some(invoke_key.into()), } } + fn invoke_key(app: &App<MockRuntime>) -> &str { + app.manager().invoke_key() + } + #[test] fn scope_not_defined() { - let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("app.tauri.app") + let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("app.tauri.app") .add_window("other") .enable_tauri_api()]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(&crate::window::ipc_scope_not_found_error_message( "main", "https://tauri.app/", @@ -243,28 +251,30 @@ mod tests { #[test] fn scope_not_defined_for_window() { - let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") + let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") .add_window("second") .enable_tauri_api()]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(&crate::window::ipc_scope_window_error_message("main")), ); } #[test] fn scope_not_defined_for_url() { - let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("github.com") + let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("github.com") .add_window("main") .enable_tauri_api()]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(&crate::window::ipc_scope_domain_error_message( "https://tauri.app/", )), @@ -281,18 +291,19 @@ mod tests { .add_window("main") .enable_tauri_api(), ]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Ok(app.package_info().version.to_string().as_str()), ); window.navigate("https://blog.tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(&crate::window::ipc_scope_domain_error_message( "https://blog.tauri.app/", )), @@ -301,15 +312,15 @@ mod tests { window.navigate("https://sub.tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Ok(app.package_info().version.to_string().as_str()), ); window.window.label = "test".into(); window.navigate("https://dev.tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(&crate::window::ipc_scope_not_found_error_message( "test", "https://dev.tauri.app/", @@ -322,53 +333,57 @@ mod tests { let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") .add_window("main") .enable_tauri_api()]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app/inner/path".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Ok(app.package_info().version.to_string().as_str()), ); } #[test] fn tauri_api_not_allowed() { - let (_app, mut window) = test_context(vec![ + let (app, mut window) = test_context(vec![ RemoteDomainAccessScope::new("tauri.app").add_window("main") ]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + app_version_payload(invoke_key), Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW), ); } #[test] fn plugin_allowed() { - let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") + let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") .add_window("main") .add_plugin(PLUGIN_NAME)]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - plugin_test_payload(), + plugin_test_payload(invoke_key), Err(&format!("plugin {PLUGIN_NAME} not found")), ); } #[test] fn plugin_not_allowed() { - let (_app, mut window) = test_context(vec![ + let (app, mut window) = test_context(vec![ RemoteDomainAccessScope::new("tauri.app").add_window("main") ]); + let invoke_key = invoke_key(&app); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - plugin_test_payload(), + plugin_test_payload(invoke_key), Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW), ); }
core/tauri/src/window.rs+71 −46 modified@@ -31,8 +31,8 @@ use crate::{ sealed::ManagerBase, sealed::RuntimeOrDispatch, utils::config::{WindowConfig, WindowUrl}, - CursorIcon, EventLoopMessage, Icon, Invoke, InvokeError, InvokeMessage, InvokeResolver, Manager, - PageLoadPayload, Runtime, Theme, WindowEvent, + CursorIcon, Error, EventLoopMessage, Icon, Invoke, InvokeError, InvokeMessage, InvokeResolver, + Manager, Runtime, Theme, WindowEvent, }; use serde::Serialize; @@ -1550,9 +1550,35 @@ impl<R: Runtime> Window<R> { self.current_url = url; } + #[cfg_attr(feature = "tracing", tracing::instrument("window::on_message"))] /// Handles this window receiving an [`InvokeMessage`]. pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> { let manager = self.manager.clone(); + + // ensure the passed key matches what our manager should have injected + let expected = manager.invoke_key(); + match payload.invoke_key.as_deref() { + Some(sent) if sent == expected => { /* good */ } + Some(sent) => { + #[cfg(feature = "tracing")] + tracing::error!("__TAURI_INVOKE_KEY__ expected {expected} but received {sent}"); + + #[cfg(not(feature = "tracing"))] + eprintln!("__TAURI_INVOKE_KEY__ expected {expected} but received {sent}"); + + return Err(Error::InvokeKey); + } + None => { + #[cfg(feature = "tracing")] + tracing::error!("received ipc message without a __TAURI_INVOKE_KEY__"); + + #[cfg(not(feature = "tracing"))] + eprintln!("received ipc message without a __TAURI_INVOKE_KEY__"); + + return Err(Error::InvokeKey); + } + } + let current_url = self.url(); let config_url = manager.get_url(); let is_local = config_url.make_relative(¤t_url).is_some(); @@ -1574,54 +1600,53 @@ impl<R: Runtime> Window<R> { } } }; - match payload.cmd.as_str() { - "__initialized" => { - let payload: PageLoadPayload = serde_json::from_value(payload.inner)?; - manager.run_on_page_load(self, payload); + + if "__initialized" == &payload.cmd { + let payload = serde_json::from_value(payload.inner)?; + manager.run_on_page_load(self, payload); + return Ok(()); + } + + let message = InvokeMessage::new( + self.clone(), + manager.state(), + payload.cmd.to_string(), + payload.inner, + ); + let resolver = InvokeResolver::new(self, payload.callback, payload.error); + let invoke = Invoke { message, resolver }; + + if !is_local && scope.is_none() { + invoke.resolver.reject(scope_not_found_error_message); + return Ok(()); + } + + if let Some(module) = &payload.tauri_module { + if !is_local && scope.map(|s| !s.enables_tauri_api()).unwrap_or_default() { + invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); + return Ok(()); } - _ => { - let message = InvokeMessage::new( - self.clone(), - manager.state(), - payload.cmd.to_string(), - payload.inner, - ); - let resolver = InvokeResolver::new(self, payload.callback, payload.error); - let invoke = Invoke { message, resolver }; - - if !is_local && scope.is_none() { - invoke.resolver.reject(scope_not_found_error_message); + crate::endpoints::handle( + module.to_string(), + invoke, + manager.config(), + manager.package_info(), + ); + } else if payload.cmd.starts_with("plugin:") { + if !is_local { + let command = invoke.message.command.replace("plugin:", ""); + let plugin_name = command.split('|').next().unwrap().to_string(); + if !scope + .map(|s| s.plugins().contains(&plugin_name)) + .unwrap_or(true) + { + invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); return Ok(()); } - - if let Some(module) = &payload.tauri_module { - if !is_local && scope.map(|s| !s.enables_tauri_api()).unwrap_or_default() { - invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); - return Ok(()); - } - crate::endpoints::handle( - module.to_string(), - invoke, - manager.config(), - manager.package_info(), - ); - } else if payload.cmd.starts_with("plugin:") { - if !is_local { - let command = invoke.message.command.replace("plugin:", ""); - let plugin_name = command.split('|').next().unwrap().to_string(); - if !scope - .map(|s| s.plugins().contains(&plugin_name)) - .unwrap_or(true) - { - invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); - return Ok(()); - } - } - manager.extend_api(invoke); - } else { - manager.run_invoke_handler(invoke); - } } + manager.extend_api(invoke); + } else { + manager.run_invoke_handler(invoke); } Ok(())
examples/api/src-tauri/Cargo.lock+20 −13 modified@@ -1806,6 +1806,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -4018,7 +4024,7 @@ checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tauri" -version = "1.6.0" +version = "1.6.6" dependencies = [ "anyhow", "base64 0.21.7", @@ -4031,10 +4037,11 @@ dependencies = [ "encoding_rs", "flate2", "futures-util", + "getrandom 0.2.12", "glib", "glob", "gtk", - "heck 0.4.1", + "heck 0.5.0", "http", "ico 0.2.0", "ignore", @@ -4083,12 +4090,12 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.5.1" +version = "1.5.2" dependencies = [ "anyhow", "cargo_toml", "dirs-next", - "heck 0.4.1", + "heck 0.5.0", "json-patch", "quote", "semver", @@ -4102,7 +4109,7 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.2" +version = "1.4.3" dependencies = [ "base64 0.21.7", "brotli", @@ -4126,9 +4133,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.3" +version = "1.4.4" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 1.0.109", @@ -4138,7 +4145,7 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.2" +version = "0.14.3" dependencies = [ "gtk", "http", @@ -4157,7 +4164,7 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.4" +version = "0.14.7" dependencies = [ "arboard", "cocoa 0.24.1", @@ -4176,15 +4183,15 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.5.3" +version = "1.5.4" dependencies = [ "aes-gcm", "brotli", "ctor", "dunce", "getrandom 0.2.12", "glob", - "heck 0.4.1", + "heck 0.5.0", "html5ever", "infer 0.13.0", "json-patch", @@ -5494,9 +5501,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.24.7" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" +checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45" dependencies = [ "base64 0.13.1", "block",
d950ac123981Merge pull request from GHSA-57fm-592m-34r7
9 files changed · +143 −3
.changes/ipc-only-main-frame.md+6 −0 added@@ -0,0 +1,6 @@ +--- +'tauri': patch:sec +'tauri-runtime-wry': patch:sec +--- + +Only process IPC commands from the main frame.
core/tauri/scripts/ipc-protocol.js+13 −2 modified@@ -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) }
core/tauri/src/app.rs+8 −0 modified@@ -1097,6 +1097,8 @@ pub struct Builder<R: Runtime> { /// 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<R: Runtime> Default for Builder<R> { impl<R: Runtime> Builder<R> { /// 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<R: Runtime> Builder<R> { 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<R: Runtime> Builder<R> { 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 {
core/tauri/src/error.rs+12 −1 modified@@ -151,10 +151,21 @@ pub enum Error { /// Failed to deserialize scope object. #[error("error deserializing scope: {0}")] CannotDeserializeScope(Box<dyn std::error::Error + Send + Sync>), - /// 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<getrandom::Error> for Error { + fn from(value: getrandom::Error) -> Self { + Self::Csprng(value) + } } /// `Result<T, ::tauri::Error>`
core/tauri/src/ipc/protocol.rs+16 −0 modified@@ -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<R: Runtime>( manager: Arc<AppManager<R>>, @@ -210,6 +211,8 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager error: CallbackFn, payload: serde_json::Value, options: Option<RequestOptions>, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, } #[allow(unused_mut)] @@ -224,6 +227,8 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager error: CallbackFn, payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>, options: Option<RequestOptions>, + #[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<R: Runtime>(request: Request<String>, 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<R: Runtime>(request: Request<String>, 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<R: Runtime>( } } + 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<R: Runtime>( url, body, headers: parts.headers, + invoke_key, }; Ok(payload)
core/tauri/src/lib.rs+49 −0 modified@@ -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<String> { + let mut bytes = [0u8; 16]; + getrandom::getrandom(&mut bytes)?; + Ok(z85::encode(&bytes)) +}
core/tauri/src/manager/mod.rs+10 −0 modified@@ -193,6 +193,9 @@ pub struct AppManager<R: Runtime> { /// Application Resources Table pub(crate) resources_table: Arc<Mutex<ResourceTable>>, + + /// Runtime-generated invoke key. + pub(crate) invoke_key: String, } impl<R: Runtime> fmt::Debug for AppManager<R> { @@ -232,6 +235,7 @@ impl<R: Runtime> AppManager<R> { crate::app::GlobalMenuEventListener<Window<R>>, >, (invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String), + invoke_key: String, ) -> Self { // generate a random isolation key at runtime #[cfg(feature = "isolation")] @@ -254,6 +258,7 @@ impl<R: Runtime> AppManager<R> { 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<R: Runtime> AppManager<R> { 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<R: Runtime> AppManager<R> { .lock() .expect("poisoned window manager") } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } } #[cfg(desktop)]
core/tauri/src/manager/webview.rs+10 −0 modified@@ -89,6 +89,9 @@ pub struct WebviewManager<R: Runtime> { pub invoke_responder: Option<Arc<InvokeResponder<R>>>, /// The script that initializes the invoke system. pub invoke_initialization_script: String, + + /// A runtime generated invoke key. + pub(crate) invoke_key: String, } impl<R: Runtime> fmt::Debug for WebviewManager<R> { @@ -98,6 +101,7 @@ impl<R: Runtime> fmt::Debug for WebviewManager<R> { "invoke_initialization_script", &self.invoke_initialization_script, ) + .field("invoke_key", &self.invoke_key) .finish() } } @@ -371,6 +375,7 @@ impl<R: Runtime> WebviewManager<R> { #[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<R: Runtime> WebviewManager<R> { 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<R: Runtime> WebviewManager<R> { pub fn labels(&self) -> HashSet<String> { self.webviews_lock().keys().cloned().collect() } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } } impl<R: Runtime> Webview<R> {
core/tauri/src/webview/mod.rs+19 −0 modified@@ -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(
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-57fm-592m-34r7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-35222ghsaADVISORY
- github.com/tauri-apps/tauri/commit/d950ac1239817d17324c035e5c4769ee71fc197dghsaWEB
- github.com/tauri-apps/tauri/commit/f6d81dfe0871e0ccd012e5190d41e3767e733608ghsaWEB
- github.com/tauri-apps/tauri/issues/8316nvdWEB
- github.com/tauri-apps/tauri/security/advisories/GHSA-57fm-592m-34r7nvdWEB
News mentions
0No linked articles in our index yet.