Przeglądaj źródła

feat(core): block remote URLs from accessing the IPC

This was cherry picked from ee71c31fd09cc5427da6d29d37c003a914547696, keeping only the logic to block remote URLs from using the IPC.
PR: #5918
Lucas Nogueira 2 lat temu
rodzic
commit
fa90214b05

+ 7 - 0
.changes/remote-urls.md

@@ -0,0 +1,7 @@
+---
+"tauri": patch
+"tauri-runtime": patch
+"tauri-runtime-wry": patch
+---
+
+Block remote URLs from accessing the IPC.

+ 1 - 1
core/tauri-build/src/static_vcruntime.rs

@@ -54,5 +54,5 @@ fn override_msvcrt_lib() {
     f.write_all(bytes).unwrap();
   }
   // Add the output directory to the native library path.
-  println!("cargo:rustc-link-search=native={}", out_dir);
+  println!("cargo:rustc-link-search=native={out_dir}");
 }

+ 1 - 0
core/tauri-runtime-wry/Cargo.toml

@@ -19,6 +19,7 @@ tauri-utils = { version = "1.0.3", path = "../tauri-utils" }
 uuid = { version = "1", features = [ "v4" ] }
 rand = "0.8"
 raw-window-handle = "0.4.3"
+url = "2"
 
 [target."cfg(windows)".dependencies]
 webview2-com = "0.16.0"

+ 15 - 2
core/tauri-runtime-wry/src/lib.rs

@@ -41,6 +41,7 @@ use wry::application::platform::windows::{WindowBuilderExtWindows, WindowExtWind
 use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder};
 
 use tauri_utils::{config::WindowConfig, debug_eprintln, Theme};
+use url::Url;
 use uuid::Uuid;
 use wry::{
   application::{
@@ -216,6 +217,7 @@ impl<T: UserEvent> Context<T> {
 impl<T: UserEvent> Context<T> {
   fn create_webview(&self, pending: PendingWindow<T, Wry<T>>) -> Result<DetachedWindow<T, Wry<T>>> {
     let label = pending.label.clone();
+    let current_url = pending.current_url.clone();
     let menu_ids = pending.menu_ids.clone();
     let js_event_listeners = pending.js_event_listeners.clone();
     let context = self.clone();
@@ -237,6 +239,7 @@ impl<T: UserEvent> Context<T> {
     };
     Ok(DetachedWindow {
       label,
+      current_url,
       dispatcher,
       menu_ids,
       js_event_listeners,
@@ -1862,6 +1865,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
 
   fn create_window(&self, pending: PendingWindow<T, Self>) -> Result<DetachedWindow<T, Self>> {
     let label = pending.label.clone();
+    let current_url = pending.current_url.clone();
     let menu_ids = pending.menu_ids.clone();
     let js_event_listeners = pending.js_event_listeners.clone();
     let window_id = rand::random();
@@ -1889,6 +1893,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
 
     Ok(DetachedWindow {
       label,
+      current_url,
       dispatcher,
       menu_ids,
       js_event_listeners,
@@ -2833,7 +2838,7 @@ fn create_webview<T: UserEvent>(
     mut window_builder,
     ipc_handler,
     label,
-    url,
+    current_url,
     menu_ids,
     js_event_listeners,
     ..
@@ -2871,16 +2876,22 @@ fn create_webview<T: UserEvent>(
   }
   let mut webview_builder = WebViewBuilder::new(window)
     .map_err(|e| Error::CreateWebview(Box::new(e)))?
-    .with_url(&url)
+    .with_url(current_url.lock().unwrap().as_str())
     .unwrap() // safe to unwrap because we validate the URL beforehand
     .with_transparent(is_window_transparent);
   if webview_attributes.file_drop_handler_enabled {
     webview_builder = webview_builder.with_file_drop_handler(create_file_drop_handler(&context));
   }
+  if let Some(navigation_handler) = pending.navigation_handler {
+    webview_builder = webview_builder.with_navigation_handler(move |url| {
+      Url::parse(&url).map(&navigation_handler).unwrap_or(true)
+    });
+  }
   if let Some(handler) = ipc_handler {
     webview_builder = webview_builder.with_ipc_handler(create_ipc_handler(
       context,
       label.clone(),
+      current_url,
       menu_ids,
       js_event_listeners,
       handler,
@@ -2984,6 +2995,7 @@ fn create_webview<T: UserEvent>(
 fn create_ipc_handler<T: UserEvent>(
   context: Context<T>,
   label: String,
+  current_url: Arc<Mutex<Url>>,
   menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
   js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
   handler: WebviewIpcHandler<T, Wry<T>>,
@@ -2992,6 +3004,7 @@ fn create_ipc_handler<T: UserEvent>(
     let window_id = context.webview_id_map.get(&window.id());
     handler(
       DetachedWindow {
+        current_url: current_url.clone(),
         dispatcher: WryDispatcher {
           window_id,
           context: context.clone(),

+ 1 - 0
core/tauri-runtime/Cargo.toml

@@ -32,6 +32,7 @@ http = "0.2.4"
 http-range = "0.1.4"
 infer = "0.7"
 raw-window-handle = "0.4.3"
+url = "2"
 
 [target."cfg(windows)".dependencies]
 webview2-com = "0.16.0"

+ 15 - 5
core/tauri-runtime/src/window.rs

@@ -12,6 +12,7 @@ use crate::{
 };
 use serde::{Deserialize, Deserializer, Serialize};
 use tauri_utils::{config::WindowConfig, Theme};
+use url::Url;
 
 use std::{
   collections::{HashMap, HashSet},
@@ -226,14 +227,17 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
   /// How to handle IPC calls on the webview window.
   pub ipc_handler: Option<WebviewIpcHandler<T, R>>,
 
-  /// The resolved URL to load on the webview.
-  pub url: String,
-
   /// Maps runtime id to a string menu id.
   pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
 
   /// A HashMap mapping JS event names with associated listener ids.
   pub js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
+
+  /// A handler to decide if incoming url is allowed to navigate.
+  pub navigation_handler: Option<Box<dyn Fn(Url) -> bool + Send>>,
+
+  /// The current webview URL.
+  pub current_url: Arc<Mutex<Url>>,
 }
 
 pub fn is_label_valid(label: &str) -> bool {
@@ -270,9 +274,10 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        url: "tauri://localhost".to_string(),
         menu_ids: Arc::new(Mutex::new(menu_ids)),
         js_event_listeners: Default::default(),
+        navigation_handler: Default::default(),
+        current_url: Arc::new(Mutex::new("tauri://localhost".parse().unwrap())),
       })
     }
   }
@@ -299,9 +304,10 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        url: "tauri://localhost".to_string(),
         menu_ids: Arc::new(Mutex::new(menu_ids)),
         js_event_listeners: Default::default(),
+        navigation_handler: Default::default(),
+        current_url: Arc::new(Mutex::new("tauri://localhost".parse().unwrap())),
       })
     }
   }
@@ -342,6 +348,9 @@ pub struct JsEventListenerKey {
 /// A webview window that is not yet managed by Tauri.
 #[derive(Debug)]
 pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
+  /// The current webview URL.
+  pub current_url: Arc<Mutex<Url>>,
+
   /// Name of the window
   pub label: String,
 
@@ -358,6 +367,7 @@ pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
 impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
   fn clone(&self) -> Self {
     Self {
+      current_url: self.current_url.clone(),
       label: self.label.clone(),
       dispatcher: self.dispatcher.clone(),
       menu_ids: self.menu_ids.clone(),

+ 1 - 1
core/tauri/src/app.rs

@@ -902,7 +902,7 @@ impl<R: Runtime> Builder<R> {
       #[cfg(any(windows, target_os = "linux"))]
       runtime_any_thread: false,
       setup: Box::new(|_| Ok(())),
-      invoke_handler: Box::new(|_| ()),
+      invoke_handler: Box::new(|invoke| invoke.resolver.reject("not implemented")),
       invoke_responder: Arc::new(window_invoke_responder),
       invoke_initialization_script:
         "Object.defineProperty(window, '__TAURI_POST_MESSAGE__', { value: (message) => window.ipc.postMessage(JSON.stringify(message)) })".into(),

+ 52 - 37
core/tauri/src/manager.rs

@@ -25,10 +25,9 @@ use tauri_utils::{
   html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
 };
 
-use crate::hooks::IpcJavascript;
 #[cfg(feature = "isolation")]
 use crate::hooks::IsolationJavascript;
-use crate::pattern::{format_real_schema, PatternJavascript};
+use crate::pattern::PatternJavascript;
 use crate::{
   app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
   event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
@@ -54,6 +53,7 @@ use crate::{
   app::{GlobalMenuEventListener, WindowMenuEvent},
   window::WebResourceRequestHandler,
 };
+use crate::{hooks::IpcJavascript, pattern::format_real_schema};
 
 #[cfg(any(target_os = "linux", target_os = "windows"))]
 use crate::api::path::{resolve_path, BaseDirectory};
@@ -136,7 +136,7 @@ fn set_csp<R: Runtime>(
     let default_src = csp
       .entry("default-src".into())
       .or_insert_with(Default::default);
-    default_src.push(format_real_schema(schema));
+    default_src.push(crate::pattern::format_real_schema(schema));
   }
 
   Csp::DirectiveMap(csp).to_string()
@@ -225,7 +225,7 @@ pub struct InnerWindowManager<R: Runtime> {
   /// The script that initializes the invoke system.
   invoke_initialization_script: String,
   /// Application pattern.
-  pattern: Pattern,
+  pub(crate) pattern: Pattern,
 }
 
 impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
@@ -357,9 +357,12 @@ impl<R: Runtime> WindowManager<R> {
   /// Get the base URL to use for webview requests.
   ///
   /// In dev mode, this will be based on the `devPath` configuration value.
-  fn get_url(&self) -> Cow<'_, Url> {
+  pub(crate) fn get_url(&self) -> Cow<'_, Url> {
     match self.base_path() {
       AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
+      #[cfg(windows)]
+      _ => Cow::Owned(Url::parse("https://tauri.localhost").unwrap()),
+      #[cfg(not(windows))]
       _ => Cow::Owned(Url::parse("tauri://localhost").unwrap()),
     }
   }
@@ -467,7 +470,7 @@ impl<R: Runtime> WindowManager<R> {
       });
     }
 
-    let window_url = Url::parse(&pending.url).unwrap();
+    let window_url = pending.current_url.lock().unwrap().clone();
     let window_origin =
       if cfg!(windows) && window_url.scheme() != "http" && window_url.scheme() != "https" {
         format!("https://{}.localhost", window_url.scheme())
@@ -1001,7 +1004,16 @@ mod test {
     );
 
     #[cfg(custom_protocol)]
-    assert_eq!(manager.get_url().to_string(), "tauri://localhost");
+    {
+      assert_eq!(
+        manager.get_url().to_string(),
+        if cfg!(windows) {
+          "https://tauri.localhost/"
+        } else {
+          "tauri://localhost"
+        }
+      );
+    }
 
     #[cfg(dev)]
     assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
@@ -1052,27 +1064,21 @@ impl<R: Runtime> WindowManager<R> {
       return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
     }
     #[allow(unused_mut)] // mut url only for the data-url parsing
-    let (is_local, mut url) = match &pending.webview_attributes.url {
+    let mut url = match &pending.webview_attributes.url {
       WindowUrl::App(path) => {
         let url = self.get_url();
-        (
-          true,
-          // ignore "index.html" just to simplify the url
-          if path.to_str() != Some("index.html") {
-            url
-              .join(&*path.to_string_lossy())
-              .map_err(crate::Error::InvalidUrl)
-              // this will never fail
-              .unwrap()
-          } else {
-            url.into_owned()
-          },
-        )
-      }
-      WindowUrl::External(url) => {
-        let config_url = self.get_url();
-        (config_url.make_relative(url).is_some(), url.clone())
+        // ignore "index.html" just to simplify the url
+        if path.to_str() != Some("index.html") {
+          url
+            .join(&*path.to_string_lossy())
+            .map_err(crate::Error::InvalidUrl)
+            // this will never fail
+            .unwrap()
+        } else {
+          url.into_owned()
+        }
       }
+      WindowUrl::External(url) => url.clone(),
       _ => unimplemented!(),
     };
 
@@ -1099,7 +1105,7 @@ impl<R: Runtime> WindowManager<R> {
       }
     }
 
-    pending.url = url.to_string();
+    *pending.current_url.lock().unwrap() = url;
 
     if !pending.window_builder.has_icon() {
       if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
@@ -1115,17 +1121,15 @@ impl<R: Runtime> WindowManager<R> {
       }
     }
 
-    if is_local {
-      let label = pending.label.clone();
-      pending = self.prepare_pending_window(
-        pending,
-        &label,
-        window_labels,
-        app_handle.clone(),
-        web_resource_request_handler,
-      )?;
-      pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle));
-    }
+    let label = pending.label.clone();
+    pending = self.prepare_pending_window(
+      pending,
+      &label,
+      window_labels,
+      app_handle.clone(),
+      web_resource_request_handler,
+    )?;
+    pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle));
 
     // in `Windows`, we need to force a data_directory
     // but we do respect user-specification
@@ -1150,6 +1154,17 @@ impl<R: Runtime> WindowManager<R> {
       }
     }
 
+    let current_url_ = pending.current_url.clone();
+    let navigation_handler = pending.navigation_handler.take();
+    pending.navigation_handler = Some(Box::new(move |url| {
+      *current_url_.lock().unwrap() = url.clone();
+      if let Some(handler) = &navigation_handler {
+        handler(url)
+      } else {
+        true
+      }
+    }));
+
     Ok(pending)
   }
 

+ 5 - 2
core/tauri/src/pattern.rs

@@ -11,6 +11,9 @@ use serialize_to_javascript::{default_template, Template};
 
 use tauri_utils::assets::{Assets, EmbeddedAssets};
 
+/// The domain of the isolation iframe source.
+pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost";
+
 /// An application pattern.
 #[derive(Debug, Clone)]
 pub enum Pattern<A: Assets = EmbeddedAssets> {
@@ -87,8 +90,8 @@ pub(crate) struct PatternJavascript {
 #[allow(dead_code)]
 pub(crate) fn format_real_schema(schema: &str) -> String {
   if cfg!(windows) {
-    format!("https://{}.localhost", schema)
+    format!("https://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}")
   } else {
-    format!("{}://localhost", schema)
+    format!("{schema}://{ISOLATION_IFRAME_SRC_DOMAIN}")
   }
 }

+ 2 - 0
core/tauri/src/test/mock_runtime.rs

@@ -67,6 +67,7 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
   ) -> Result<DetachedWindow<T, Self::Runtime>> {
     Ok(DetachedWindow {
       label: pending.label,
+      current_url: Arc::new(Mutex::new("tauri://localhost".parse().unwrap())),
       dispatcher: MockDispatcher {
         context: self.context.clone(),
       },
@@ -616,6 +617,7 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
   fn create_window(&self, pending: PendingWindow<T, Self>) -> Result<DetachedWindow<T, Self>> {
     Ok(DetachedWindow {
       label: pending.label,
+      current_url: Arc::new(Mutex::new("tauri://localhost".parse().unwrap())),
       dispatcher: MockDispatcher {
         context: self.context.clone(),
       },

+ 19 - 2
core/tauri/src/window.rs

@@ -33,6 +33,7 @@ use crate::{
 };
 
 use serde::Serialize;
+use url::Url;
 #[cfg(windows)]
 use windows::Win32::Foundation::HWND;
 
@@ -483,7 +484,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
 #[derive(Debug)]
 pub struct Window<R: Runtime> {
   /// The webview window created by the runtime.
-  window: DetachedWindow<EventLoopMessage, R>,
+  pub(crate) window: DetachedWindow<EventLoopMessage, R>,
   /// The manager to associate this webview window with.
   manager: WindowManager<R>,
   pub(crate) app_handle: AppHandle<R>,
@@ -1157,9 +1158,18 @@ impl<R: Runtime> Window<R> {
 
 /// Webview APIs.
 impl<R: Runtime> Window<R> {
+  /// Returns the current url of the webview.
+  pub fn url(&self) -> Url {
+    self.window.current_url.lock().unwrap().clone()
+  }
+
   /// Handles this window receiving an [`InvokeMessage`].
   pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> {
     let manager = self.manager.clone();
+    let current_url = self.url();
+    let config_url = manager.get_url();
+    let is_local = config_url.make_relative(&current_url).is_some();
+
     match payload.cmd.as_str() {
       "__initialized" => {
         let payload: PageLoadPayload = serde_json::from_value(payload.inner)?;
@@ -1173,8 +1183,15 @@ impl<R: Runtime> Window<R> {
           payload.inner,
         );
         let resolver = InvokeResolver::new(self, payload.callback, payload.error);
-
         let invoke = Invoke { message, resolver };
+
+        if !is_local {
+          invoke
+            .resolver
+            .reject("Remote URLs are not allowed to access the IPC");
+          return Ok(());
+        }
+
         if let Some(module) = &payload.tauri_module {
           crate::endpoints::handle(
             module.to_string(),

+ 9 - 7
examples/api/src-tauri/Cargo.lock

@@ -3132,7 +3132,7 @@ dependencies = [
 
 [[package]]
 name = "tauri"
-version = "1.0.2"
+version = "1.0.8"
 dependencies = [
  "anyhow",
  "attohttpc",
@@ -3194,7 +3194,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-build"
-version = "1.0.2"
+version = "1.0.4"
 dependencies = [
  "anyhow",
  "cargo_toml",
@@ -3209,7 +3209,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-codegen"
-version = "1.0.2"
+version = "1.0.4"
 dependencies = [
  "base64",
  "brotli",
@@ -3233,7 +3233,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-macros"
-version = "1.0.2"
+version = "1.0.4"
 dependencies = [
  "heck 0.4.0",
  "proc-macro2",
@@ -3245,7 +3245,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-runtime"
-version = "0.10.1"
+version = "0.10.2"
 dependencies = [
  "gtk",
  "http",
@@ -3256,6 +3256,7 @@ dependencies = [
  "serde_json",
  "tauri-utils",
  "thiserror",
+ "url",
  "uuid 1.1.2",
  "webview2-com",
  "windows 0.37.0",
@@ -3263,7 +3264,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-runtime-wry"
-version = "0.10.1"
+version = "0.10.2"
 dependencies = [
  "cocoa",
  "gtk",
@@ -3272,6 +3273,7 @@ dependencies = [
  "raw-window-handle",
  "tauri-runtime",
  "tauri-utils",
+ "url",
  "uuid 1.1.2",
  "webkit2gtk",
  "webview2-com",
@@ -3281,7 +3283,7 @@ dependencies = [
 
 [[package]]
 name = "tauri-utils"
-version = "1.0.2"
+version = "1.0.3"
 dependencies = [
  "aes-gcm",
  "brotli",