Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/enforce-acl-remote-origins.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 10 additions & 2 deletions crates/tauri/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@ pub fn mock_app() -> App<MockRuntime> {
/// 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(),
Expand Down Expand Up @@ -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(),
Expand Down
67 changes: 65 additions & 2 deletions crates/tauri/src/webview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1793,8 +1793,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()
Expand Down Expand Up @@ -2334,6 +2337,66 @@ mod tests {
crate::test_utils::assert_sync::<super::Webview>();
}

/// 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() {
Expand Down
Loading