Skip to content

Commit

Permalink
feat(core): validate callbacks and event names [TRI-038] [TRI-020] (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Jan 9, 2022
1 parent 2f3a582 commit a48b8b1
Show file tree
Hide file tree
Showing 20 changed files with 211 additions and 144 deletions.
5 changes: 5 additions & 0 deletions .changes/api-format-callback.md
@@ -0,0 +1,5 @@
---
"api": patch
---

The `formatCallback` helper function now returns a number instead of a string.
5 changes: 5 additions & 0 deletions .changes/callback-validation.md
@@ -0,0 +1,5 @@
---
"tauri": patch
---

The `callback` and `error` invoke fields, along with other `transformCallback` usages, are now validated to be numeric.
6 changes: 6 additions & 0 deletions .changes/validate-event-name.md
@@ -0,0 +1,6 @@
---
"tauri": "patch"
---

The event name is now validated. On a IPC message, it returns an error if it fails validation; on the Rust side, it panics.
It must include only alphanumeric characters, `-`, `/`, `:` and `_`.
19 changes: 1 addition & 18 deletions core/tauri-runtime/src/webview.rs
Expand Up @@ -4,12 +4,8 @@

//! Items specific to the [`Runtime`](crate::Runtime)'s webview.

use crate::{window::DetachedWindow, Icon};
use crate::{menu::Menu, window::DetachedWindow, Icon};

use crate::menu::Menu;

use serde::Deserialize;
use serde_json::Value as JsonValue;
use tauri_utils::config::{WindowConfig, WindowUrl};

#[cfg(windows)]
Expand Down Expand Up @@ -184,16 +180,3 @@ pub type WebviewIpcHandler<R> = Box<dyn Fn(DetachedWindow<R>, String) + Send>;
/// File drop handler callback
/// Return `true` in the callback to block the OS' default behavior of handling a file drop.
pub type FileDropHandler<R> = Box<dyn Fn(FileDropEvent, DetachedWindow<R>) -> bool + Send>;

#[derive(Debug, Deserialize)]
pub struct InvokePayload {
pub command: String,
#[serde(rename = "__tauriModule")]
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.

6 changes: 1 addition & 5 deletions core/tauri/scripts/core.js
Expand Up @@ -4,11 +4,7 @@

;(function () {
function uid() {
const length = new Int8Array(1)
window.crypto.getRandomValues(length)
const array = new Uint8Array(Math.max(16, Math.abs(length[0])))
window.crypto.getRandomValues(array)
return array.join('')
return window.crypto.getRandomValues(new Uint32Array(1))[0]
}

if (!window.__TAURI__) {
Expand Down
68 changes: 37 additions & 31 deletions core/tauri/src/api/ipc.rs
Expand Up @@ -6,9 +6,13 @@
//!
//! This module includes utilities to send messages to the JS layer of the webview.

use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;

/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub struct CallbackFn(pub usize);

/// 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).
///
/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
Expand Down Expand Up @@ -70,8 +74,8 @@ fn escape_json_parse(json: &RawValue) -> String {
/// ```
/// use tauri::api::ipc::format_callback;
/// // callback with a string argument
/// let cb = format_callback("callback-function-name", &"the string response").unwrap();
/// assert!(cb.contains(r#"window["callback-function-name"]("the string response")"#));
/// let cb = format_callback(12345, &"the string response").unwrap();
/// assert!(cb.contains(r#"window["12345"]("the string response")"#));
/// ```
///
/// - With types implement [`serde::Serialize`]:
Expand All @@ -86,14 +90,14 @@ fn escape_json_parse(json: &RawValue) -> String {
/// }
///
/// let cb = format_callback(
/// "callback-function-name",
/// 6789,
/// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
/// }).expect("failed to serialize");
///
/// assert!(cb.contains(r#"window["callback-function-name"](JSON.parse('{"value":"XXXXXXXXX"#));
/// assert!(cb.contains(r#"window["6789"](JSON.parse('{"value":"XXXXXXXXX"#));
/// ```
pub fn format_callback<T: Serialize, S: AsRef<str>>(
function_name: S,
pub fn format_callback<T: Serialize>(
function_name: CallbackFn,
arg: &T,
) -> crate::api::Result<String> {
macro_rules! format_callback {
Expand All @@ -106,7 +110,7 @@ pub fn format_callback<T: Serialize, S: AsRef<str>>(
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.")
}}
"#,
fn = function_name.as_ref(),
fn = function_name.0,
arg = $arg
)
}
Expand Down Expand Up @@ -169,8 +173,8 @@ pub fn format_callback<T: Serialize, S: AsRef<str>>(
// TODO: better example to explain
pub fn format_callback_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
success_callback: impl AsRef<str>,
error_callback: impl AsRef<str>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::api::Result<String> {
match result {
Ok(res) => format_callback(success_callback, &res),
Expand All @@ -181,8 +185,15 @@ pub fn format_callback_result<T: Serialize, E: Serialize>(
#[cfg(test)]
mod test {
use crate::api::ipc::*;
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;

impl Arbitrary for CallbackFn {
fn arbitrary(g: &mut Gen) -> CallbackFn {
CallbackFn(usize::arbitrary(g))
}
}

#[test]
fn test_escape_json_parse() {
let dangerous_json = RawValue::from_string(
Expand All @@ -205,38 +216,33 @@ mod test {

// check abritrary strings in the format callback function
#[quickcheck]
fn qc_formating(f: String, a: String) -> bool {
// can not accept empty strings
if !f.is_empty() && !a.is_empty() {
// call format callback
let fc = format_callback(f.clone(), &a).unwrap();
fc.contains(&format!(
r#"window["{}"](JSON.parse('{}'))"#,
f,
serde_json::Value::String(a.clone()),
)) || fc.contains(&format!(
r#"window["{}"]({})"#,
f,
serde_json::Value::String(a),
))
} else {
true
}
fn qc_formating(f: CallbackFn, a: String) -> bool {
// call format callback
let fc = format_callback(f, &a).unwrap();
fc.contains(&format!(
r#"window["{}"](JSON.parse('{}'))"#,
f.0,
serde_json::Value::String(a.clone()),
)) || fc.contains(&format!(
r#"window["{}"]({})"#,
f.0,
serde_json::Value::String(a),
))
}

// check arbitrary strings in format_callback_result
#[quickcheck]
fn qc_format_res(result: Result<String, String>, c: String, ec: String) -> bool {
let resp = format_callback_result(result.clone(), c.clone(), ec.clone())
.expect("failed to format callback result");
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
let resp =
format_callback_result(result.clone(), c, ec).expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),
};

resp.contains(&format!(
r#"window["{}"]({})"#,
function,
function.0,
serde_json::Value::String(value),
))
}
Expand Down
3 changes: 2 additions & 1 deletion core/tauri/src/app.rs
Expand Up @@ -6,6 +6,7 @@
pub(crate) mod tray;

use crate::{
api::ipc::CallbackFn,
command::{CommandArg, CommandItem},
hooks::{
window_invoke_responder, InvokeHandler, InvokeResponder, OnPageLoad, PageLoadPayload, SetupHook,
Expand Down Expand Up @@ -702,7 +703,7 @@ impl<R: Runtime> Builder<R> {
/// That function must take the `command: string` and `args: object` types and send a message to the backend.
pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
where
F: Fn(Window<R>, InvokeResponse, String, String) + Send + Sync + 'static,
F: Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
{
self.invoke_initialization_script = initialization_script;
self.invoke_responder = Arc::new(responder);
Expand Down
55 changes: 39 additions & 16 deletions core/tauri/src/endpoints/event.rs
Expand Up @@ -3,24 +3,44 @@
// SPDX-License-Identifier: MIT

use super::InvokeContext;
use crate::{sealed::ManagerBase, Manager, Runtime, Window};
use serde::Deserialize;
use crate::{
api::ipc::CallbackFn, event::is_event_name_valid, sealed::ManagerBase, Manager, Runtime, Window,
};
use serde::{de::Deserializer, Deserialize};
use tauri_macros::CommandModule;

pub struct EventId(String);

impl<'de> Deserialize<'de> for EventId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let event_id = String::deserialize(deserializer)?;
if is_event_name_valid(&event_id) {
Ok(EventId(event_id))
} else {
Err(serde::de::Error::custom(
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.",
))
}
}
}

/// The API descriptor.
#[derive(Deserialize, CommandModule)]
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
/// Listen to an event.
Listen { event: String, handler: String },
Listen { event: EventId, handler: CallbackFn },
/// Unlisten to an event.
#[serde(rename_all = "camelCase")]
Unlisten { event_id: u64 },
/// Emit an event to the webview associated with the given window.
/// If the window_label is omitted, the event will be triggered on all listeners.
#[serde(rename_all = "camelCase")]
Emit {
event: String,
event: EventId,
window_label: Option<String>,
payload: Option<String>,
},
Expand All @@ -29,14 +49,17 @@ pub enum Cmd {
impl Cmd {
fn listen<R: Runtime>(
context: InvokeContext<R>,
event: String,
handler: String,
event: EventId,
handler: CallbackFn,
) -> crate::Result<u64> {
let event_id = rand::random();
context
.window
.eval(&listen_js(&context.window, event.clone(), event_id, handler))?;
context.window.register_js_listener(event, event_id);
context.window.eval(&listen_js(
&context.window,
event.0.clone(),
event_id,
handler,
))?;
context.window.register_js_listener(event.0, event_id);
Ok(event_id)
}

Expand All @@ -50,17 +73,17 @@ impl Cmd {

fn emit<R: Runtime>(
context: InvokeContext<R>,
event: String,
event: EventId,
window_label: Option<String>,
payload: Option<String>,
) -> crate::Result<()> {
// dispatch the event to Rust listeners
context.window.trigger(&event, payload.clone());
context.window.trigger(&event.0, payload.clone());

if let Some(target) = window_label {
context.window.emit_to(&target, &event, payload)?;
context.window.emit_to(&target, &event.0, payload)?;
} else {
context.window.emit_all(&event, payload)?;
context.window.emit_all(&event.0, payload)?;
}
Ok(())
}
Expand All @@ -85,7 +108,7 @@ pub fn listen_js<R: Runtime>(
window: &Window<R>,
event: String,
event_id: u64,
handler: String,
handler: CallbackFn,
) -> String {
format!(
"if (window['{listeners}'] === void 0) {{
Expand All @@ -102,6 +125,6 @@ pub fn listen_js<R: Runtime>(
listeners = window.manager().event_listeners_object_name(),
event = event,
event_id = event_id,
handler = handler
handler = handler.0
)
}
24 changes: 11 additions & 13 deletions core/tauri/src/endpoints/global_shortcut.rs
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT

use super::InvokeContext;
use crate::Runtime;
use crate::{api::ipc::CallbackFn, Runtime};
use serde::Deserialize;
use tauri_macros::{module_command_handler, CommandModule};

Expand All @@ -15,11 +15,14 @@ use crate::runtime::GlobalShortcutManager;
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
/// Register a global shortcut.
Register { shortcut: String, handler: String },
Register {
shortcut: String,
handler: CallbackFn,
},
/// Register a list of global shortcuts.
RegisterAll {
shortcuts: Vec<String>,
handler: String,
handler: CallbackFn,
},
/// Unregister a global shortcut.
Unregister { shortcut: String },
Expand All @@ -34,7 +37,7 @@ impl Cmd {
fn register<R: Runtime>(
context: InvokeContext<R>,
shortcut: String,
handler: String,
handler: CallbackFn,
) -> crate::Result<()> {
let mut manager = context.window.app_handle.global_shortcut_manager();
register_shortcut(context.window, &mut manager, shortcut, handler)?;
Expand All @@ -45,16 +48,11 @@ impl Cmd {
fn register_all<R: Runtime>(
context: InvokeContext<R>,
shortcuts: Vec<String>,
handler: String,
handler: CallbackFn,
) -> crate::Result<()> {
let mut manager = context.window.app_handle.global_shortcut_manager();
for shortcut in shortcuts {
register_shortcut(
context.window.clone(),
&mut manager,
shortcut,
handler.clone(),
)?;
register_shortcut(context.window.clone(), &mut manager, shortcut, handler)?;
}
Ok(())
}
Expand Down Expand Up @@ -96,11 +94,11 @@ fn register_shortcut<R: Runtime>(
window: crate::Window<R>,
manager: &mut R::GlobalShortcutManager,
shortcut: String,
handler: String,
handler: CallbackFn,
) -> crate::Result<()> {
let accelerator = shortcut.clone();
manager.register(&shortcut, move || {
let callback_string = crate::api::ipc::format_callback(&handler, &accelerator)
let callback_string = crate::api::ipc::format_callback(handler, &accelerator)
.expect("unable to serialize shortcut string to json");
let _ = window.eval(callback_string.as_str());
})?;
Expand Down

0 comments on commit a48b8b1

Please sign in to comment.