Skip to content

Commit 160fb05

Browse files
authored
feat(core): improve RPC security, closes #814 (#2047)
1 parent 030c9c7 commit 160fb05

File tree

16 files changed

+124
-46
lines changed

16 files changed

+124
-46
lines changed

.changes/rpc-security.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"api": patch
3+
"tauri": patch
4+
---
5+
6+
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.

core/tauri-runtime/src/webview.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ pub struct InvokePayload {
228228
pub tauri_module: Option<String>,
229229
pub callback: String,
230230
pub error: String,
231+
#[serde(rename = "__invokeKey")]
232+
pub key: u32,
231233
#[serde(flatten)]
232234
pub inner: JsonValue,
233235
}

core/tauri/scripts/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/tauri/scripts/core.js

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ if (!String.prototype.startsWith) {
9292
return identifier;
9393
};
9494

95-
window.__TAURI__.invoke = function invoke(cmd, args = {}) {
95+
window.__TAURI__._invoke = function invoke(cmd, args = {}, key = null) {
9696
return new Promise(function (resolve, reject) {
9797
var callback = window.__TAURI__.transformCallback(function (r) {
9898
resolve(r);
@@ -118,6 +118,7 @@ if (!String.prototype.startsWith) {
118118
{
119119
callback: callback,
120120
error: error,
121+
__invokeKey: key || __TAURI_INVOKE_KEY__,
121122
},
122123
args
123124
)
@@ -130,6 +131,7 @@ if (!String.prototype.startsWith) {
130131
{
131132
callback: callback,
132133
error: error,
134+
__invokeKey: key || __TAURI_INVOKE_KEY__,
133135
},
134136
args
135137
)
@@ -154,13 +156,13 @@ if (!String.prototype.startsWith) {
154156
target.href.startsWith("http") &&
155157
target.target === "_blank"
156158
) {
157-
window.__TAURI__.invoke('tauri', {
159+
window.__TAURI__._invoke('tauri', {
158160
__tauriModule: "Shell",
159161
message: {
160162
cmd: "open",
161163
path: target.href,
162164
},
163-
});
165+
}, _KEY_VALUE_);
164166
e.preventDefault();
165167
}
166168
break;
@@ -191,16 +193,16 @@ if (!String.prototype.startsWith) {
191193
document.addEventListener('mousedown', (e) => {
192194
// start dragging if the element has a `tauri-drag-region` data attribute
193195
if (e.target.hasAttribute('data-tauri-drag-region') && e.buttons === 1) {
194-
window.__TAURI__.invoke('tauri', {
196+
window.__TAURI__._invoke('tauri', {
195197
__tauriModule: "Window",
196198
message: {
197199
cmd: "startDragging",
198200
}
199-
})
201+
}, _KEY_VALUE_)
200202
}
201203
})
202204

203-
window.__TAURI__.invoke('tauri', {
205+
window.__TAURI__._invoke('tauri', {
204206
__tauriModule: "Event",
205207
message: {
206208
cmd: "listen",
@@ -212,7 +214,7 @@ if (!String.prototype.startsWith) {
212214
}
213215
}),
214216
},
215-
});
217+
}, _KEY_VALUE_);
216218

217219
let permissionSettable = false;
218220
let permissionValue = "default";
@@ -221,12 +223,12 @@ if (!String.prototype.startsWith) {
221223
if (window.Notification.permission !== "default") {
222224
return Promise.resolve(window.Notification.permission === "granted");
223225
}
224-
return window.__TAURI__.invoke('tauri', {
226+
return window.__TAURI__._invoke('tauri', {
225227
__tauriModule: "Notification",
226228
message: {
227229
cmd: "isNotificationPermissionGranted",
228230
},
229-
});
231+
}, _KEY_VALUE_);
230232
}
231233

232234
function setNotificationPermission(value) {
@@ -242,7 +244,7 @@ if (!String.prototype.startsWith) {
242244
message: {
243245
cmd: "requestNotificationPermission",
244246
},
245-
})
247+
}, _KEY_VALUE_)
246248
.then(function (permission) {
247249
setNotificationPermission(permission);
248250
return permission;
@@ -256,7 +258,7 @@ if (!String.prototype.startsWith) {
256258

257259
isPermissionGranted().then(function (permission) {
258260
if (permission) {
259-
return window.__TAURI__.invoke('tauri', {
261+
return window.__TAURI__._invoke('tauri', {
260262
__tauriModule: "Notification",
261263
message: {
262264
cmd: "notification",
@@ -267,7 +269,7 @@ if (!String.prototype.startsWith) {
267269
}
268270
: options,
269271
},
270-
});
272+
}, _KEY_VALUE_);
271273
}
272274
});
273275
}
@@ -305,34 +307,34 @@ if (!String.prototype.startsWith) {
305307
});
306308

307309
window.alert = function (message) {
308-
window.__TAURI__.invoke('tauri', {
310+
window.__TAURI__._invoke('tauri', {
309311
__tauriModule: "Dialog",
310312
message: {
311313
cmd: "messageDialog",
312314
message: message,
313315
},
314-
});
316+
}, _KEY_VALUE_);
315317
};
316318

317319
window.confirm = function (message) {
318-
return window.__TAURI__.invoke('tauri', {
320+
return window.__TAURI__._invoke('tauri', {
319321
__tauriModule: "Dialog",
320322
message: {
321323
cmd: "askDialog",
322324
message: message,
323325
},
324-
});
326+
}, _KEY_VALUE_);
325327
};
326328

327329
// window.print works on Linux/Windows; need to use the API on macOS
328330
if (navigator.userAgent.includes('Mac')) {
329331
window.print = function () {
330-
return window.__TAURI__.invoke('tauri', {
332+
return window.__TAURI__._invoke('tauri', {
331333
__tauriModule: "Window",
332334
message: {
333335
cmd: "print"
334336
},
335-
});
337+
}, _KEY_VALUE_);
336338
}
337339
}
338340
})();

core/tauri/src/manager.rs

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ impl<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> Params
194194
crate::manager::default_args! {
195195
pub struct WindowManager<P: Params> {
196196
pub inner: Arc<InnerWindowManager<P>>,
197+
invoke_keys: Arc<Mutex<Vec<u32>>>,
197198
#[allow(clippy::type_complexity)]
198199
_marker: Args<P::Event, P::Label, P::MenuId, P::SystemTrayMenuId, P::Assets, P::Runtime>,
199200
}
@@ -203,6 +204,7 @@ impl<P: Params> Clone for WindowManager<P> {
203204
fn clone(&self) -> Self {
204205
Self {
205206
inner: self.inner.clone(),
207+
invoke_keys: self.invoke_keys.clone(),
206208
_marker: Args::default(),
207209
}
208210
}
@@ -264,6 +266,7 @@ impl<P: Params> WindowManager<P> {
264266
menu_event_listeners: Arc::new(menu_event_listeners),
265267
window_event_listeners: Arc::new(window_event_listeners),
266268
}),
269+
invoke_keys: Default::default(),
267270
_marker: Args::default(),
268271
}
269272
}
@@ -301,6 +304,19 @@ impl<P: Params> WindowManager<P> {
301304
}
302305
}
303306

307+
fn generate_invoke_key(&self) -> u32 {
308+
let key = rand::random();
309+
self.invoke_keys.lock().unwrap().push(key);
310+
key
311+
}
312+
313+
/// Checks whether the invoke key is valid or not.
314+
///
315+
/// An invoke key is valid if it was generated by this manager instance.
316+
pub(crate) fn verify_invoke_key(&self, key: u32) -> bool {
317+
self.invoke_keys.lock().unwrap().contains(&key)
318+
}
319+
304320
fn prepare_pending_window(
305321
&self,
306322
mut pending: PendingWindow<P>,
@@ -315,7 +331,8 @@ impl<P: Params> WindowManager<P> {
315331
.expect("poisoned plugin store")
316332
.initialization_script();
317333

318-
let mut webview_attributes = pending.webview_attributes
334+
let mut webview_attributes = pending.webview_attributes;
335+
webview_attributes = webview_attributes
319336
.initialization_script(&self.initialization_script(&plugin_init, is_init_global))
320337
.initialization_script(&format!(
321338
r#"
@@ -326,6 +343,14 @@ impl<P: Params> WindowManager<P> {
326343
current_window_label = label.to_js_string()?,
327344
));
328345

346+
#[cfg(dev)]
347+
{
348+
webview_attributes = webview_attributes.initialization_script(&format!(
349+
"window.__TAURI_INVOKE_KEY__ = {}",
350+
self.generate_invoke_key()
351+
));
352+
}
353+
329354
if !pending.window_builder.has_icon() {
330355
if let Some(default_window_icon) = &self.inner.default_window_icon {
331356
let icon = Icon::Raw(default_window_icon.clone());
@@ -402,6 +427,7 @@ impl<P: Params> WindowManager<P> {
402427

403428
fn prepare_uri_scheme_protocol(&self) -> CustomProtocol {
404429
let assets = self.inner.assets.clone();
430+
let manager = self.clone();
405431
CustomProtocol {
406432
protocol: Box::new(move |path| {
407433
let mut path = path
@@ -424,6 +450,8 @@ impl<P: Params> WindowManager<P> {
424450
// skip leading `/`
425451
path.chars().skip(1).collect::<String>()
426452
};
453+
let is_javascript =
454+
path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
427455

428456
let asset_response = assets
429457
.get(&path)
@@ -435,7 +463,25 @@ impl<P: Params> WindowManager<P> {
435463
.ok_or(crate::Error::AssetNotFound(path))
436464
.map(Cow::into_owned);
437465
match asset_response {
438-
Ok(asset) => Ok(asset),
466+
Ok(asset) => {
467+
if is_javascript {
468+
let js = String::from_utf8_lossy(&asset).into_owned();
469+
Ok(
470+
format!(
471+
r#"(function () {{
472+
const __TAURI_INVOKE_KEY__ = {};
473+
{}
474+
}})()"#,
475+
manager.generate_invoke_key(),
476+
js
477+
)
478+
.as_bytes()
479+
.to_vec(),
480+
)
481+
} else {
482+
Ok(asset)
483+
}
484+
}
439485
Err(e) => {
440486
#[cfg(debug_assertions)]
441487
eprintln!("{:?}", e); // TODO log::error!
@@ -477,32 +523,37 @@ impl<P: Params> WindowManager<P> {
477523
plugin_initialization_script: &str,
478524
with_global_tauri: bool,
479525
) -> String {
526+
let key = self.generate_invoke_key();
480527
format!(
481528
r#"
482-
{bundle_script}
529+
(function () {{
530+
const __TAURI_INVOKE_KEY__ = {key};
531+
{bundle_script}
532+
}})()
483533
{core_script}
484534
{event_initialization_script}
485535
if (window.rpc) {{
486-
window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
536+
window.__TAURI__._invoke("__initialized", {{ url: window.location.href }}, {key})
487537
}} else {{
488538
window.addEventListener('DOMContentLoaded', function () {{
489-
window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
539+
window.__TAURI__._invoke("__initialized", {{ url: window.location.href }}, {key})
490540
}})
491541
}}
492542
{plugin_initialization_script}
493543
"#,
494-
core_script = include_str!("../scripts/core.js"),
544+
key = key,
545+
core_script = include_str!("../scripts/core.js").replace("_KEY_VALUE_", &key.to_string()),
495546
bundle_script = if with_global_tauri {
496547
include_str!("../scripts/bundle.js")
497548
} else {
498549
""
499550
},
500-
event_initialization_script = self.event_initialization_script(),
551+
event_initialization_script = self.event_initialization_script(key),
501552
plugin_initialization_script = plugin_initialization_script
502553
)
503554
}
504555

505-
fn event_initialization_script(&self) -> String {
556+
fn event_initialization_script(&self, key: u32) -> String {
506557
return format!(
507558
"
508559
window['{queue}'] = [];
@@ -516,13 +567,13 @@ impl<P: Params> WindowManager<P> {
516567
}}
517568
518569
if (listeners.length > 0) {{
519-
window.__TAURI__.invoke('tauri', {{
570+
window.__TAURI__._invoke('tauri', {{
520571
__tauriModule: 'Internal',
521572
message: {{
522573
cmd: 'validateSalt',
523574
salt: salt
524575
}}
525-
}}).then(function (flag) {{
576+
}}, {key}).then(function (flag) {{
526577
if (flag) {{
527578
for (let i = listeners.length - 1; i >= 0; i--) {{
528579
const listener = listeners[i]
@@ -534,6 +585,7 @@ impl<P: Params> WindowManager<P> {
534585
}}
535586
}}
536587
",
588+
key = key,
537589
function = self.inner.listeners.function_name(),
538590
queue = self.inner.listeners.queue_object_name(),
539591
listeners = self.inner.listeners.listeners_object_name()

core/tauri/src/window.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,20 @@ impl<P: Params> Window<P> {
212212
);
213213
let resolver = InvokeResolver::new(self, payload.callback, payload.error);
214214
let invoke = Invoke { message, resolver };
215-
if let Some(module) = &payload.tauri_module {
216-
let module = module.to_string();
217-
crate::endpoints::handle(module, invoke, manager.config(), manager.package_info());
218-
} else if command.starts_with("plugin:") {
219-
manager.extend_api(invoke);
215+
if manager.verify_invoke_key(payload.key) {
216+
if let Some(module) = &payload.tauri_module {
217+
let module = module.to_string();
218+
crate::endpoints::handle(module, invoke, manager.config(), manager.package_info());
219+
} else if command.starts_with("plugin:") {
220+
manager.extend_api(invoke);
221+
} else {
222+
manager.run_invoke_handler(invoke);
223+
}
220224
} else {
221-
manager.run_invoke_handler(invoke);
225+
panic!(
226+
r#"The invoke key "{}" is invalid. This means that an external, possible malicious script is trying to access the system interface."#,
227+
payload.key
228+
);
222229
}
223230
}
224231
}

docs/api/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ It's composed of the following properties:
2323
{property: "devPath", type: "string", description: `Can be a path—either absolute or relative—to a folder or a URL (like a live reload server).`},
2424
{property: "beforeDevCommand", optional: true, type: "string", description: `A command to run before starting Tauri in dev mode.`},
2525
{property: "beforeBuildCommand", optional: true, type: "string", description: `A command to run before starting Tauri in build mode.`},
26-
{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."}
26+
{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."}
2727
]}/>
2828

2929
```js title=Example

0 commit comments

Comments
 (0)