mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-24 19:25:12 +03:00
parent
030c9c736c
commit
160fb0529f
6
.changes/rpc-security.md
Normal file
6
.changes/rpc-security.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"api": patch
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Improve RPC security by requiring a numeric code to invoke commands. The codes are generated by the Rust side and injected into the app's code using a closure, so external scripts can't access the backend. This change doesn't protect `withGlobalTauri` (`window.__TAURI__`) usage.
|
@ -228,6 +228,8 @@ pub struct InvokePayload {
|
||||
pub tauri_module: Option<String>,
|
||||
pub callback: String,
|
||||
pub error: String,
|
||||
#[serde(rename = "__invokeKey")]
|
||||
pub key: u32,
|
||||
#[serde(flatten)]
|
||||
pub inner: JsonValue,
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -92,7 +92,7 @@ if (!String.prototype.startsWith) {
|
||||
return identifier;
|
||||
};
|
||||
|
||||
window.__TAURI__.invoke = function invoke(cmd, args = {}) {
|
||||
window.__TAURI__._invoke = function invoke(cmd, args = {}, key = null) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var callback = window.__TAURI__.transformCallback(function (r) {
|
||||
resolve(r);
|
||||
@ -118,6 +118,7 @@ if (!String.prototype.startsWith) {
|
||||
{
|
||||
callback: callback,
|
||||
error: error,
|
||||
__invokeKey: key || __TAURI_INVOKE_KEY__,
|
||||
},
|
||||
args
|
||||
)
|
||||
@ -130,6 +131,7 @@ if (!String.prototype.startsWith) {
|
||||
{
|
||||
callback: callback,
|
||||
error: error,
|
||||
__invokeKey: key || __TAURI_INVOKE_KEY__,
|
||||
},
|
||||
args
|
||||
)
|
||||
@ -154,13 +156,13 @@ if (!String.prototype.startsWith) {
|
||||
target.href.startsWith("http") &&
|
||||
target.target === "_blank"
|
||||
) {
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Shell",
|
||||
message: {
|
||||
cmd: "open",
|
||||
path: target.href,
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
@ -191,16 +193,16 @@ if (!String.prototype.startsWith) {
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
// start dragging if the element has a `tauri-drag-region` data attribute
|
||||
if (e.target.hasAttribute('data-tauri-drag-region') && e.buttons === 1) {
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Window",
|
||||
message: {
|
||||
cmd: "startDragging",
|
||||
}
|
||||
})
|
||||
}, _KEY_VALUE_)
|
||||
}
|
||||
})
|
||||
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Event",
|
||||
message: {
|
||||
cmd: "listen",
|
||||
@ -212,7 +214,7 @@ if (!String.prototype.startsWith) {
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
|
||||
let permissionSettable = false;
|
||||
let permissionValue = "default";
|
||||
@ -221,12 +223,12 @@ if (!String.prototype.startsWith) {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
return window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Notification",
|
||||
message: {
|
||||
cmd: "isNotificationPermissionGranted",
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
}
|
||||
|
||||
function setNotificationPermission(value) {
|
||||
@ -242,7 +244,7 @@ if (!String.prototype.startsWith) {
|
||||
message: {
|
||||
cmd: "requestNotificationPermission",
|
||||
},
|
||||
})
|
||||
}, _KEY_VALUE_)
|
||||
.then(function (permission) {
|
||||
setNotificationPermission(permission);
|
||||
return permission;
|
||||
@ -256,7 +258,7 @@ if (!String.prototype.startsWith) {
|
||||
|
||||
isPermissionGranted().then(function (permission) {
|
||||
if (permission) {
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
return window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Notification",
|
||||
message: {
|
||||
cmd: "notification",
|
||||
@ -267,7 +269,7 @@ if (!String.prototype.startsWith) {
|
||||
}
|
||||
: options,
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -305,34 +307,34 @@ if (!String.prototype.startsWith) {
|
||||
});
|
||||
|
||||
window.alert = function (message) {
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Dialog",
|
||||
message: {
|
||||
cmd: "messageDialog",
|
||||
message: message,
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
};
|
||||
|
||||
window.confirm = function (message) {
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
return window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Dialog",
|
||||
message: {
|
||||
cmd: "askDialog",
|
||||
message: message,
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
};
|
||||
|
||||
// window.print works on Linux/Windows; need to use the API on macOS
|
||||
if (navigator.userAgent.includes('Mac')) {
|
||||
window.print = function () {
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
return window.__TAURI__._invoke('tauri', {
|
||||
__tauriModule: "Window",
|
||||
message: {
|
||||
cmd: "print"
|
||||
},
|
||||
});
|
||||
}, _KEY_VALUE_);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -194,6 +194,7 @@ impl<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> Params
|
||||
crate::manager::default_args! {
|
||||
pub struct WindowManager<P: Params> {
|
||||
pub inner: Arc<InnerWindowManager<P>>,
|
||||
invoke_keys: Arc<Mutex<Vec<u32>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
_marker: Args<P::Event, P::Label, P::MenuId, P::SystemTrayMenuId, P::Assets, P::Runtime>,
|
||||
}
|
||||
@ -203,6 +204,7 @@ impl<P: Params> Clone for WindowManager<P> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
invoke_keys: self.invoke_keys.clone(),
|
||||
_marker: Args::default(),
|
||||
}
|
||||
}
|
||||
@ -264,6 +266,7 @@ impl<P: Params> WindowManager<P> {
|
||||
menu_event_listeners: Arc::new(menu_event_listeners),
|
||||
window_event_listeners: Arc::new(window_event_listeners),
|
||||
}),
|
||||
invoke_keys: Default::default(),
|
||||
_marker: Args::default(),
|
||||
}
|
||||
}
|
||||
@ -301,6 +304,19 @@ impl<P: Params> WindowManager<P> {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_invoke_key(&self) -> u32 {
|
||||
let key = rand::random();
|
||||
self.invoke_keys.lock().unwrap().push(key);
|
||||
key
|
||||
}
|
||||
|
||||
/// Checks whether the invoke key is valid or not.
|
||||
///
|
||||
/// An invoke key is valid if it was generated by this manager instance.
|
||||
pub(crate) fn verify_invoke_key(&self, key: u32) -> bool {
|
||||
self.invoke_keys.lock().unwrap().contains(&key)
|
||||
}
|
||||
|
||||
fn prepare_pending_window(
|
||||
&self,
|
||||
mut pending: PendingWindow<P>,
|
||||
@ -315,7 +331,8 @@ impl<P: Params> WindowManager<P> {
|
||||
.expect("poisoned plugin store")
|
||||
.initialization_script();
|
||||
|
||||
let mut webview_attributes = pending.webview_attributes
|
||||
let mut webview_attributes = pending.webview_attributes;
|
||||
webview_attributes = webview_attributes
|
||||
.initialization_script(&self.initialization_script(&plugin_init, is_init_global))
|
||||
.initialization_script(&format!(
|
||||
r#"
|
||||
@ -326,6 +343,14 @@ impl<P: Params> WindowManager<P> {
|
||||
current_window_label = label.to_js_string()?,
|
||||
));
|
||||
|
||||
#[cfg(dev)]
|
||||
{
|
||||
webview_attributes = webview_attributes.initialization_script(&format!(
|
||||
"window.__TAURI_INVOKE_KEY__ = {}",
|
||||
self.generate_invoke_key()
|
||||
));
|
||||
}
|
||||
|
||||
if !pending.window_builder.has_icon() {
|
||||
if let Some(default_window_icon) = &self.inner.default_window_icon {
|
||||
let icon = Icon::Raw(default_window_icon.clone());
|
||||
@ -402,6 +427,7 @@ impl<P: Params> WindowManager<P> {
|
||||
|
||||
fn prepare_uri_scheme_protocol(&self) -> CustomProtocol {
|
||||
let assets = self.inner.assets.clone();
|
||||
let manager = self.clone();
|
||||
CustomProtocol {
|
||||
protocol: Box::new(move |path| {
|
||||
let mut path = path
|
||||
@ -424,6 +450,8 @@ impl<P: Params> WindowManager<P> {
|
||||
// skip leading `/`
|
||||
path.chars().skip(1).collect::<String>()
|
||||
};
|
||||
let is_javascript =
|
||||
path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
|
||||
|
||||
let asset_response = assets
|
||||
.get(&path)
|
||||
@ -435,7 +463,25 @@ impl<P: Params> WindowManager<P> {
|
||||
.ok_or(crate::Error::AssetNotFound(path))
|
||||
.map(Cow::into_owned);
|
||||
match asset_response {
|
||||
Ok(asset) => Ok(asset),
|
||||
Ok(asset) => {
|
||||
if is_javascript {
|
||||
let js = String::from_utf8_lossy(&asset).into_owned();
|
||||
Ok(
|
||||
format!(
|
||||
r#"(function () {{
|
||||
const __TAURI_INVOKE_KEY__ = {};
|
||||
{}
|
||||
}})()"#,
|
||||
manager.generate_invoke_key(),
|
||||
js
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
} else {
|
||||
Ok(asset)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("{:?}", e); // TODO log::error!
|
||||
@ -477,32 +523,37 @@ impl<P: Params> WindowManager<P> {
|
||||
plugin_initialization_script: &str,
|
||||
with_global_tauri: bool,
|
||||
) -> String {
|
||||
let key = self.generate_invoke_key();
|
||||
format!(
|
||||
r#"
|
||||
{bundle_script}
|
||||
(function () {{
|
||||
const __TAURI_INVOKE_KEY__ = {key};
|
||||
{bundle_script}
|
||||
}})()
|
||||
{core_script}
|
||||
{event_initialization_script}
|
||||
if (window.rpc) {{
|
||||
window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
|
||||
window.__TAURI__._invoke("__initialized", {{ url: window.location.href }}, {key})
|
||||
}} else {{
|
||||
window.addEventListener('DOMContentLoaded', function () {{
|
||||
window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
|
||||
window.__TAURI__._invoke("__initialized", {{ url: window.location.href }}, {key})
|
||||
}})
|
||||
}}
|
||||
{plugin_initialization_script}
|
||||
"#,
|
||||
core_script = include_str!("../scripts/core.js"),
|
||||
key = key,
|
||||
core_script = include_str!("../scripts/core.js").replace("_KEY_VALUE_", &key.to_string()),
|
||||
bundle_script = if with_global_tauri {
|
||||
include_str!("../scripts/bundle.js")
|
||||
} else {
|
||||
""
|
||||
},
|
||||
event_initialization_script = self.event_initialization_script(),
|
||||
event_initialization_script = self.event_initialization_script(key),
|
||||
plugin_initialization_script = plugin_initialization_script
|
||||
)
|
||||
}
|
||||
|
||||
fn event_initialization_script(&self) -> String {
|
||||
fn event_initialization_script(&self, key: u32) -> String {
|
||||
return format!(
|
||||
"
|
||||
window['{queue}'] = [];
|
||||
@ -516,13 +567,13 @@ impl<P: Params> WindowManager<P> {
|
||||
}}
|
||||
|
||||
if (listeners.length > 0) {{
|
||||
window.__TAURI__.invoke('tauri', {{
|
||||
window.__TAURI__._invoke('tauri', {{
|
||||
__tauriModule: 'Internal',
|
||||
message: {{
|
||||
cmd: 'validateSalt',
|
||||
salt: salt
|
||||
}}
|
||||
}}).then(function (flag) {{
|
||||
}}, {key}).then(function (flag) {{
|
||||
if (flag) {{
|
||||
for (let i = listeners.length - 1; i >= 0; i--) {{
|
||||
const listener = listeners[i]
|
||||
@ -534,6 +585,7 @@ impl<P: Params> WindowManager<P> {
|
||||
}}
|
||||
}}
|
||||
",
|
||||
key = key,
|
||||
function = self.inner.listeners.function_name(),
|
||||
queue = self.inner.listeners.queue_object_name(),
|
||||
listeners = self.inner.listeners.listeners_object_name()
|
||||
|
@ -212,13 +212,20 @@ impl<P: Params> Window<P> {
|
||||
);
|
||||
let resolver = InvokeResolver::new(self, payload.callback, payload.error);
|
||||
let invoke = Invoke { message, resolver };
|
||||
if let Some(module) = &payload.tauri_module {
|
||||
let module = module.to_string();
|
||||
crate::endpoints::handle(module, invoke, manager.config(), manager.package_info());
|
||||
} else if command.starts_with("plugin:") {
|
||||
manager.extend_api(invoke);
|
||||
if manager.verify_invoke_key(payload.key) {
|
||||
if let Some(module) = &payload.tauri_module {
|
||||
let module = module.to_string();
|
||||
crate::endpoints::handle(module, invoke, manager.config(), manager.package_info());
|
||||
} else if command.starts_with("plugin:") {
|
||||
manager.extend_api(invoke);
|
||||
} else {
|
||||
manager.run_invoke_handler(invoke);
|
||||
}
|
||||
} else {
|
||||
manager.run_invoke_handler(invoke);
|
||||
panic!(
|
||||
r#"The invoke key "{}" is invalid. This means that an external, possible malicious script is trying to access the system interface."#,
|
||||
payload.key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ It's composed of the following properties:
|
||||
{property: "devPath", type: "string", description: `Can be a path—either absolute or relative—to a folder or a URL (like a live reload server).`},
|
||||
{property: "beforeDevCommand", optional: true, type: "string", description: `A command to run before starting Tauri in dev mode.`},
|
||||
{property: "beforeBuildCommand", optional: true, type: "string", description: `A command to run before starting Tauri in build mode.`},
|
||||
{property: "withGlobalTauri", optional: true, type: "boolean", description: "Enables the API injection to the window.__TAURI__ object. Useful if you're using Vanilla JS instead of importing the API using Rollup or Webpack."}
|
||||
{property: "withGlobalTauri", optional: true, type: "boolean", description: "Enables the API injection to the window.__TAURI__ object. Useful if you're using Vanilla JS instead of importing the API using Rollup or Webpack. Reduces the command security since any external code can access it, so be careful with XSS attacks."}
|
||||
]}/>
|
||||
|
||||
```js title=Example
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -7,7 +7,8 @@
|
||||
"../index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
|
@ -7,7 +7,8 @@
|
||||
"../index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
|
@ -19,6 +19,7 @@ import * as shell from './shell'
|
||||
import * as tauri from './tauri'
|
||||
import * as updater from './updater'
|
||||
import * as window from './window'
|
||||
const invoke = tauri.invoke
|
||||
|
||||
export {
|
||||
app,
|
||||
@ -34,5 +35,6 @@ export {
|
||||
shell,
|
||||
tauri,
|
||||
updater,
|
||||
window
|
||||
window,
|
||||
invoke
|
||||
}
|
||||
|
@ -79,6 +79,8 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
|
||||
}, true)
|
||||
|
||||
window.rpc.notify(cmd, {
|
||||
// @ts-expect-error the `__TAURI_INVOKE_KEY__` variable is injected at runtime by Tauri
|
||||
__invokeKey: __TAURI_INVOKE_KEY__,
|
||||
callback,
|
||||
error,
|
||||
...args
|
||||
|
@ -3,7 +3,8 @@
|
||||
"distDir": "../public",
|
||||
"devPath": "../public",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
|
@ -3,7 +3,8 @@
|
||||
"distDir": "../public",
|
||||
"devPath": "../public",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
|
@ -3,7 +3,8 @@
|
||||
"distDir": "../public",
|
||||
"devPath": "../public",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
|
Loading…
Reference in New Issue
Block a user