锁定桌面导航请求边界
桌面导航 HostBridge 单测覆盖外链协议白名单 桌面导航 HostBridge 单测覆盖同源 H5 路由限制 桌面壳配置检查反查导航请求边界测试
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user