feat(core): allow configuring remote domains with IPC access, closes #5088 (#5918)

This commit is contained in:
Lucas Fernandes Nogueira 2023-04-11 04:50:15 -07:00 committed by GitHub
parent 6ff801e27d
commit ee71c31fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 794 additions and 98 deletions

6
.changes/remote-urls.md Normal file
View File

@ -0,0 +1,6 @@
---
"tauri": patch
"tauri-utils": patch
---
Added configuration to specify remote URLs allowed to access the IPC.

View File

@ -339,7 +339,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
if target_triple.contains("darwin") {
if let Some(version) = &config.tauri.bundle.macos.minimum_system_version {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={}", version);
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
}
}

View File

@ -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}");
}

View File

@ -163,6 +163,7 @@
},
"security": {
"dangerousDisableAssetCspModification": false,
"dangerousRemoteDomainIpcAccess": [],
"freezePrototype": false
},
"updater": {
@ -415,6 +416,7 @@
"description": "Security configuration.",
"default": {
"dangerousDisableAssetCspModification": false,
"dangerousRemoteDomainIpcAccess": [],
"freezePrototype": false
},
"allOf": [
@ -2603,6 +2605,14 @@
"$ref": "#/definitions/DisabledCspModificationKind"
}
]
},
"dangerousRemoteDomainIpcAccess": {
"description": "Allow external domains to send command to Tauri.\n\nBy default, external domains do not have access to `window.__TAURI__`, which means they cannot communicate with the commands defined in Rust. This prevents attacks where an externally loaded malicious or compromised sites could start executing commands on the user's device.\n\nThis configuration allows a set of external domains to have access to the Tauri commands. When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed.\n\n**WARNING:** Only use this option if you either have internal checks against malicious external sites or you can trust the allowed external sites. You application might be vulnerable to dangerous Tauri command related attacks otherwise.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/RemoteDomainAccessScope"
}
}
},
"additionalProperties": false
@ -2655,6 +2665,48 @@
}
]
},
"RemoteDomainAccessScope": {
"description": "External command access definition.",
"type": "object",
"required": [
"domain",
"windows"
],
"properties": {
"scheme": {
"description": "The URL scheme to allow. By default, all schemas are allowed.",
"type": [
"string",
"null"
]
},
"domain": {
"description": "The domain to allow.",
"type": "string"
},
"windows": {
"description": "The list of window labels this scope applies to.",
"type": "array",
"items": {
"type": "string"
}
},
"plugins": {
"description": "The list of plugins that are allowed in this scope.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"enableTauriAPI": {
"description": "Enables access to the Tauri API.",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false
},
"UpdaterConfig": {
"description": "The Updater configuration object.\n\nSee more: https://tauri.app/v1/api/config#updaterconfig",
"type": "object",

View File

@ -214,6 +214,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();
@ -235,6 +236,7 @@ impl<T: UserEvent> Context<T> {
};
Ok(DetachedWindow {
label,
current_url,
dispatcher,
menu_ids,
js_event_listeners,
@ -1985,6 +1987,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();
@ -2011,6 +2014,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
Ok(DetachedWindow {
label,
current_url,
dispatcher,
menu_ids,
js_event_listeners,
@ -3040,7 +3044,7 @@ fn create_webview<T: UserEvent>(
mut window_builder,
ipc_handler,
label,
url,
current_url,
menu_ids,
js_event_listeners,
..
@ -3089,7 +3093,7 @@ 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)
.with_accept_first_mouse(webview_attributes.accept_first_mouse);
@ -3124,6 +3128,7 @@ fn create_webview<T: UserEvent>(
webview_builder = webview_builder.with_ipc_handler(create_ipc_handler(
context,
label.clone(),
current_url,
menu_ids,
js_event_listeners,
handler,
@ -3234,6 +3239,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>>,
@ -3242,6 +3248,7 @@ fn create_ipc_handler<T: UserEvent>(
let window_id = context.webview_id_map.get(&window.id()).unwrap();
handler(
DetachedWindow {
current_url: current_url.clone(),
dispatcher: WryDispatcher {
window_id,
context: context.clone(),

View File

@ -225,9 +225,6 @@ 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>>>,
@ -236,6 +233,9 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
/// 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 {
@ -272,10 +272,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())),
})
}
}
@ -302,10 +302,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())),
})
}
}
@ -346,6 +346,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,
@ -362,6 +365,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(),

View File

@ -1196,6 +1196,25 @@ impl Default for DisabledCspModificationKind {
}
}
/// External command access definition.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RemoteDomainAccessScope {
/// The URL scheme to allow. By default, all schemas are allowed.
pub scheme: Option<String>,
/// The domain to allow.
pub domain: String,
/// The list of window labels this scope applies to.
pub windows: Vec<String>,
/// The list of plugins that are allowed in this scope.
#[serde(default)]
pub plugins: Vec<String>,
/// Enables access to the Tauri API.
#[serde(default, rename = "enableTauriAPI", alias = "enable-tauri-api")]
pub enable_tauri_api: bool,
}
/// Security configuration.
///
/// See more: https://tauri.app/v1/api/config#securityconfig
@ -1233,6 +1252,20 @@ pub struct SecurityConfig {
/// Your application might be vulnerable to XSS attacks without this Tauri protection.
#[serde(default, alias = "dangerous-disable-asset-csp-modification")]
pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
/// Allow external domains to send command to Tauri.
///
/// By default, external domains do not have access to `window.__TAURI__`, which means they cannot
/// communicate with the commands defined in Rust. This prevents attacks where an externally
/// loaded malicious or compromised sites could start executing commands on the user's device.
///
/// This configuration allows a set of external domains to have access to the Tauri commands.
/// When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed.
///
/// **WARNING:** Only use this option if you either have internal checks against malicious
/// external sites or you can trust the allowed external sites. You application might be
/// vulnerable to dangerous Tauri command related attacks otherwise.
#[serde(default, alias = "dangerous-remote-domain-ipc-access")]
pub dangerous_remote_domain_ipc_access: Vec<RemoteDomainAccessScope>,
}
/// Defines an allowlist type.
@ -3590,12 +3623,34 @@ mod build {
}
}
impl ToTokens for RemoteDomainAccessScope {
fn to_tokens(&self, tokens: &mut TokenStream) {
let scheme = opt_str_lit(self.scheme.as_ref());
let domain = str_lit(&self.domain);
let windows = vec_lit(&self.windows, str_lit);
let plugins = vec_lit(&self.plugins, str_lit);
let enable_tauri_api = self.enable_tauri_api;
literal_struct!(
tokens,
RemoteDomainAccessScope,
scheme,
domain,
windows,
plugins,
enable_tauri_api
);
}
}
impl ToTokens for SecurityConfig {
fn to_tokens(&self, tokens: &mut TokenStream) {
let csp = opt_lit(self.csp.as_ref());
let dev_csp = opt_lit(self.dev_csp.as_ref());
let freeze_prototype = self.freeze_prototype;
let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;
let dangerous_remote_domain_ipc_access =
vec_lit(&self.dangerous_remote_domain_ipc_access, identity);
literal_struct!(
tokens,
@ -3603,7 +3658,8 @@ mod build {
csp,
dev_csp,
freeze_prototype,
dangerous_disable_asset_csp_modification
dangerous_disable_asset_csp_modification,
dangerous_remote_domain_ipc_access
);
}
}
@ -3868,6 +3924,7 @@ mod test {
dev_csp: None,
freeze_prototype: false,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
dangerous_remote_domain_ipc_access: Vec::new(),
},
allowlist: AllowlistConfig::default(),
system_tray: None,

View File

@ -2,35 +2,33 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
if (window.location.origin.startsWith(__TEMPLATE_origin__)) {
__RAW_freeze_prototype__
; (function () {
__RAW_freeze_prototype__
;(function () {
; (function () {
__RAW_hotkeys__
})()
__RAW_pattern_script__
__RAW_pattern_script__
__RAW_ipc_script__
;(function () {
__RAW_ipc_script__
; (function () {
__RAW_bundle_script__
})()
__RAW_listen_function__
__RAW_listen_function__
__RAW_core_script__
__RAW_core_script__
__RAW_event_initialization_script__
__RAW_event_initialization_script__
if (window.ipc) {
if (window.ipc) {
window.__TAURI_INVOKE__('__initialized', { url: window.location.href })
} else {
window.addEventListener('DOMContentLoaded', function () {
window.__TAURI_INVOKE__('__initialized', { url: window.location.href })
} else {
window.addEventListener('DOMContentLoaded', function () {
window.__TAURI_INVOKE__('__initialized', { url: window.location.href })
})
}
__RAW_plugin_initialization_script__
})
}
__RAW_plugin_initialization_script__
})()

View File

@ -3,15 +3,13 @@
// SPDX-License-Identifier: MIT
window.addEventListener('DOMContentLoaded', () => {
if (window.location.origin.startsWith(__TEMPLATE_origin__)) {
let style = document.createElement('style')
style.textContent = __TEMPLATE_style__
document.head.append(style)
let style = document.createElement('style')
style.textContent = __TEMPLATE_style__
document.head.append(style)
let iframe = document.createElement('iframe')
iframe.id = '__tauri_isolation__'
iframe.sandbox.add('allow-scripts')
iframe.src = __TEMPLATE_isolation_src__
document.body.append(iframe)
}
let iframe = document.createElement('iframe')
iframe.id = '__tauri_isolation__'
iframe.sandbox.add('allow-scripts')
iframe.src = __TEMPLATE_isolation_src__
document.body.append(iframe)
})

View File

@ -19,7 +19,7 @@ use crate::{
window::{PendingWindow, WindowEvent as RuntimeWindowEvent},
ExitRequestedEventAction, RunEvent as RuntimeRunEvent,
},
scope::FsScope,
scope::{FsScope, IpcScope},
sealed::{ManagerBase, RuntimeOrDispatch},
utils::config::Config,
utils::{assets::Assets, resources::resource_relpath, Env},
@ -1049,7 +1049,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:
format!("Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {{ value: (message) => window.ipc.postMessage({}(message)) }})", crate::manager::STRINGIFY_IPC_MESSAGE_FN),
@ -1571,10 +1571,10 @@ impl<R: Runtime> Builder<R> {
let mut webview_attributes =
WebviewAttributes::new(url).accept_first_mouse(config.accept_first_mouse);
if let Some(ua) = &config.user_agent {
webview_attributes = webview_attributes.user_agent(&ua.to_string());
webview_attributes = webview_attributes.user_agent(ua);
}
if let Some(args) = &config.additional_browser_args {
webview_attributes = webview_attributes.additional_browser_args(&args.to_string());
webview_attributes = webview_attributes.additional_browser_args(args);
}
if !config.file_drop_enabled {
webview_attributes = webview_attributes.disable_file_drop_handler();
@ -1627,6 +1627,7 @@ impl<R: Runtime> Builder<R> {
let env = Env::default();
app.manage(Scopes {
ipc: IpcScope::new(&app.config(), &app.manager),
fs: FsScope::for_fs_api(
&app.manager.config(),
app.package_info(),

View File

@ -39,7 +39,6 @@ pub(crate) struct IpcJavascript<'a> {
#[derive(Template)]
#[default_template("../scripts/isolation.js")]
pub(crate) struct IsolationJavascript<'a> {
pub(crate) origin: String,
pub(crate) isolation_src: &'a str,
pub(crate) style: &'a str,
}

View File

@ -779,6 +779,11 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
self.state::<Scopes>().inner().fs.clone()
}
/// Gets the scope for the IPC.
fn ipc_scope(&self) -> IpcScope {
self.state::<Scopes>().inner().ipc.clone()
}
/// Gets the scope for the asset protocol.
#[cfg(protocol_asset)]
fn asset_protocol_scope(&self) -> FsScope {

View File

@ -28,7 +28,7 @@ use tauri_utils::{
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},
@ -142,7 +142,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()
@ -234,7 +234,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> {
@ -370,21 +370,16 @@ 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()),
}
}
/// Get the origin as it will be seen in the webview.
fn get_browser_origin(&self) -> String {
match self.base_path() {
AppUrl::Url(WindowUrl::External(url)) => url.origin().ascii_serialization(),
_ => format_real_schema("tauri"),
}
}
fn csp(&self) -> Option<Csp> {
if cfg!(feature = "custom-protocol") {
self.inner.config.tauri.security.csp.clone()
@ -458,7 +453,6 @@ impl<R: Runtime> WindowManager<R> {
if let Pattern::Isolation { schema, .. } = self.pattern() {
webview_attributes = webview_attributes.initialization_script(
&IsolationJavascript {
origin: self.get_browser_origin(),
isolation_src: &crate::pattern::format_real_schema(schema),
style: tauri_utils::pattern::isolation::IFRAME_STYLE,
}
@ -480,7 +474,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())
@ -943,7 +937,6 @@ impl<R: Runtime> WindowManager<R> {
#[derive(Template)]
#[default_template("../scripts/init.js")]
struct InitJavascript<'a> {
origin: String,
#[raw]
pattern_script: &'a str,
#[raw]
@ -1006,7 +999,6 @@ impl<R: Runtime> WindowManager<R> {
let hotkeys = "";
InitJavascript {
origin: self.get_browser_origin(),
pattern_script,
ipc_script,
bundle_script,
@ -1076,7 +1068,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/");
@ -1127,27 +1128,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!(),
};
@ -1174,7 +1169,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() {
@ -1190,17 +1185,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
@ -1225,6 +1218,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)
}

View File

@ -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://{schema}.localhost")
format!("https://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}")
} else {
format!("{schema}://localhost")
format!("{schema}://{ISOLATION_IFRAME_SRC_DOMAIN}")
}
}

430
core/tauri/src/scope/ipc.rs Normal file
View File

@ -0,0 +1,430 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::{Arc, Mutex};
use crate::{manager::WindowManager, Config, Runtime, Window};
#[cfg(feature = "isolation")]
use crate::{pattern::ISOLATION_IFRAME_SRC_DOMAIN, sealed::ManagerBase, Pattern};
use url::Url;
/// IPC access configuration for a remote domain.
#[derive(Debug, Clone)]
pub struct RemoteDomainAccessScope {
scheme: Option<String>,
domain: String,
windows: Vec<String>,
plugins: Vec<String>,
enable_tauri_api: bool,
}
impl RemoteDomainAccessScope {
/// Creates a new access scope.
pub fn new(domain: impl Into<String>) -> Self {
Self {
scheme: None,
domain: domain.into(),
windows: Vec::new(),
plugins: Vec::new(),
enable_tauri_api: false,
}
}
/// Sets the scheme of the URL to allow in this scope. By default, all schemes with the given domain are allowed.
pub fn allow_on_scheme(mut self, scheme: impl Into<String>) -> Self {
self.scheme.replace(scheme.into());
self
}
/// Adds the given window label to the list of windows that uses this scope.
pub fn add_window(mut self, window: impl Into<String>) -> Self {
self.windows.push(window.into());
self
}
/// Adds the given plugin to the allowed plugin list.
pub fn add_plugin(mut self, plugin: impl Into<String>) -> Self {
self.plugins.push(plugin.into());
self
}
/// Enables access to the Tauri API.
pub fn enable_tauri_api(mut self) -> Self {
self.enable_tauri_api = true;
self
}
/// The domain of the URLs that can access this scope.
pub fn domain(&self) -> &str {
&self.domain
}
/// The list of window labels that can access this scope.
pub fn windows(&self) -> &Vec<String> {
&self.windows
}
/// The list of plugins enabled by this scope.
pub fn plugins(&self) -> &Vec<String> {
&self.plugins
}
/// Whether this scope enables Tauri API access or not.
pub fn enables_tauri_api(&self) -> bool {
self.enable_tauri_api
}
}
pub(crate) struct RemoteAccessError {
pub matches_window: bool,
pub matches_domain: bool,
}
/// IPC scope.
#[derive(Clone)]
pub struct Scope {
remote_access: Arc<Mutex<Vec<RemoteDomainAccessScope>>>,
}
impl Scope {
#[allow(unused_variables)]
pub(crate) fn new<R: Runtime>(config: &Config, manager: &WindowManager<R>) -> Self {
#[allow(unused_mut)]
let mut remote_access: Vec<RemoteDomainAccessScope> = config
.tauri
.security
.dangerous_remote_domain_ipc_access
.clone()
.into_iter()
.map(|s| RemoteDomainAccessScope {
scheme: s.scheme,
domain: s.domain,
windows: s.windows,
plugins: s.plugins,
enable_tauri_api: s.enable_tauri_api,
})
.collect();
#[cfg(feature = "isolation")]
if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
remote_access.push(RemoteDomainAccessScope {
scheme: Some(schema.clone()),
domain: ISOLATION_IFRAME_SRC_DOMAIN.into(),
windows: Vec::new(),
plugins: Vec::new(),
enable_tauri_api: true,
});
}
Self {
remote_access: Arc::new(Mutex::new(remote_access)),
}
}
/// Adds the given configuration for remote access.
///
/// # Examples
///
/// ```
/// use tauri::{Manager, scope::ipc::RemoteDomainAccessScope};
/// tauri::Builder::default()
/// .setup(|app| {
/// app.ipc_scope().configure_remote_access(
/// RemoteDomainAccessScope::new("tauri.app")
/// .add_window("main")
/// .enable_tauri_api()
/// );
/// Ok(())
/// });
/// ```
pub fn configure_remote_access(&self, access: RemoteDomainAccessScope) {
self.remote_access.lock().unwrap().push(access);
}
pub(crate) fn remote_access_for<R: Runtime>(
&self,
window: &Window<R>,
url: &Url,
) -> Result<RemoteDomainAccessScope, RemoteAccessError> {
let mut scope = None;
let mut found_scope_for_window = false;
let mut found_scope_for_domain = false;
let label = window.label().to_string();
for s in &*self.remote_access.lock().unwrap() {
#[allow(unused_mut)]
let mut matches_window = s.windows.contains(&label);
// the isolation iframe is always able to access the IPC
#[cfg(feature = "isolation")]
if let Pattern::Isolation { schema, .. } = &window.manager().inner.pattern {
if schema == url.scheme() && url.domain() == Some(ISOLATION_IFRAME_SRC_DOMAIN) {
matches_window = true;
}
}
let matches_scheme = s
.scheme
.as_ref()
.map(|scheme| scheme == url.scheme())
.unwrap_or(true);
let matches_domain =
matches_scheme && url.domain().map(|d| d == s.domain).unwrap_or_default();
found_scope_for_window = found_scope_for_window || matches_window;
found_scope_for_domain = found_scope_for_domain || matches_domain;
if matches_window && matches_domain && scope.is_none() {
scope.replace(s.clone());
}
}
if let Some(s) = scope {
Ok(s)
} else {
Err(RemoteAccessError {
matches_window: found_scope_for_window,
matches_domain: found_scope_for_domain,
})
}
}
}
#[cfg(test)]
mod tests {
use super::RemoteDomainAccessScope;
use crate::{api::ipc::CallbackFn, test::MockRuntime, App, InvokePayload, Manager, Window};
const PLUGIN_NAME: &str = "test";
fn test_context(scopes: Vec<RemoteDomainAccessScope>) -> (App<MockRuntime>, Window<MockRuntime>) {
let app = crate::test::mock_app();
let window = app.get_window("main").unwrap();
for scope in scopes {
app.ipc_scope().configure_remote_access(scope);
}
(app, window)
}
fn assert_ipc_response(
window: &Window<MockRuntime>,
payload: InvokePayload,
expected: Result<&str, &str>,
) {
let callback = payload.callback;
let error = payload.error;
window.clone().on_message(payload).unwrap();
let mut num_tries = 0;
let evaluated_script = loop {
std::thread::sleep(std::time::Duration::from_millis(50));
let evaluated_script = window.dispatcher().last_evaluated_script();
if let Some(s) = evaluated_script {
break s;
}
num_tries += 1;
if num_tries == 20 {
panic!("Response script not evaluated");
}
};
let (expected_response, fn_name) = match expected {
Ok(payload) => (payload, callback),
Err(payload) => (payload, error),
};
let expected = format!(
"window[\"_{}\"]({})",
fn_name.0,
crate::api::ipc::serialize_js(&expected_response).unwrap()
);
println!("Last evaluated script:");
println!("{evaluated_script}");
println!("Expected:");
println!("{expected}");
assert!(evaluated_script.contains(&expected));
}
fn app_version_payload() -> InvokePayload {
let callback = CallbackFn(0);
let error = CallbackFn(1);
let mut payload = serde_json::Map::new();
let mut msg = serde_json::Map::new();
msg.insert(
"cmd".into(),
serde_json::Value::String("getAppVersion".into()),
);
payload.insert("message".into(), serde_json::Value::Object(msg));
InvokePayload {
cmd: "".into(),
tauri_module: Some("App".into()),
callback,
error,
inner: serde_json::Value::Object(payload),
}
}
fn plugin_test_payload() -> InvokePayload {
let callback = CallbackFn(0);
let error = CallbackFn(1);
InvokePayload {
cmd: format!("plugin:{PLUGIN_NAME}|doSomething"),
tauri_module: None,
callback,
error,
inner: Default::default(),
}
}
#[test]
fn scope_not_defined() {
let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("app.tauri.app")
.add_window("other")
.enable_tauri_api()]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(&crate::window::ipc_scope_not_found_error_message(
"main",
"https://tauri.app/",
)),
);
}
#[test]
fn scope_not_defined_for_window() {
let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
.add_window("second")
.enable_tauri_api()]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(&crate::window::ipc_scope_window_error_message("main")),
);
}
#[test]
fn scope_not_defined_for_url() {
let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("github.com")
.add_window("main")
.enable_tauri_api()]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(&crate::window::ipc_scope_domain_error_message(
"https://tauri.app/",
)),
);
}
#[test]
fn subdomain_is_not_allowed() {
let (app, mut window) = test_context(vec![
RemoteDomainAccessScope::new("tauri.app")
.add_window("main")
.enable_tauri_api(),
RemoteDomainAccessScope::new("sub.tauri.app")
.add_window("main")
.enable_tauri_api(),
]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Ok(app.package_info().version.to_string().as_str()),
);
window.navigate("https://blog.tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(&crate::window::ipc_scope_domain_error_message(
"https://blog.tauri.app/",
)),
);
window.navigate("https://sub.tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Ok(app.package_info().version.to_string().as_str()),
);
window.window.label = "test".into();
window.navigate("https://dev.tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(&crate::window::ipc_scope_not_found_error_message(
"test",
"https://dev.tauri.app/",
)),
);
}
#[test]
fn subpath_is_allowed() {
let (app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
.add_window("main")
.enable_tauri_api()]);
window.navigate("https://tauri.app/inner/path".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Ok(app.package_info().version.to_string().as_str()),
);
}
#[test]
fn tauri_api_not_allowed() {
let (_app, window) = test_context(vec![
RemoteDomainAccessScope::new("tauri.app").add_window("main")
]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
app_version_payload(),
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
);
}
#[test]
fn plugin_allowed() {
let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
.add_window("main")
.add_plugin(PLUGIN_NAME)]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
plugin_test_payload(),
Err(&format!("plugin {PLUGIN_NAME} not found")),
);
}
#[test]
fn plugin_not_allowed() {
let (_app, window) = test_context(vec![
RemoteDomainAccessScope::new("tauri.app").add_window("main")
]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
plugin_test_payload(),
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
);
}
}

View File

@ -4,10 +4,13 @@
mod fs;
mod http;
/// IPC scope.
pub mod ipc;
#[cfg(shell_scope)]
mod shell;
pub use self::http::Scope as HttpScope;
pub use self::ipc::Scope as IpcScope;
pub use fs::{Event as FsScopeEvent, Pattern as GlobPattern, Scope as FsScope};
#[cfg(shell_scope)]
pub use shell::{
@ -18,6 +21,7 @@ pub use shell::{
use std::path::Path;
pub(crate) struct Scopes {
pub ipc: IpcScope,
pub fs: FsScope,
#[cfg(protocol_asset)]
pub asset_protocol: FsScope,

View File

@ -69,8 +69,10 @@ 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(),
last_evaluated_script: Default::default(),
},
menu_ids: Default::default(),
js_event_listeners: Default::default(),
@ -111,6 +113,13 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
#[derive(Debug, Clone)]
pub struct MockDispatcher {
context: RuntimeContext,
last_evaluated_script: Arc<Mutex<Option<String>>>,
}
impl MockDispatcher {
pub fn last_evaluated_script(&self) -> Option<String> {
self.last_evaluated_script.lock().unwrap().clone()
}
}
#[cfg(all(desktop, feature = "global-shortcut"))]
@ -558,6 +567,11 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
}
fn eval_script<S: Into<String>>(&self, script: S) -> Result<()> {
self
.last_evaluated_script
.lock()
.unwrap()
.replace(script.into());
Ok(())
}
@ -689,8 +703,10 @@ 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(),
last_evaluated_script: Default::default(),
},
menu_ids: Default::default(),
js_event_listeners: Default::default(),

View File

@ -317,13 +317,13 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
self.label.clone(),
)?;
let labels = self.manager.labels().into_iter().collect::<Vec<_>>();
let mut pending = self.manager.prepare_window(
let pending = self.manager.prepare_window(
self.app_handle.clone(),
pending,
&labels,
web_resource_request_handler,
)?;
pending.navigation_handler = self.navigation_handler.take();
let window = match &mut self.runtime {
RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending),
RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending),
@ -678,7 +678,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>,
@ -1384,13 +1384,39 @@ impl<R: Runtime> Window<R> {
/// Webview APIs.
impl<R: Runtime> Window<R> {
/// Returns the current url of the webview.
pub fn url(&self) -> crate::Result<Url> {
self.window.dispatcher.url().map_err(Into::into)
pub fn url(&self) -> Url {
self.window.current_url.lock().unwrap().clone()
}
#[cfg(test)]
pub(crate) fn navigate(&self, url: Url) {
*self.window.current_url.lock().unwrap() = url;
}
/// 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();
let mut scope_not_found_error_message =
ipc_scope_not_found_error_message(&self.window.label, current_url.as_str());
let scope = if is_local {
None
} else {
match self.ipc_scope().remote_access_for(&self, &current_url) {
Ok(scope) => Some(scope),
Err(e) => {
if e.matches_window {
scope_not_found_error_message = ipc_scope_domain_error_message(current_url.as_str());
} else if e.matches_domain {
scope_not_found_error_message = ipc_scope_window_error_message(&self.window.label);
}
None
}
}
};
match payload.cmd.as_str() {
"__initialized" => {
let payload: PageLoadPayload = serde_json::from_value(payload.inner)?;
@ -1404,9 +1430,18 @@ impl<R: Runtime> Window<R> {
payload.inner,
);
let resolver = InvokeResolver::new(self, payload.callback, payload.error);
let invoke = Invoke { message, resolver };
if !is_local && scope.is_none() {
invoke.resolver.reject(scope_not_found_error_message);
return Ok(());
}
if let Some(module) = &payload.tauri_module {
if !is_local && scope.map(|s| !s.enables_tauri_api()).unwrap_or_default() {
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
return Ok(());
}
crate::endpoints::handle(
module.to_string(),
invoke,
@ -1414,6 +1449,17 @@ impl<R: Runtime> Window<R> {
manager.package_info(),
);
} else if payload.cmd.starts_with("plugin:") {
if !is_local {
let command = invoke.message.command.replace("plugin:", "");
let plugin_name = command.split('|').next().unwrap().to_string();
if !scope
.map(|s| s.plugins().contains(&plugin_name))
.unwrap_or(true)
{
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
return Ok(());
}
}
manager.extend_api(invoke);
} else {
manager.run_invoke_handler(invoke);
@ -1645,6 +1691,20 @@ impl<R: Runtime> Window<R> {
}
}
pub(crate) const IPC_SCOPE_DOES_NOT_ALLOW: &str = "Not allowed by the scope";
pub(crate) fn ipc_scope_not_found_error_message(label: &str, url: &str) -> String {
format!("Scope not defined for window `{label}` and URL `{url}`. See https://tauri.app/v1/api/config/#securityconfig.dangerousremotedomainipcaccess and https://docs.rs/tauri/1/tauri/scope/struct.IpcScope.html#method.configure_remote_access")
}
pub(crate) fn ipc_scope_window_error_message(label: &str) -> String {
format!("Scope not defined for window `{}`. See https://tauri.app/v1/api/config/#securityconfig.dangerousremotedomainipcaccess and https://docs.rs/tauri/1/tauri/scope/struct.IpcScope.html#method.configure_remote_access", label)
}
pub(crate) fn ipc_scope_domain_error_message(url: &str) -> String {
format!("Scope not defined for URL `{url}`. See https://tauri.app/v1/api/config/#securityconfig.dangerousremotedomainipcaccess and https://docs.rs/tauri/1/tauri/scope/struct.IpcScope.html#method.configure_remote_access")
}
#[cfg(test)]
mod tests {
#[test]

View File

@ -163,6 +163,7 @@
},
"security": {
"dangerousDisableAssetCspModification": false,
"dangerousRemoteDomainIpcAccess": [],
"freezePrototype": false
},
"updater": {
@ -415,6 +416,7 @@
"description": "Security configuration.",
"default": {
"dangerousDisableAssetCspModification": false,
"dangerousRemoteDomainIpcAccess": [],
"freezePrototype": false
},
"allOf": [
@ -2603,6 +2605,14 @@
"$ref": "#/definitions/DisabledCspModificationKind"
}
]
},
"dangerousRemoteDomainIpcAccess": {
"description": "Allow external domains to send command to Tauri.\n\nBy default, external domains do not have access to `window.__TAURI__`, which means they cannot communicate with the commands defined in Rust. This prevents attacks where an externally loaded malicious or compromised sites could start executing commands on the user's device.\n\nThis configuration allows a set of external domains to have access to the Tauri commands. When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed.\n\n**WARNING:** Only use this option if you either have internal checks against malicious external sites or you can trust the allowed external sites. You application might be vulnerable to dangerous Tauri command related attacks otherwise.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/RemoteDomainAccessScope"
}
}
},
"additionalProperties": false
@ -2655,6 +2665,48 @@
}
]
},
"RemoteDomainAccessScope": {
"description": "External command access definition.",
"type": "object",
"required": [
"domain",
"windows"
],
"properties": {
"scheme": {
"description": "The URL scheme to allow. By default, all schemas are allowed.",
"type": [
"string",
"null"
]
},
"domain": {
"description": "The domain to allow.",
"type": "string"
},
"windows": {
"description": "The list of window labels this scope applies to.",
"type": "array",
"items": {
"type": "string"
}
},
"plugins": {
"description": "The list of plugins that are allowed in this scope.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"enableTauriAPI": {
"description": "Enables access to the Tauri API.",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false
},
"UpdaterConfig": {
"description": "The Updater configuration object.\n\nSee more: https://tauri.app/v1/api/config#updaterconfig",
"type": "object",