Skip to content

Commit 1b26769

Browse files
authored
fix(tauri): enforce ACL for remote origins even without AppManifest (#15266)
1 parent 3057eda commit 1b26769

3 files changed

Lines changed: 80 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'tauri': 'patch:sec'
3+
---
4+
5+
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.

crates/tauri/src/test/mod.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,11 @@ pub fn mock_app() -> App<MockRuntime> {
216216
/// cmd: "ping".into(),
217217
/// callback: tauri::ipc::CallbackFn(0),
218218
/// error: tauri::ipc::CallbackFn(1),
219-
/// url: "http://tauri.localhost".parse().unwrap(),
219+
/// url: if cfg!(any(windows, target_os = "android")) {
220+
/// "http://tauri.localhost"
221+
/// } else {
222+
/// "tauri://localhost"
223+
/// }.parse().unwrap(),
220224
/// body: tauri::ipc::InvokeBody::default(),
221225
/// headers: Default::default(),
222226
/// invoke_key: tauri::test::INVOKE_KEY.to_string(),
@@ -275,7 +279,11 @@ pub fn assert_ipc_response<
275279
/// cmd: "ping".into(),
276280
/// callback: tauri::ipc::CallbackFn(0),
277281
/// error: tauri::ipc::CallbackFn(1),
278-
/// url: "http://tauri.localhost".parse().unwrap(),
282+
/// url: if cfg!(any(windows, target_os = "android")) {
283+
/// "http://tauri.localhost"
284+
/// } else {
285+
/// "tauri://localhost"
286+
/// }.parse().unwrap(),
279287
/// body: tauri::ipc::InvokeBody::default(),
280288
/// headers: Default::default(),
281289
/// invoke_key: tauri::test::INVOKE_KEY.to_string(),

crates/tauri/src/webview/mod.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,8 +1816,11 @@ tauri::Builder::default()
18161816
(plugin, command)
18171817
});
18181818

1819-
// we only check ACL on plugin commands or if the app defined its ACL manifest
1820-
if (plugin_command.is_some() || has_app_acl_manifest)
1819+
// Check ACL on plugin commands, when the app defined its ACL manifest,
1820+
// or when the request comes from a non-local (remote) origin. This
1821+
// ensures remote content can never reach custom commands unless an
1822+
// explicit `remote` capability has been configured for them.
1823+
if (plugin_command.is_some() || has_app_acl_manifest || !is_local)
18211824
// TODO: Remove this special check in v3
18221825
&& request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
18231826
&& invoke.acl.is_none()
@@ -2357,6 +2360,66 @@ mod tests {
23572360
crate::test_utils::assert_sync::<super::Webview>();
23582361
}
23592362

2363+
/// Custom (non-plugin) commands must be rejected when the IPC request
2364+
/// originates from a remote URL, even when no `AppManifest` has been
2365+
/// configured. Only local (bundled) origins should be able to reach
2366+
/// custom commands.
2367+
#[test]
2368+
fn remote_origin_blocked_for_custom_commands_without_app_manifest() {
2369+
use crate::test::{mock_builder, mock_context, noop_assets, INVOKE_KEY};
2370+
use crate::webview::InvokeRequest;
2371+
2372+
let app = mock_builder().build(mock_context(noop_assets())).unwrap();
2373+
2374+
let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default())
2375+
.build()
2376+
.unwrap();
2377+
2378+
// Request from a remote origin for a custom (non-plugin) command
2379+
// – should be rejected even without an AppManifest.
2380+
let remote_result = crate::test::get_ipc_response(
2381+
&webview,
2382+
InvokeRequest {
2383+
cmd: "any_custom_command".into(),
2384+
callback: crate::ipc::CallbackFn(0),
2385+
error: crate::ipc::CallbackFn(1),
2386+
url: "https://evil.com".parse().unwrap(),
2387+
body: crate::ipc::InvokeBody::default(),
2388+
headers: Default::default(),
2389+
invoke_key: INVOKE_KEY.to_string(),
2390+
},
2391+
);
2392+
assert!(
2393+
remote_result.is_err(),
2394+
"custom command should be rejected from a remote origin"
2395+
);
2396+
2397+
// Same command from the local origin – should NOT be rejected by the
2398+
// remote-origin guard (it may still fail because the command doesn't
2399+
// exist, but the error message will be different).
2400+
let local_result = crate::test::get_ipc_response(
2401+
&webview,
2402+
InvokeRequest {
2403+
cmd: "any_custom_command".into(),
2404+
callback: crate::ipc::CallbackFn(0),
2405+
error: crate::ipc::CallbackFn(1),
2406+
url: "tauri://localhost".parse().unwrap(),
2407+
body: crate::ipc::InvokeBody::default(),
2408+
headers: Default::default(),
2409+
invoke_key: INVOKE_KEY.to_string(),
2410+
},
2411+
);
2412+
// The local request should either succeed or fail for a reason OTHER
2413+
// than "not allowed from remote context".
2414+
if let Err(e) = &local_result {
2415+
let msg = e.to_string();
2416+
assert!(
2417+
!msg.contains("not allowed from remote context"),
2418+
"local origin should not be blocked by the remote-origin guard, got: {msg}"
2419+
);
2420+
}
2421+
}
2422+
23602423
#[cfg(target_os = "macos")]
23612424
#[test]
23622425
fn test_webview_window_has_set_simple_fullscreen_method() {

0 commit comments

Comments
 (0)