Merge pull request from GHSA-57fm-592m-34r7

* only allow tauri-initialized frames to call the ipc

* dont regenerate the invoke key

* return early if the invoke key does not match

* add change file

* wry 0.40

* update change file

* trigger ci

---------

Co-authored-by: Chip Reed <chip@chip.sh>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Tillmann 2024-05-23 05:49:08 +09:00 committed by GitHub
parent beda18bce9
commit d950ac1239
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 143 additions and 3 deletions

View File

@ -0,0 +1,6 @@
---
'tauri': patch:sec
'tauri-runtime-wry': patch:sec
---
Only process IPC commands from the main frame.

View File

@ -2,7 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
;(function() {
/**
* A runtime generated key to ensure an IPC call comes from an initialized frame.
*
* This is declared outside the `window.__TAURI_INVOKE__` definition to prevent
* the key from being leaked by `window.__TAURI_INVOKE__.toString()`.
* @var {string} __TEMPLATE_invoke_key__
*/
const __TAURI_INVOKE_KEY__ = __TEMPLATE_invoke_key__
const processIpcMessage = __RAW_process_ipc_message_fn__
const osName = __TEMPLATE_os_name__
const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__
@ -29,6 +38,7 @@
'Content-Type': contentType,
'Tauri-Callback': callback,
'Tauri-Error': error,
'Tauri-Invoke-Key': __TAURI_INVOKE_KEY__,
...((options && options.headers) || {})
}
})
@ -66,7 +76,8 @@
callback,
error,
options,
payload
payload,
__TAURI_INVOKE_KEY__
})
window.ipc.postMessage(data)
}

View File

@ -1097,6 +1097,8 @@ pub struct Builder<R: Runtime> {
/// The device event filter.
device_event_filter: DeviceEventFilter,
invoke_key: String,
}
#[derive(Template)]
@ -1108,6 +1110,7 @@ struct InvokeInitializationScript<'a> {
os_name: &'a str,
fetch_channel_data_command: &'a str,
linux_ipc_protocol_enabled: bool,
invoke_key: &'a str,
}
/// Make `Wry` the default `Runtime` for `Builder`
@ -1130,6 +1133,8 @@ impl<R: Runtime> Default for Builder<R> {
impl<R: Runtime> Builder<R> {
/// Creates a new App builder.
pub fn new() -> Self {
let invoke_key = crate::generate_invoke_key().unwrap();
Self {
#[cfg(any(windows, target_os = "linux"))]
runtime_any_thread: false,
@ -1141,6 +1146,7 @@ impl<R: Runtime> Builder<R> {
os_name: std::env::consts::OS,
fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
linux_ipc_protocol_enabled: cfg!(feature = "linux-ipc-protocol"),
invoke_key: &invoke_key.clone(),
}
.render_default(&Default::default())
.unwrap()
@ -1155,6 +1161,7 @@ impl<R: Runtime> Builder<R> {
window_event_listeners: Vec::new(),
webview_event_listeners: Vec::new(),
device_event_filter: Default::default(),
invoke_key,
}
}
}
@ -1622,6 +1629,7 @@ tauri::Builder::default()
#[cfg(desktop)]
HashMap::new(),
(self.invoke_responder, self.invoke_initialization_script),
self.invoke_key,
));
let runtime_args = RuntimeInitArgs {

View File

@ -151,10 +151,21 @@ pub enum Error {
/// Failed to deserialize scope object.
#[error("error deserializing scope: {0}")]
CannotDeserializeScope(Box<dyn std::error::Error + Send + Sync>),
/// Failed to get a raw handle.
#[error(transparent)]
RawHandleError(#[from] raw_window_handle::HandleError),
/// Something went wrong with the CSPRNG.
#[error("unable to generate random bytes from the operating system: {0}")]
Csprng(getrandom::Error),
/// Bad `__TAURI_INVOKE_KEY__` value received in ipc message.
#[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")]
InvokeKey,
}
impl From<getrandom::Error> for Error {
fn from(value: getrandom::Error) -> Self {
Self::Csprng(value)
}
}
/// `Result<T, ::tauri::Error>`

View File

@ -19,6 +19,7 @@ use super::{CallbackFn, InvokeBody, InvokeResponse};
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key";
pub fn message_handler<R: Runtime>(
manager: Arc<AppManager<R>>,
@ -210,6 +211,8 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
error: CallbackFn,
payload: serde_json::Value,
options: Option<RequestOptions>,
#[serde(rename = "__TAURI_INVOKE_KEY__")]
invoke_key: String,
}
#[allow(unused_mut)]
@ -224,6 +227,8 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
error: CallbackFn,
payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>,
options: Option<RequestOptions>,
#[serde(rename = "__TAURI_INVOKE_KEY__")]
invoke_key: String,
}
if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
@ -240,6 +245,7 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
error: message.error,
payload: serde_json::from_slice(&crypto_keys.decrypt(message.payload)?)?,
options: message.options,
invoke_key: message.invoke_key,
})
}),
);
@ -261,6 +267,7 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"),
body: message.payload.into(),
headers: message.options.map(|o| o.headers.0).unwrap_or_default(),
invoke_key: message.invoke_key,
};
#[cfg(feature = "tracing")]
@ -394,6 +401,14 @@ fn parse_invoke_request<R: Runtime>(
}
}
let invoke_key = parts
.headers
.get(TAURI_INVOKE_KEY_HEADER_NAME)
.ok_or("missing Tauri-Invoke-Key header")?
.to_str()
.map_err(|_| "Tauri invoke key header value must be a string")?
.to_owned();
let url = Url::parse(
parts
.headers
@ -461,6 +476,7 @@ fn parse_invoke_request<R: Runtime>(
url,
body,
headers: parts.headers,
invoke_key,
};
Ok(payload)

View File

@ -1092,3 +1092,52 @@ mod test_utils {
}
}
}
/// Simple dependency-free string encoder using [Z85].
mod z85 {
const TABLE: &[u8; 85] =
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";
/// Encode bytes with [Z85].
///
/// # Panics
///
/// Will panic if the input bytes are not a multiple of 4.
pub fn encode(bytes: &[u8]) -> String {
assert_eq!(bytes.len() % 4, 0);
let mut buf = String::with_capacity(bytes.len() * 5 / 4);
for chunk in bytes.chunks_exact(4) {
let mut chars = [0u8; 5];
let mut chunk = u32::from_be_bytes(chunk.try_into().unwrap()) as usize;
for byte in chars.iter_mut().rev() {
*byte = TABLE[chunk % 85];
chunk /= 85;
}
buf.push_str(std::str::from_utf8(&chars).unwrap());
}
buf
}
#[cfg(test)]
mod tests {
#[test]
fn encode() {
assert_eq!(
super::encode(&[0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B]),
"HelloWorld"
);
}
}
}
/// Generate a random 128-bit [Z85] encoded [`String`].
///
/// [Z85]: https://rfc.zeromq.org/spec/32/
pub(crate) fn generate_invoke_key() -> Result<String> {
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes)?;
Ok(z85::encode(&bytes))
}

View File

@ -193,6 +193,9 @@ pub struct AppManager<R: Runtime> {
/// Application Resources Table
pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
/// Runtime-generated invoke key.
pub(crate) invoke_key: String,
}
impl<R: Runtime> fmt::Debug for AppManager<R> {
@ -232,6 +235,7 @@ impl<R: Runtime> AppManager<R> {
crate::app::GlobalMenuEventListener<Window<R>>,
>,
(invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
invoke_key: String,
) -> Self {
// generate a random isolation key at runtime
#[cfg(feature = "isolation")]
@ -254,6 +258,7 @@ impl<R: Runtime> AppManager<R> {
event_listeners: Arc::new(webiew_event_listeners),
invoke_responder,
invoke_initialization_script,
invoke_key: invoke_key.clone(),
},
#[cfg(all(desktop, feature = "tray-icon"))]
tray: tray::TrayManager {
@ -279,6 +284,7 @@ impl<R: Runtime> AppManager<R> {
pattern: Arc::new(context.pattern),
plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts),
resources_table: Arc::default(),
invoke_key,
}
}
@ -570,6 +576,10 @@ impl<R: Runtime> AppManager<R> {
.lock()
.expect("poisoned window manager")
}
pub(crate) fn invoke_key(&self) -> &str {
&self.invoke_key
}
}
#[cfg(desktop)]

View File

@ -89,6 +89,9 @@ pub struct WebviewManager<R: Runtime> {
pub invoke_responder: Option<Arc<InvokeResponder<R>>>,
/// The script that initializes the invoke system.
pub invoke_initialization_script: String,
/// A runtime generated invoke key.
pub(crate) invoke_key: String,
}
impl<R: Runtime> fmt::Debug for WebviewManager<R> {
@ -98,6 +101,7 @@ impl<R: Runtime> fmt::Debug for WebviewManager<R> {
"invoke_initialization_script",
&self.invoke_initialization_script,
)
.field("invoke_key", &self.invoke_key)
.finish()
}
}
@ -371,6 +375,7 @@ impl<R: Runtime> WebviewManager<R> {
#[default_template("../../scripts/core.js")]
struct CoreJavascript<'a> {
os_name: &'a str,
invoke_key: &'a str,
}
let bundle_script = if with_global_tauri {
@ -391,6 +396,7 @@ impl<R: Runtime> WebviewManager<R> {
bundle_script,
core_script: &CoreJavascript {
os_name: std::env::consts::OS,
invoke_key: self.invoke_key(),
}
.render_default(&Default::default())?
.into_string(),
@ -660,6 +666,10 @@ impl<R: Runtime> WebviewManager<R> {
pub fn labels(&self) -> HashSet<String> {
self.webviews_lock().keys().cloned().collect()
}
pub(crate) fn invoke_key(&self) -> &str {
&self.invoke_key
}
}
impl<R: Runtime> Webview<R> {

View File

@ -124,6 +124,7 @@ pub struct InvokeRequest {
pub body: InvokeBody,
/// The request headers.
pub headers: HeaderMap,
pub(crate) invoke_key: String,
}
/// The platform webview handle. Accessed with [`Webview#method.with_webview`];
@ -1132,6 +1133,24 @@ fn main() {
let manager = self.manager_owned();
let is_local = self.is_local_url(&request.url);
// ensure the passed key matches what our manager should have injected
let expected = manager.invoke_key();
if request.invoke_key != expected {
#[cfg(feature = "tracing")]
tracing::error!(
"__TAURI_INVOKE_KEY__ expected {expected} but received {}",
request.invoke_key
);
#[cfg(not(feature = "tracing"))]
eprintln!(
"__TAURI_INVOKE_KEY__ expected {expected} but received {}",
request.invoke_key
);
return;
}
let custom_responder = self.manager().webview.invoke_responder.clone();
let resolver = InvokeResolver::new(