Skip to content

Commit

Permalink
feat(core): improve RPC security, closes #814 (#2047)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Jun 22, 2021
1 parent 030c9c7 commit 160fb05
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 46 deletions.
6 changes: 6 additions & 0 deletions .changes/rpc-security.md
@@ -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.
2 changes: 2 additions & 0 deletions core/tauri-runtime/src/webview.rs
Expand Up @@ -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,
}
2 changes: 1 addition & 1 deletion core/tauri/scripts/bundle.js

Large diffs are not rendered by default.

38 changes: 20 additions & 18 deletions core/tauri/scripts/core.js
Expand Up @@ -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);
Expand All @@ -118,6 +118,7 @@ if (!String.prototype.startsWith) {
{
callback: callback,
error: error,
__invokeKey: key || __TAURI_INVOKE_KEY__,
},
args
)
Expand All @@ -130,6 +131,7 @@ if (!String.prototype.startsWith) {
{
callback: callback,
error: error,
__invokeKey: key || __TAURI_INVOKE_KEY__,
},
args
)
Expand All @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -212,7 +214,7 @@ if (!String.prototype.startsWith) {
}
}),
},
});
}, _KEY_VALUE_);

let permissionSettable = false;
let permissionValue = "default";
Expand All @@ -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) {
Expand All @@ -242,7 +244,7 @@ if (!String.prototype.startsWith) {
message: {
cmd: "requestNotificationPermission",
},
})
}, _KEY_VALUE_)
.then(function (permission) {
setNotificationPermission(permission);
return permission;
Expand All @@ -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",
Expand All @@ -267,7 +269,7 @@ if (!String.prototype.startsWith) {
}
: options,
},
});
}, _KEY_VALUE_);
}
});
}
Expand Down Expand Up @@ -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_);
}
}
})();
72 changes: 62 additions & 10 deletions core/tauri/src/manager.rs
Expand Up @@ -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>,
}
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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(),
}
}
Expand Down Expand Up @@ -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>,
Expand All @@ -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#"
Expand All @@ -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());
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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!
Expand Down Expand Up @@ -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}'] = [];
Expand All @@ -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]
Expand All @@ -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()
Expand Down
19 changes: 13 additions & 6 deletions core/tauri/src/window.rs
Expand Up @@ -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
);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api/config.md
Expand Up @@ -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
Expand Down

0 comments on commit 160fb05

Please sign in to comment.