远程页面调不到命令
主线位置:调用主线上
parse_invoke_request取来源、on_message判断本地或远程、再交给 ACL 的那一段。来源怎么判定、remote.urls怎么匹配,运行时检查讲了机制;远程页面的ipc://请求长什么样,见 ipc 协议处理。本页复现最常见的一种:窗口本来是本地页面,跳到了外部网站。
flowchart TB
nav["窗口导航到 http 页面"] --> hdr["fetch ipc 带 Origin 头"]
hdr --> loc{"is_local_url"}
loc -->|否| rem["Origin Remote"]
rem --> cap{"有 remote.urls 匹配 Origin"}
cap -->|"模式带路径"| miss["匹配不上 拒绝"]
cap -->|没有| miss
cap -->|"模式只有源"| ok["按能力放行"]
实验 crate exp-tauri/pit3-acl(和 Command greet not allowed by ACL 同一个应用:有应用清单,能力 main 给 main 窗口 core:default 和 allow-greet,能力 report 给所有窗口和 http://127.0.0.1:19010 放行 allow-report)。远程页面由 python3 -m http.server --bind 127.0.0.1 19010 提供,内容和本地页面相同。tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口,debug 构建,平台 M2 Max、macOS 27、rustc 1.99.0。
现象
main 窗口先加载本地的 nav.html:调一次 greet,报告,然后跳到外部页面,像 OAuth 登录回跳、或者「直接打开线上那个网页版」那样:
(async () => {
const T = window.__TAURI_INTERNALS__;
const r = await T.invoke('greet', { name: 'rie' });
await T.invoke('report', { msg: `[local ${location.href}] greet -> ok ${JSON.stringify(r)}` });
// e.g. an OAuth redirect or a link to the hosted web app
location.href = 'http://127.0.0.1:19010/app/page.html';
})();
外部页面上同样调 greet、一个拼错的 gret、plugin:window|set_title:
REPORT [local tauri://localhost/nav.html] greet -> ok "hello, rie"
REPORT [main http://127.0.0.1:19010/app/page.html]
greet -> err greet not allowed on window "main", webview "main", URL: http://127.0.0.1:19010/
allowed on: [windows: "main", URL: local]
referenced by: capability: main, permission: allow-greet
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed. Permissions associated with this command: core:window:allow-set-title
同一个窗口、同一个 webview、同一个 greet,几毫秒之前还能调。页面上 __TAURI_INTERNALS__ 还在(tauri 的初始化脚本注入每一次导航后的主 frame),ipc:// 请求也照常到达 Rust,拦住它的是 ACL:来源从 local 变成了 http://127.0.0.1:19010/,main 能力只对 URL: local 生效。
按能力文档的写法补一份远程能力,模式写到页面所在的目录:
{
"identifier": "remote",
"windows": ["main"],
"remote": { "urls": ["http://127.0.0.1:19010/app/*"] },
"permissions": ["allow-greet"]
}
REPORT [local tauri://localhost/nav.html] greet -> ok "hello, rie"
REPORT [main http://127.0.0.1:19010/app/page.html]
greet -> err greet not allowed on window "main", webview "main", URL: http://127.0.0.1:19010/
allowed on: [windows: "main", URL: local], [windows: "main", URL: local], [windows: "main", URL: http://127.0.0.1:19010/app/*]
referenced by: capability: main, permission: allow-greet || capability: remote, permission: allow-greet || capability: remote, permission: allow-greet
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed. Permissions associated with this command: core:window:allow-set-title
还是拒绝。注意两处:拿来匹配的 URL 是 http://127.0.0.1:19010/,不是页面的 /app/page.html;allowed on 里 remote 能力贡献了两条,一条远程,还有一条 URL: local。
出处
ipc:// 这条路上,请求的来源 URL 是 parse_invoke_request 从 Origin 请求头解析出来的:
let url = Url::parse(
parts
.headers
.get("Origin")
.ok_or("missing Origin header")?
.to_str()
.map_err(|_| "Origin header value must be a string")?,
)
.map_err(|_| "Origin header is not a valid URL")?;
Origin 只有 scheme、主机和端口,解析后路径永远是 /。is_local_url 判断它不是 tauri://localhost、不在 devUrl / frontendDist 之下、也不是用户注册的协议,于是来源是 Remote;on_message 的拒绝条件里有 !is_local,远程来源不管有没有应用清单都要过 ACL。remote.urls 的模式按 URLPattern 去测这个路径为 / 的 URL,/app/* 自然不中。postMessage 退路取的是 frame 的完整 URL,带路径的模式在那条路上才会匹配,这个差别和实测见运行时检查的「remote.urls:匹配的是源,不是页面」。
多出来的那条 URL: local 来自能力的 local 字段:
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
#[serde(default = "default_capability_local")]
pub local: bool,
只写了 remote、没写 local 的能力,对本地页面同样生效,编译期解析给 local 一条、每个远程模式一条。
修法
模式只写源,并且显式关掉 local:
{
"identifier": "remote",
"windows": ["main"],
"local": false,
"remote": { "urls": ["http://127.0.0.1:19010"] },
"permissions": ["allow-greet"]
}
REPORT [local tauri://localhost/nav.html] greet -> ok "hello, rie"
REPORT [main http://127.0.0.1:19010/app/page.html]
greet -> ok "hello, rie"
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed. Permissions associated with this command: core:window:allow-set-title
再开一个本地的 settings 窗口看拒绝信息,allowed on 里 remote 能力只剩远程那一条:[windows: "main", URL: local], [windows: "main", URL: http://127.0.0.1:19010]。set_title 仍然被拒,因为远程能力里只给了 allow-greet:远程页面能调什么,就是这份能力列出的那几个,core:default 这类给本地页面的权限不会自动跟过去。
给远程页面开命令之前想清楚:这等于把这些命令交给那个源上能执行 JS 的任何东西,包括它加载的第三方脚本和它自己的 XSS 漏洞。授权的最小单位实际上是一个源(路径区分不了),远程能力里只放这个页面确实要调的命令。
怎么确认没中招
- debug 拒绝信息里的
URL:一栏就是参与匹配的来源。看到URL: http://.../而你以为是本地页面,说明窗口已经导航走了。 - 页面里打印
location.origin;Rust 侧可以给窗口挂on_navigation,记录(或者返回false拦下)意料之外的导航。 - 远程能力一律写成「只有源」的模式,并写上
"local": false;用 debug 拒绝信息核对allowed on里没有多出来的URL: local。 - 本地页面用
<iframe>嵌远程页面是另一回事:tauri 的初始化脚本只进主 frame,iframe 里连__TAURI_INTERNALS__都没有(见初始化脚本)。
小结
- 窗口导航到
http(s)页面后,来源变成远程,所有local能力不再适用,应用命令和插件命令一起被拒。 ipc://路上参与匹配的是Origin头,路径恒为/;remote.urls写成https://host/path/*永远匹配不上,只写源。- 能力的
local默认是true,只给远程用的能力要写"local": false。 - 远程能力是把命令交给一个源,里面只放必需的命令。