feat(core): improve RPC security, closes #814 (#2047)

This commit is contained in:
Lucas Fernandes Nogueira 2021-06-22 17:29:10 -03:00 committed by GitHub
parent 030c9c736c
commit 160fb0529f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 124 additions and 46 deletions

6
.changes/rpc-security.md Normal file
View 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@
"../index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": ""
"beforeBuildCommand": "",
"withGlobalTauri": true
},
"tauri": {
"bundle": {

View File

@ -7,7 +7,8 @@
"../index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": ""
"beforeBuildCommand": "",
"withGlobalTauri": true
},
"tauri": {
"bundle": {

View File

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

View File

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

View File

@ -3,7 +3,8 @@
"distDir": "../public",
"devPath": "../public",
"beforeDevCommand": "",
"beforeBuildCommand": ""
"beforeBuildCommand": "",
"withGlobalTauri": true
},
"tauri": {
"bundle": {

View File

@ -3,7 +3,8 @@
"distDir": "../public",
"devPath": "../public",
"beforeDevCommand": "",
"beforeBuildCommand": ""
"beforeBuildCommand": "",
"withGlobalTauri": true
},
"tauri": {
"bundle": {

View File

@ -3,7 +3,8 @@
"distDir": "../public",
"devPath": "../public",
"beforeDevCommand": "",
"beforeBuildCommand": ""
"beforeBuildCommand": "",
"withGlobalTauri": true
},
"tauri": {
"bundle": {