锁定桌面导航请求边界

桌面导航 HostBridge 单测覆盖外链协议白名单

桌面导航 HostBridge 单测覆盖同源 H5 路由限制

桌面壳配置检查反查导航请求边界测试
This commit is contained in:
2026-06-20 11:28:35 +08:00
parent 1e5b7a5c48
commit f8b385ae0b
2 changed files with 119 additions and 21 deletions
@@ -1867,11 +1867,15 @@ if (
}
for (const snippet of [
'open_desktop_host_bridge_external_url',
'desktop_external_url_from_request',
'normalize_external_url',
'open_normalized_desktop_external_url(app, url)',
'desktop_external_url_request_accepts_only_safe_system_protocols',
'open_desktop_host_bridge_native_page',
'desktop_native_page_url_from_request',
'normalize_native_page_url',
'window.navigate(url)',
'desktop_native_page_request_accepts_only_same_origin_h5_routes',
'reload_desktop_host_bridge_webview',
'window.reload()',
]) {
@@ -7,22 +7,43 @@ use crate::shell::webview::{
use serde_json::json;
use tauri::Manager;
pub(crate) fn open_desktop_host_bridge_external_url(
app: &tauri::AppHandle,
fn desktop_external_url_from_request(
request: &HostBridgeRequest,
) -> HostBridgeResponse {
let url = match required_string_payload(request, "url")
) -> Result<String, HostBridgeResponse> {
required_string_payload(request, "url")
.ok()
.and_then(normalize_external_url)
{
Some(url) => url,
None => {
return failed(
.ok_or_else(|| {
failed(
request.id.clone(),
"invalid_request",
"url must use an allowed external protocol",
)
}
})
}
fn desktop_native_page_url_from_request(
request: &HostBridgeRequest,
) -> Result<tauri::Url, HostBridgeResponse> {
required_string_payload(request, "url")
.ok()
.and_then(normalize_native_page_url)
.ok_or_else(|| {
failed(
request.id.clone(),
"invalid_request",
"url must use an allowed same-origin H5 route",
)
})
}
pub(crate) fn open_desktop_host_bridge_external_url(
app: &tauri::AppHandle,
request: &HostBridgeRequest,
) -> HostBridgeResponse {
let url = match desktop_external_url_from_request(request) {
Ok(url) => url,
Err(response) => return response,
};
match open_normalized_desktop_external_url(app, url) {
@@ -35,18 +56,9 @@ pub(crate) fn open_desktop_host_bridge_native_page(
app: &tauri::AppHandle,
request: &HostBridgeRequest,
) -> HostBridgeResponse {
let url = match required_string_payload(request, "url")
.ok()
.and_then(normalize_native_page_url)
{
Some(url) => url,
None => {
return failed(
request.id.clone(),
"invalid_request",
"url must use an allowed same-origin H5 route",
)
}
let url = match desktop_native_page_url_from_request(request) {
Ok(url) => url,
Err(response) => return response,
};
match app.get_webview_window("main") {
@@ -58,6 +70,88 @@ pub(crate) fn open_desktop_host_bridge_native_page(
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host_bridge::protocol::request;
use serde_json::json;
fn request_with_url(method: &str, url: &str) -> HostBridgeRequest {
let mut request = request(method);
request.payload = Some(json!({ "url": url }));
request
}
#[test]
fn desktop_external_url_request_accepts_only_safe_system_protocols() {
let request = request_with_url("app.openExternalUrl", " https://example.com/share ");
assert_eq!(
desktop_external_url_from_request(&request).expect("external url"),
"https://example.com/share"
);
for url in [
"",
"/works/detail?work=PZ-1",
"javascript:alert(1)",
"file:///etc/passwd",
"https://example.com/\nnext",
] {
let response = desktop_external_url_from_request(&request_with_url(
"app.openExternalUrl",
url,
))
.expect_err("invalid external url");
let error = response.error.expect("error");
assert_eq!(error.code, "invalid_request");
assert_eq!(
error.message,
"url must use an allowed external protocol"
);
}
}
#[test]
fn desktop_native_page_request_accepts_only_same_origin_h5_routes() {
let route = desktop_native_page_url_from_request(&request_with_url(
"navigation.openNativePage",
"/works/detail?work=PZ-1",
))
.expect("native page url");
assert_eq!(
route.origin().ascii_serialization(),
"https://app.genarrative.world"
);
assert_eq!(route.path(), "/works/detail");
assert!(route
.query_pairs()
.any(|(key, value)| key == "hostShell" && value == "tauri_desktop"));
for url in [
"https://example.com/works",
"//example.com/works",
"javascript:alert(1)",
"https://app.genarrative.world/\nnext",
] {
let response = desktop_native_page_url_from_request(&request_with_url(
"navigation.openNativePage",
url,
))
.expect_err("invalid native page url");
let error = response.error.expect("error");
assert_eq!(error.code, "invalid_request");
assert_eq!(
error.message,
"url must use an allowed same-origin H5 route"
);
}
}
}
pub(crate) fn reload_desktop_host_bridge_webview(
app: &tauri::AppHandle,
request: &HostBridgeRequest,