From 3057eda067b87761644209adeec077f232585c5d Mon Sep 17 00:00:00 2001 From: Xuhui Zheng <2529677678@qq.com> Date: Mon, 4 May 2026 21:02:08 +0800 Subject: [PATCH 1/5] fix(driver): enable `eq-separator` feature for `pico-args`. (#15324) --- .changes/driver-arg-eq-separator.md | 5 +++++ crates/tauri-driver/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changes/driver-arg-eq-separator.md diff --git a/.changes/driver-arg-eq-separator.md b/.changes/driver-arg-eq-separator.md new file mode 100644 index 000000000000..98b985a1d633 --- /dev/null +++ b/.changes/driver-arg-eq-separator.md @@ -0,0 +1,5 @@ +--- +"tauri-driver": patch +--- + +Support `eq-separator` for `tauri-driver`. diff --git a/crates/tauri-driver/Cargo.toml b/crates/tauri-driver/Cargo.toml index c25b57feeac0..fbaacf841692 100644 --- a/crates/tauri-driver/Cargo.toml +++ b/crates/tauri-driver/Cargo.toml @@ -24,7 +24,7 @@ hyper-util = { version = "0.1", features = [ "server", "tokio", ] } -pico-args = "0.5" +pico-args = { version = "0.5", features = ["eq-separator"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["macros"] } From 1b26769f92b54b158777a35a7f548f870f4e7901 Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Mon, 4 May 2026 15:04:29 +0200 Subject: [PATCH 2/5] fix(tauri): enforce ACL for remote origins even without AppManifest (#15266) --- .changes/enforce-acl-remote-origins.md | 5 ++ crates/tauri/src/test/mod.rs | 12 ++++- crates/tauri/src/webview/mod.rs | 67 +++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 .changes/enforce-acl-remote-origins.md diff --git a/.changes/enforce-acl-remote-origins.md b/.changes/enforce-acl-remote-origins.md new file mode 100644 index 000000000000..eff8fab21a24 --- /dev/null +++ b/.changes/enforce-acl-remote-origins.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:sec' +--- + +Enforce ACL checks for IPC requests from remote origins even when no `AppManifest` is configured. Previously, custom (non-plugin) commands bypassed ACL entirely without an `AppManifest`, allowing any origin to invoke them. Now, remote origins are always subject to ACL resolution, and can only reach custom commands if an explicit `remote` capability has been granted. diff --git a/crates/tauri/src/test/mod.rs b/crates/tauri/src/test/mod.rs index 9161f097951d..a1428c390127 100644 --- a/crates/tauri/src/test/mod.rs +++ b/crates/tauri/src/test/mod.rs @@ -216,7 +216,11 @@ pub fn mock_app() -> App { /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), @@ -275,7 +279,11 @@ pub fn assert_ipc_response< /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index c30b8f040563..f4396d90a269 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -1816,8 +1816,11 @@ tauri::Builder::default() (plugin, command) }); - // we only check ACL on plugin commands or if the app defined its ACL manifest - if (plugin_command.is_some() || has_app_acl_manifest) + // Check ACL on plugin commands, when the app defined its ACL manifest, + // or when the request comes from a non-local (remote) origin. This + // ensures remote content can never reach custom commands unless an + // explicit `remote` capability has been configured for them. + if (plugin_command.is_some() || has_app_acl_manifest || !is_local) // TODO: Remove this special check in v3 && request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND && invoke.acl.is_none() @@ -2357,6 +2360,66 @@ mod tests { crate::test_utils::assert_sync::(); } + /// Custom (non-plugin) commands must be rejected when the IPC request + /// originates from a remote URL, even when no `AppManifest` has been + /// configured. Only local (bundled) origins should be able to reach + /// custom commands. + #[test] + fn remote_origin_blocked_for_custom_commands_without_app_manifest() { + use crate::test::{mock_builder, mock_context, noop_assets, INVOKE_KEY}; + use crate::webview::InvokeRequest; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + // Request from a remote origin for a custom (non-plugin) command + // – should be rejected even without an AppManifest. + let remote_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "https://evil.com".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + assert!( + remote_result.is_err(), + "custom command should be rejected from a remote origin" + ); + + // Same command from the local origin – should NOT be rejected by the + // remote-origin guard (it may still fail because the command doesn't + // exist, but the error message will be different). + let local_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "tauri://localhost".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + // The local request should either succeed or fail for a reason OTHER + // than "not allowed from remote context". + if let Err(e) = &local_result { + let msg = e.to_string(); + assert!( + !msg.contains("not allowed from remote context"), + "local origin should not be blocked by the remote-origin guard, got: {msg}" + ); + } + } + #[cfg(target_os = "macos")] #[test] fn test_webview_window_has_set_simple_fullscreen_method() { From 5f479c0c364d7f5d89a83eaff66fbb7ef5045ce9 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 4 May 2026 10:10:20 -0300 Subject: [PATCH 3/5] fix(core): requestPermission crash regression on Android, closes #15323 (#15336) * fix(core): requestPermission crash regression on Android, closes #15323 regression from #14484 PluginManager::onActivityCreate is never called, this fixes it * tag * super --- .changes/fix-request-permission-android.md | 5 +++++ crates/tauri/mobile/android-codegen/TauriActivity.kt | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changes/fix-request-permission-android.md diff --git a/.changes/fix-request-permission-android.md b/.changes/fix-request-permission-android.md new file mode 100644 index 000000000000..a9a55642548a --- /dev/null +++ b/.changes/fix-request-permission-android.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:bug +--- + +Fix crash when using the requestPermission API on Android. diff --git a/crates/tauri/mobile/android-codegen/TauriActivity.kt b/crates/tauri/mobile/android-codegen/TauriActivity.kt index d4c2cbaad95f..251c41e3598e 100644 --- a/crates/tauri/mobile/android-codegen/TauriActivity.kt +++ b/crates/tauri/mobile/android-codegen/TauriActivity.kt @@ -8,6 +8,7 @@ package {{package}} import android.content.Intent import android.content.res.Configuration +import android.os.Bundle import app.tauri.plugin.PluginManager import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -33,6 +34,11 @@ object TauriLifecycleObserver : DefaultLifecycleObserver { abstract class TauriActivity : WryActivity() { override val handleBackNavigation: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PluginManager.onActivityCreate(this) + } + fun getPluginManager(): PluginManager { return PluginManager } From ba025588f3559858f43547e8c04424c47a3c445b Mon Sep 17 00:00:00 2001 From: Chip Reed Date: Mon, 4 May 2026 06:16:43 -0700 Subject: [PATCH 4/5] Merge commit from fork * check .localhost suffix on windows and android i didn't actually run this on windows, i'm relying on CI to tell me * Create tauri-sec-localhost-suffix.md --------- Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> --- .changes/tauri-sec-localhost-suffix.md | 6 ++ crates/tauri/src/webview/mod.rs | 83 ++++++++++++++++++++------ 2 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 .changes/tauri-sec-localhost-suffix.md diff --git a/.changes/tauri-sec-localhost-suffix.md b/.changes/tauri-sec-localhost-suffix.md new file mode 100644 index 000000000000..c86f36352ae2 --- /dev/null +++ b/.changes/tauri-sec-localhost-suffix.md @@ -0,0 +1,6 @@ +--- +tauri: patch:sec +--- + +Correctly handle .localhost suffix in local origins on Windows and Android to fix a security issue that made tauri think remote websites that started with a registered scheme were local websites. +For example, when registering an `app` custom protocol, Tauri would think `http://app.evil.com/` would be a local URL on Windows/Android. diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index f4396d90a269..b89d3c95d0e8 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -1724,14 +1724,14 @@ tauri::Builder::default() // so we check using the first part of the domain #[cfg(any(windows, target_os = "android"))] let local = { - let protocol_url = self.manager().tauri_protocol_url(uses_https); - let maybe_protocol = current_url + let scheme = scheme == self.manager().tauri_protocol_url(uses_https).scheme(); + let protocol = current_url .domain() - .and_then(|d| d .split_once('.')) - .unwrap_or_default() - .0; + .and_then(|d| d.strip_suffix(".localhost")) + .map(|protocol| protocols.contains_key(protocol)) + .unwrap_or_default(); - protocols.contains_key(maybe_protocol) && scheme == protocol_url.scheme() + scheme && protocol }; local @@ -2354,12 +2354,68 @@ impl ResolvedScope { #[cfg(test)] mod tests { + use url::Url; + + fn test_webview_window() -> crate::WebviewWindow { + use crate::test::{mock_builder, mock_context, noop_assets}; + + // Create a mock app with proper context + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + // Create a webview window + crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + } + #[test] fn webview_is_send_sync() { crate::test_utils::assert_send::(); crate::test_utils::assert_sync::(); } + #[test] + fn tauri_protocol_is_local() { + let webview = test_webview_window().webview; + + #[cfg(all(not(windows), not(target_os = "android")))] + assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap())); + + #[cfg(any(windows, target_os = "android"))] + assert!(webview.is_local_url(&Url::parse("https://tauri.localhost/").unwrap())); + } + + // On Windows/Android, custom protocols are served as `https://.localhost/`. + // We ensure only `.localhost` domains are accepted to prevent a subdomain being able to + // impersonate a protocol name. + #[cfg(any(windows, target_os = "android"))] + #[test] + fn windows_custom_protocol_rejects_spoofed_domain() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder() + .register_uri_scheme_protocol("myproto", |_, _| { + http::Response::builder().body(Vec::new()).unwrap() + }) + .build(mock_context(noop_assets())) + .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + .webview; + + let url = |s| Url::parse(s).unwrap(); + + // Legitimate Windows custom protocol URL + assert!(webview.is_local_url(&url("https://myproto.localhost/"))); + + // Attacker domain that starts with a registered protocol name — must NOT be local. + assert!(!webview.is_local_url(&url("https://myproto.evil.com/"))); + + // Subdomain of .localhost with unregistered name — must NOT be local + assert!(!webview.is_local_url(&url("https://notregistered.localhost/"))); + } + /// Custom (non-plugin) commands must be rejected when the IPC request /// originates from a remote URL, even when no `AppManifest` has been /// configured. Only local (bundled) origins should be able to reach @@ -2376,7 +2432,7 @@ mod tests { .unwrap(); // Request from a remote origin for a custom (non-plugin) command - // – should be rejected even without an AppManifest. + // - should be rejected even without an AppManifest. let remote_result = crate::test::get_ipc_response( &webview, InvokeRequest { @@ -2394,7 +2450,7 @@ mod tests { "custom command should be rejected from a remote origin" ); - // Same command from the local origin – should NOT be rejected by the + // Same command from the local origin - should NOT be rejected by the // remote-origin guard (it may still fail because the command doesn't // exist, but the error message will be different). let local_result = crate::test::get_ipc_response( @@ -2423,16 +2479,7 @@ mod tests { #[cfg(target_os = "macos")] #[test] fn test_webview_window_has_set_simple_fullscreen_method() { - use crate::test::{mock_builder, mock_context, noop_assets}; - - // Create a mock app with proper context - let app = mock_builder().build(mock_context(noop_assets())).unwrap(); - - // Get or create a webview window - let webview_window = - crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) - .build() - .unwrap(); + let webview_window = test_webview_window(); // This should compile if set_simple_fullscreen exists let result = webview_window.set_simple_fullscreen(true); From 5e3126ff7045aec54811b227cb4d33d78b3957b5 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 4 May 2026 13:35:16 -0300 Subject: [PATCH 5/5] feat(mobile): expose monitor APIs (#15338) they are actually implemented by tao now noticed while testing https://github.com/tauri-apps/tao/pull/1211 --- .changes/expose-mobile-monitor-apis.md | 5 +++ crates/tauri/src/window/plugin.rs | 44 ++++++++++++++------------ 2 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 .changes/expose-mobile-monitor-apis.md diff --git a/.changes/expose-mobile-monitor-apis.md b/.changes/expose-mobile-monitor-apis.md new file mode 100644 index 000000000000..76ba0b68e184 --- /dev/null +++ b/.changes/expose-mobile-monitor-apis.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +Expose the monitor (display) APIs on mobile. diff --git a/crates/tauri/src/window/plugin.rs b/crates/tauri/src/window/plugin.rs index 4fba9e72e9cd..01a95f034767 100644 --- a/crates/tauri/src/window/plugin.rs +++ b/crates/tauri/src/window/plugin.rs @@ -54,8 +54,8 @@ mod commands { use super::*; use crate::{ command, sealed::ManagerBase, utils::config::WindowConfig, window::Color, - window::WindowBuilder, AppHandle, PhysicalPosition, PhysicalSize, Position, Size, Theme, - Window, + window::WindowBuilder, AppHandle, Monitor, PhysicalPosition, PhysicalSize, Position, Size, + Theme, Window, }; #[command(root = "crate")] @@ -102,6 +102,21 @@ mod commands { setter!(set_size_constraints, WindowSizeConstraints); setter!(set_theme, Option); setter!(set_enabled, bool); + + getter!(current_monitor, Option); + getter!(primary_monitor, Option); + getter!(available_monitors, Vec); + + #[command(root = "crate")] + pub async fn monitor_from_point( + window: Window, + label: Option, + x: f64, + y: f64, + ) -> crate::Result> { + let window = get_window(window, label)?; + window.monitor_from_point(x, y) + } } #[cfg(desktop)] @@ -112,7 +127,7 @@ mod desktop_commands { use super::*; use crate::{ command, utils::config::WindowEffectsConfig, window::ProgressBarState, CursorIcon, Manager, - Monitor, PhysicalPosition, Position, UserAttentionType, Webview, + PhysicalPosition, Position, UserAttentionType, Webview, }; getter!(is_fullscreen, bool); @@ -122,9 +137,6 @@ mod desktop_commands { getter!(is_maximizable, bool); getter!(is_minimizable, bool); getter!(is_closable, bool); - getter!(current_monitor, Option); - getter!(primary_monitor, Option); - getter!(available_monitors, Vec); getter!(cursor_position, PhysicalPosition); getter!(is_always_on_top, bool); @@ -217,17 +229,6 @@ mod desktop_commands { } Ok(()) } - - #[command(root = "crate")] - pub async fn monitor_from_point( - window: Window, - label: Option, - x: f64, - y: f64, - ) -> crate::Result> { - let window = get_window(window, label)?; - window.monitor_from_point(x, y) - } } /// Initializes the plugin. @@ -291,6 +292,10 @@ pub fn init() -> TauriPlugin { commands::set_enabled, commands::set_background_color, commands::set_theme, + commands::current_monitor, + commands::primary_monitor, + commands::monitor_from_point, + commands::available_monitors, #[cfg(desktop)] desktop_commands::is_fullscreen, #[cfg(desktop)] desktop_commands::is_minimized, @@ -299,10 +304,7 @@ pub fn init() -> TauriPlugin { #[cfg(desktop)] desktop_commands::is_maximizable, #[cfg(desktop)] desktop_commands::is_minimizable, #[cfg(desktop)] desktop_commands::is_closable, - #[cfg(desktop)] desktop_commands::current_monitor, - #[cfg(desktop)] desktop_commands::primary_monitor, - #[cfg(desktop)] desktop_commands::monitor_from_point, - #[cfg(desktop)] desktop_commands::available_monitors, + #[cfg(desktop)] desktop_commands::cursor_position, #[cfg(desktop)] desktop_commands::is_always_on_top, // setters