收紧桌面壳错误响应边界
限制 Tauri HostBridge failed 出口只返回协议错误码 非法桌面错误码统一归一为 host_error 固定失败文案 新增桌面协议测试和配置门禁防止错误码漂移 同步 HostBridge envelope 错误归一决策记录
This commit is contained in:
@@ -1252,6 +1252,10 @@ const desktopHostBridgePayloadLimits = {
|
||||
};
|
||||
const desktopMethods = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_METHODS');
|
||||
const desktopEvents = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_EVENTS');
|
||||
const desktopErrorCodes = extractRustStringArrayConst(
|
||||
rustHostSource,
|
||||
'HOST_BRIDGE_ERROR_CODES',
|
||||
);
|
||||
const desktopHostBridgeProtocol = extractRustStringConst(
|
||||
rustHostSource,
|
||||
'HOST_BRIDGE_PROTOCOL',
|
||||
@@ -1287,6 +1291,25 @@ if (desktopHostBridgeVersion !== sharedHostBridgeVersion) {
|
||||
);
|
||||
}
|
||||
|
||||
assertSameList(
|
||||
desktopErrorCodes,
|
||||
[
|
||||
'invalid_request',
|
||||
'unsupported_method',
|
||||
'unsupported_capability',
|
||||
'timeout',
|
||||
'cancelled',
|
||||
'host_error',
|
||||
],
|
||||
'desktop shell HostBridge error codes',
|
||||
);
|
||||
if (
|
||||
!rustHostSource.includes('HOST_BRIDGE_ERROR_CODES.contains(&code)') ||
|
||||
!rustHostSource.includes('desktop host bridge request failed')
|
||||
) {
|
||||
throw new Error('desktop shell protocol must normalize non-contract host errors');
|
||||
}
|
||||
|
||||
if (desktopPublicWebOrigin !== sharedPublicWebOrigin) {
|
||||
throw new Error(
|
||||
`desktop shell public web origin drifted: expected ${sharedPublicWebOrigin} but got ${desktopPublicWebOrigin}`,
|
||||
|
||||
@@ -34,6 +34,14 @@ pub(crate) const HOST_BRIDGE_METHODS: [&str; 25] = [
|
||||
];
|
||||
pub(crate) const HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: usize = 120;
|
||||
const HOST_BRIDGE_RESPONSE_CACHE_MAX: usize = 128;
|
||||
const HOST_BRIDGE_ERROR_CODES: [&str; 6] = [
|
||||
"invalid_request",
|
||||
"unsupported_method",
|
||||
"unsupported_capability",
|
||||
"timeout",
|
||||
"cancelled",
|
||||
"host_error",
|
||||
];
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -159,6 +167,12 @@ pub(crate) fn failed(
|
||||
code: &'static str,
|
||||
message: impl Into<String>,
|
||||
) -> HostBridgeResponse {
|
||||
let (code, message) = if HOST_BRIDGE_ERROR_CODES.contains(&code) {
|
||||
(code, message.into())
|
||||
} else {
|
||||
("host_error", "desktop host bridge request failed".to_string())
|
||||
};
|
||||
|
||||
HostBridgeResponse {
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
@@ -167,7 +181,7 @@ pub(crate) fn failed(
|
||||
result: None,
|
||||
error: Some(HostBridgeError {
|
||||
code,
|
||||
message: message.into(),
|
||||
message,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -300,6 +314,20 @@ mod tests {
|
||||
assert_eq!(error.message, "invalid host bridge method");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_error_code_is_normalized_before_response() {
|
||||
let response = failed(
|
||||
"request-1".to_string(),
|
||||
"native_clipboard_failure",
|
||||
"native clipboard failed",
|
||||
);
|
||||
|
||||
assert!(!response.ok);
|
||||
let error = response.error.expect("error");
|
||||
assert_eq!(error.code, "host_error");
|
||||
assert_eq!(error.message, "desktop host bridge request failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_bridge_replay_state_reuses_first_response_for_duplicate_id() {
|
||||
let replay_state = HostBridgeReplayState::default();
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
- 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝自动生成权限目录缺失、权限文件集合漂移、多余 command、权限列表顺序漂移和残留的自动生成权限文件。
|
||||
- 2026-06-18 桌面壳 capability 最小化:Tauri 主窗口 capability 只授予 `allow-host-bridge-request`,不得授予 `core:default`、`core:*:default`、任意 core 子权限或 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再经 `host_bridge_request` 分发。
|
||||
- 2026-06-18 HostBridge request id replay:Expo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。
|
||||
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误;未知原生错误对象统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。
|
||||
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误,Tauri 壳的 `failed(...)` 出口也必须先校验同一错误码白名单;未知原生错误对象或非法错误码统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。
|
||||
- 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 method,Tauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。
|
||||
- 2026-06-18 HostBridge capability / handler 关系门禁:两端壳声明 request method capability 时必须有对应 HostBridge handler;壳 handler 处理的 method 必须已被该壳声明,登录 / 支付等 SDK-backed method 只能保留明确 `unsupported_method` 路径。事件类 capability 不要求 request handler。
|
||||
- 2026-06-18 桌面壳 CSP 分层:Tauri release `csp` 不得包含 `http://127.0.0.1:*`、`ws://127.0.0.1:*` 或其它本机调试源,本机 Vite、HMR WebSocket 和开发 frame 只允许出现在 `devCsp`。桌面壳配置检查会同时拒绝 release CSP 混入本机调试源、dev CSP 缺失本机开发源,以及 release / dev CSP 加入 `unsafe-eval`、`tauri:` 或 `file:`。
|
||||
|
||||
Reference in New Issue
Block a user