Skip to content

Commit a48b8b1

Browse files
committed
feat(core): validate callbacks and event names [TRI-038] [TRI-020] (#21)
1 parent 2f3a582 commit a48b8b1

File tree

20 files changed

+211
-144
lines changed

20 files changed

+211
-144
lines changed

.changes/api-format-callback.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"api": patch
3+
---
4+
5+
The `formatCallback` helper function now returns a number instead of a string.

.changes/callback-validation.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
The `callback` and `error` invoke fields, along with other `transformCallback` usages, are now validated to be numeric.

.changes/validate-event-name.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": "patch"
3+
---
4+
5+
The event name is now validated. On a IPC message, it returns an error if it fails validation; on the Rust side, it panics.
6+
It must include only alphanumeric characters, `-`, `/`, `:` and `_`.

core/tauri-runtime/src/webview.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@
44

55
//! Items specific to the [`Runtime`](crate::Runtime)'s webview.
66
7-
use crate::{window::DetachedWindow, Icon};
7+
use crate::{menu::Menu, window::DetachedWindow, Icon};
88

9-
use crate::menu::Menu;
10-
11-
use serde::Deserialize;
12-
use serde_json::Value as JsonValue;
139
use tauri_utils::config::{WindowConfig, WindowUrl};
1410

1511
#[cfg(windows)]
@@ -184,16 +180,3 @@ pub type WebviewIpcHandler<R> = Box<dyn Fn(DetachedWindow<R>, String) + Send>;
184180
/// File drop handler callback
185181
/// Return `true` in the callback to block the OS' default behavior of handling a file drop.
186182
pub type FileDropHandler<R> = Box<dyn Fn(FileDropEvent, DetachedWindow<R>) -> bool + Send>;
187-
188-
#[derive(Debug, Deserialize)]
189-
pub struct InvokePayload {
190-
pub command: String,
191-
#[serde(rename = "__tauriModule")]
192-
pub tauri_module: Option<String>,
193-
pub callback: String,
194-
pub error: String,
195-
#[serde(rename = "__invokeKey")]
196-
pub key: u32,
197-
#[serde(flatten)]
198-
pub inner: JsonValue,
199-
}

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: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@
44

55
;(function () {
66
function uid() {
7-
const length = new Int8Array(1)
8-
window.crypto.getRandomValues(length)
9-
const array = new Uint8Array(Math.max(16, Math.abs(length[0])))
10-
window.crypto.getRandomValues(array)
11-
return array.join('')
7+
return window.crypto.getRandomValues(new Uint32Array(1))[0]
128
}
139

1410
if (!window.__TAURI__) {

core/tauri/src/api/ipc.rs

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
//!
77
//! This module includes utilities to send messages to the JS layer of the webview.
88
9-
use serde::Serialize;
9+
use serde::{Deserialize, Serialize};
1010
use serde_json::value::RawValue;
1111

12+
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
13+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
14+
pub struct CallbackFn(pub usize);
15+
1216
/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
1317
///
1418
/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
@@ -70,8 +74,8 @@ fn escape_json_parse(json: &RawValue) -> String {
7074
/// ```
7175
/// use tauri::api::ipc::format_callback;
7276
/// // callback with a string argument
73-
/// let cb = format_callback("callback-function-name", &"the string response").unwrap();
74-
/// assert!(cb.contains(r#"window["callback-function-name"]("the string response")"#));
77+
/// let cb = format_callback(12345, &"the string response").unwrap();
78+
/// assert!(cb.contains(r#"window["12345"]("the string response")"#));
7579
/// ```
7680
///
7781
/// - With types implement [`serde::Serialize`]:
@@ -86,14 +90,14 @@ fn escape_json_parse(json: &RawValue) -> String {
8690
/// }
8791
///
8892
/// let cb = format_callback(
89-
/// "callback-function-name",
93+
/// 6789,
9094
/// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
9195
/// }).expect("failed to serialize");
9296
///
93-
/// assert!(cb.contains(r#"window["callback-function-name"](JSON.parse('{"value":"XXXXXXXXX"#));
97+
/// assert!(cb.contains(r#"window["6789"](JSON.parse('{"value":"XXXXXXXXX"#));
9498
/// ```
95-
pub fn format_callback<T: Serialize, S: AsRef<str>>(
96-
function_name: S,
99+
pub fn format_callback<T: Serialize>(
100+
function_name: CallbackFn,
97101
arg: &T,
98102
) -> crate::api::Result<String> {
99103
macro_rules! format_callback {
@@ -106,7 +110,7 @@ pub fn format_callback<T: Serialize, S: AsRef<str>>(
106110
console.warn("[TAURI] Couldn't find callback id {fn} in window. This happens when the app is reloaded while Rust is running an asynchronous operation.")
107111
}}
108112
"#,
109-
fn = function_name.as_ref(),
113+
fn = function_name.0,
110114
arg = $arg
111115
)
112116
}
@@ -169,8 +173,8 @@ pub fn format_callback<T: Serialize, S: AsRef<str>>(
169173
// TODO: better example to explain
170174
pub fn format_callback_result<T: Serialize, E: Serialize>(
171175
result: Result<T, E>,
172-
success_callback: impl AsRef<str>,
173-
error_callback: impl AsRef<str>,
176+
success_callback: CallbackFn,
177+
error_callback: CallbackFn,
174178
) -> crate::api::Result<String> {
175179
match result {
176180
Ok(res) => format_callback(success_callback, &res),
@@ -181,8 +185,15 @@ pub fn format_callback_result<T: Serialize, E: Serialize>(
181185
#[cfg(test)]
182186
mod test {
183187
use crate::api::ipc::*;
188+
use quickcheck::{Arbitrary, Gen};
184189
use quickcheck_macros::quickcheck;
185190

191+
impl Arbitrary for CallbackFn {
192+
fn arbitrary(g: &mut Gen) -> CallbackFn {
193+
CallbackFn(usize::arbitrary(g))
194+
}
195+
}
196+
186197
#[test]
187198
fn test_escape_json_parse() {
188199
let dangerous_json = RawValue::from_string(
@@ -205,38 +216,33 @@ mod test {
205216

206217
// check abritrary strings in the format callback function
207218
#[quickcheck]
208-
fn qc_formating(f: String, a: String) -> bool {
209-
// can not accept empty strings
210-
if !f.is_empty() && !a.is_empty() {
211-
// call format callback
212-
let fc = format_callback(f.clone(), &a).unwrap();
213-
fc.contains(&format!(
214-
r#"window["{}"](JSON.parse('{}'))"#,
215-
f,
216-
serde_json::Value::String(a.clone()),
217-
)) || fc.contains(&format!(
218-
r#"window["{}"]({})"#,
219-
f,
220-
serde_json::Value::String(a),
221-
))
222-
} else {
223-
true
224-
}
219+
fn qc_formating(f: CallbackFn, a: String) -> bool {
220+
// call format callback
221+
let fc = format_callback(f, &a).unwrap();
222+
fc.contains(&format!(
223+
r#"window["{}"](JSON.parse('{}'))"#,
224+
f.0,
225+
serde_json::Value::String(a.clone()),
226+
)) || fc.contains(&format!(
227+
r#"window["{}"]({})"#,
228+
f.0,
229+
serde_json::Value::String(a),
230+
))
225231
}
226232

227233
// check arbitrary strings in format_callback_result
228234
#[quickcheck]
229-
fn qc_format_res(result: Result<String, String>, c: String, ec: String) -> bool {
230-
let resp = format_callback_result(result.clone(), c.clone(), ec.clone())
231-
.expect("failed to format callback result");
235+
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
236+
let resp =
237+
format_callback_result(result.clone(), c, ec).expect("failed to format callback result");
232238
let (function, value) = match result {
233239
Ok(v) => (c, v),
234240
Err(e) => (ec, e),
235241
};
236242

237243
resp.contains(&format!(
238244
r#"window["{}"]({})"#,
239-
function,
245+
function.0,
240246
serde_json::Value::String(value),
241247
))
242248
}

core/tauri/src/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
pub(crate) mod tray;
77

88
use crate::{
9+
api::ipc::CallbackFn,
910
command::{CommandArg, CommandItem},
1011
hooks::{
1112
window_invoke_responder, InvokeHandler, InvokeResponder, OnPageLoad, PageLoadPayload, SetupHook,
@@ -702,7 +703,7 @@ impl<R: Runtime> Builder<R> {
702703
/// That function must take the `command: string` and `args: object` types and send a message to the backend.
703704
pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
704705
where
705-
F: Fn(Window<R>, InvokeResponse, String, String) + Send + Sync + 'static,
706+
F: Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
706707
{
707708
self.invoke_initialization_script = initialization_script;
708709
self.invoke_responder = Arc::new(responder);

core/tauri/src/endpoints/event.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,44 @@
33
// SPDX-License-Identifier: MIT
44

55
use super::InvokeContext;
6-
use crate::{sealed::ManagerBase, Manager, Runtime, Window};
7-
use serde::Deserialize;
6+
use crate::{
7+
api::ipc::CallbackFn, event::is_event_name_valid, sealed::ManagerBase, Manager, Runtime, Window,
8+
};
9+
use serde::{de::Deserializer, Deserialize};
810
use tauri_macros::CommandModule;
911

12+
pub struct EventId(String);
13+
14+
impl<'de> Deserialize<'de> for EventId {
15+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
16+
where
17+
D: Deserializer<'de>,
18+
{
19+
let event_id = String::deserialize(deserializer)?;
20+
if is_event_name_valid(&event_id) {
21+
Ok(EventId(event_id))
22+
} else {
23+
Err(serde::de::Error::custom(
24+
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.",
25+
))
26+
}
27+
}
28+
}
29+
1030
/// The API descriptor.
1131
#[derive(Deserialize, CommandModule)]
1232
#[serde(tag = "cmd", rename_all = "camelCase")]
1333
pub enum Cmd {
1434
/// Listen to an event.
15-
Listen { event: String, handler: String },
35+
Listen { event: EventId, handler: CallbackFn },
1636
/// Unlisten to an event.
1737
#[serde(rename_all = "camelCase")]
1838
Unlisten { event_id: u64 },
1939
/// Emit an event to the webview associated with the given window.
2040
/// If the window_label is omitted, the event will be triggered on all listeners.
2141
#[serde(rename_all = "camelCase")]
2242
Emit {
23-
event: String,
43+
event: EventId,
2444
window_label: Option<String>,
2545
payload: Option<String>,
2646
},
@@ -29,14 +49,17 @@ pub enum Cmd {
2949
impl Cmd {
3050
fn listen<R: Runtime>(
3151
context: InvokeContext<R>,
32-
event: String,
33-
handler: String,
52+
event: EventId,
53+
handler: CallbackFn,
3454
) -> crate::Result<u64> {
3555
let event_id = rand::random();
36-
context
37-
.window
38-
.eval(&listen_js(&context.window, event.clone(), event_id, handler))?;
39-
context.window.register_js_listener(event, event_id);
56+
context.window.eval(&listen_js(
57+
&context.window,
58+
event.0.clone(),
59+
event_id,
60+
handler,
61+
))?;
62+
context.window.register_js_listener(event.0, event_id);
4063
Ok(event_id)
4164
}
4265

@@ -50,17 +73,17 @@ impl Cmd {
5073

5174
fn emit<R: Runtime>(
5275
context: InvokeContext<R>,
53-
event: String,
76+
event: EventId,
5477
window_label: Option<String>,
5578
payload: Option<String>,
5679
) -> crate::Result<()> {
5780
// dispatch the event to Rust listeners
58-
context.window.trigger(&event, payload.clone());
81+
context.window.trigger(&event.0, payload.clone());
5982

6083
if let Some(target) = window_label {
61-
context.window.emit_to(&target, &event, payload)?;
84+
context.window.emit_to(&target, &event.0, payload)?;
6285
} else {
63-
context.window.emit_all(&event, payload)?;
86+
context.window.emit_all(&event.0, payload)?;
6487
}
6588
Ok(())
6689
}
@@ -85,7 +108,7 @@ pub fn listen_js<R: Runtime>(
85108
window: &Window<R>,
86109
event: String,
87110
event_id: u64,
88-
handler: String,
111+
handler: CallbackFn,
89112
) -> String {
90113
format!(
91114
"if (window['{listeners}'] === void 0) {{
@@ -102,6 +125,6 @@ pub fn listen_js<R: Runtime>(
102125
listeners = window.manager().event_listeners_object_name(),
103126
event = event,
104127
event_id = event_id,
105-
handler = handler
128+
handler = handler.0
106129
)
107130
}

core/tauri/src/endpoints/global_shortcut.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// SPDX-License-Identifier: MIT
44

55
use super::InvokeContext;
6-
use crate::Runtime;
6+
use crate::{api::ipc::CallbackFn, Runtime};
77
use serde::Deserialize;
88
use tauri_macros::{module_command_handler, CommandModule};
99

@@ -15,11 +15,14 @@ use crate::runtime::GlobalShortcutManager;
1515
#[serde(tag = "cmd", rename_all = "camelCase")]
1616
pub enum Cmd {
1717
/// Register a global shortcut.
18-
Register { shortcut: String, handler: String },
18+
Register {
19+
shortcut: String,
20+
handler: CallbackFn,
21+
},
1922
/// Register a list of global shortcuts.
2023
RegisterAll {
2124
shortcuts: Vec<String>,
22-
handler: String,
25+
handler: CallbackFn,
2326
},
2427
/// Unregister a global shortcut.
2528
Unregister { shortcut: String },
@@ -34,7 +37,7 @@ impl Cmd {
3437
fn register<R: Runtime>(
3538
context: InvokeContext<R>,
3639
shortcut: String,
37-
handler: String,
40+
handler: CallbackFn,
3841
) -> crate::Result<()> {
3942
let mut manager = context.window.app_handle.global_shortcut_manager();
4043
register_shortcut(context.window, &mut manager, shortcut, handler)?;
@@ -45,16 +48,11 @@ impl Cmd {
4548
fn register_all<R: Runtime>(
4649
context: InvokeContext<R>,
4750
shortcuts: Vec<String>,
48-
handler: String,
51+
handler: CallbackFn,
4952
) -> crate::Result<()> {
5053
let mut manager = context.window.app_handle.global_shortcut_manager();
5154
for shortcut in shortcuts {
52-
register_shortcut(
53-
context.window.clone(),
54-
&mut manager,
55-
shortcut,
56-
handler.clone(),
57-
)?;
55+
register_shortcut(context.window.clone(), &mut manager, shortcut, handler)?;
5856
}
5957
Ok(())
6058
}
@@ -96,11 +94,11 @@ fn register_shortcut<R: Runtime>(
9694
window: crate::Window<R>,
9795
manager: &mut R::GlobalShortcutManager,
9896
shortcut: String,
99-
handler: String,
97+
handler: CallbackFn,
10098
) -> crate::Result<()> {
10199
let accelerator = shortcut.clone();
102100
manager.register(&shortcut, move || {
103-
let callback_string = crate::api::ipc::format_callback(&handler, &accelerator)
101+
let callback_string = crate::api::ipc::format_callback(handler, &accelerator)
104102
.expect("unable to serialize shortcut string to json");
105103
let _ = window.eval(callback_string.as_str());
106104
})?;

0 commit comments

Comments
 (0)