@@ -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