Skip to content

Commit ba02558

Browse files
chippersFabianLars
andauthored
Merge commit from fork
* check .localhost suffix on windows and android i didn't actually run this on windows, i'm relying on CI to tell me * Create tauri-sec-localhost-suffix.md --------- Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
1 parent 5f479c0 commit ba02558

2 files changed

Lines changed: 71 additions & 18 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
tauri: patch:sec
3+
---
4+
5+
Correctly handle .localhost suffix in local origins on Windows and Android to fix a security issue that made tauri think remote websites that started with a registered scheme were local websites.
6+
For example, when registering an `app` custom protocol, Tauri would think `http://app.evil.com/` would be a local URL on Windows/Android.

crates/tauri/src/webview/mod.rs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,14 +1724,14 @@ tauri::Builder::default()
17241724
// so we check using the first part of the domain
17251725
#[cfg(any(windows, target_os = "android"))]
17261726
let local = {
1727-
let protocol_url = self.manager().tauri_protocol_url(uses_https);
1728-
let maybe_protocol = current_url
1727+
let scheme = scheme == self.manager().tauri_protocol_url(uses_https).scheme();
1728+
let protocol = current_url
17291729
.domain()
1730-
.and_then(|d| d .split_once('.'))
1731-
.unwrap_or_default()
1732-
.0;
1730+
.and_then(|d| d.strip_suffix(".localhost"))
1731+
.map(|protocol| protocols.contains_key(protocol))
1732+
.unwrap_or_default();
17331733

1734-
protocols.contains_key(maybe_protocol) && scheme == protocol_url.scheme()
1734+
scheme && protocol
17351735
};
17361736

17371737
local
@@ -2354,12 +2354,68 @@ impl<T: ScopeObject> ResolvedScope<T> {
23542354

23552355
#[cfg(test)]
23562356
mod tests {
2357+
use url::Url;
2358+
2359+
fn test_webview_window() -> crate::WebviewWindow<crate::test::MockRuntime> {
2360+
use crate::test::{mock_builder, mock_context, noop_assets};
2361+
2362+
// Create a mock app with proper context
2363+
let app = mock_builder().build(mock_context(noop_assets())).unwrap();
2364+
2365+
// Create a webview window
2366+
crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default())
2367+
.build()
2368+
.unwrap()
2369+
}
2370+
23572371
#[test]
23582372
fn webview_is_send_sync() {
23592373
crate::test_utils::assert_send::<super::Webview>();
23602374
crate::test_utils::assert_sync::<super::Webview>();
23612375
}
23622376

2377+
#[test]
2378+
fn tauri_protocol_is_local() {
2379+
let webview = test_webview_window().webview;
2380+
2381+
#[cfg(all(not(windows), not(target_os = "android")))]
2382+
assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap()));
2383+
2384+
#[cfg(any(windows, target_os = "android"))]
2385+
assert!(webview.is_local_url(&Url::parse("https://tauri.localhost/").unwrap()));
2386+
}
2387+
2388+
// On Windows/Android, custom protocols are served as `https://<name>.localhost/`.
2389+
// We ensure only `.localhost` domains are accepted to prevent a subdomain being able to
2390+
// impersonate a protocol name.
2391+
#[cfg(any(windows, target_os = "android"))]
2392+
#[test]
2393+
fn windows_custom_protocol_rejects_spoofed_domain() {
2394+
use crate::test::{mock_builder, mock_context, noop_assets};
2395+
2396+
let app = mock_builder()
2397+
.register_uri_scheme_protocol("myproto", |_, _| {
2398+
http::Response::builder().body(Vec::new()).unwrap()
2399+
})
2400+
.build(mock_context(noop_assets()))
2401+
.unwrap();
2402+
let webview = crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default())
2403+
.build()
2404+
.unwrap()
2405+
.webview;
2406+
2407+
let url = |s| Url::parse(s).unwrap();
2408+
2409+
// Legitimate Windows custom protocol URL
2410+
assert!(webview.is_local_url(&url("https://myproto.localhost/")));
2411+
2412+
// Attacker domain that starts with a registered protocol name — must NOT be local.
2413+
assert!(!webview.is_local_url(&url("https://myproto.evil.com/")));
2414+
2415+
// Subdomain of .localhost with unregistered name — must NOT be local
2416+
assert!(!webview.is_local_url(&url("https://notregistered.localhost/")));
2417+
}
2418+
23632419
/// Custom (non-plugin) commands must be rejected when the IPC request
23642420
/// originates from a remote URL, even when no `AppManifest` has been
23652421
/// configured. Only local (bundled) origins should be able to reach
@@ -2376,7 +2432,7 @@ mod tests {
23762432
.unwrap();
23772433

23782434
// Request from a remote origin for a custom (non-plugin) command
2379-
// should be rejected even without an AppManifest.
2435+
// - should be rejected even without an AppManifest.
23802436
let remote_result = crate::test::get_ipc_response(
23812437
&webview,
23822438
InvokeRequest {
@@ -2394,7 +2450,7 @@ mod tests {
23942450
"custom command should be rejected from a remote origin"
23952451
);
23962452

2397-
// Same command from the local origin should NOT be rejected by the
2453+
// Same command from the local origin - should NOT be rejected by the
23982454
// remote-origin guard (it may still fail because the command doesn't
23992455
// exist, but the error message will be different).
24002456
let local_result = crate::test::get_ipc_response(
@@ -2423,16 +2479,7 @@ mod tests {
24232479
#[cfg(target_os = "macos")]
24242480
#[test]
24252481
fn test_webview_window_has_set_simple_fullscreen_method() {
2426-
use crate::test::{mock_builder, mock_context, noop_assets};
2427-
2428-
// Create a mock app with proper context
2429-
let app = mock_builder().build(mock_context(noop_assets())).unwrap();
2430-
2431-
// Get or create a webview window
2432-
let webview_window =
2433-
crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default())
2434-
.build()
2435-
.unwrap();
2482+
let webview_window = test_webview_window();
24362483

24372484
// This should compile if set_simple_fullscreen exists
24382485
let result = webview_window.set_simple_fullscreen(true);

0 commit comments

Comments
 (0)