收口H5原生导航目标预校验
H5 原生 App 导航请求发送前拒绝外域和危险协议目标 H5 HostBridge 测试覆盖不安全导航目标不触达原生壳 原生壳门禁反查 navigation.openNativePage 预校验
This commit is contained in:
@@ -1080,6 +1080,19 @@ function assertH5HostBridgePayloadBoundaries() {
|
||||
'H5 HostBridge facade must normalize app.openExternalUrl payloads with the shared external URL boundary',
|
||||
);
|
||||
}
|
||||
if (
|
||||
!h5HostBridgeSource.includes('function normalizeNativeAppPageUrl(url: string)') ||
|
||||
!h5HostBridgeSource.includes("trimmedUrl.startsWith('//')") ||
|
||||
!h5HostBridgeSource.includes(
|
||||
"nativePageUrl.origin !== HOST_BRIDGE_PUBLIC_WEB_ORIGIN",
|
||||
) ||
|
||||
!h5HostBridgeSource.includes('const normalizedUrl = normalizeNativeAppPageUrl(url);') ||
|
||||
!h5HostBridgeSource.includes("{ url: normalizedUrl },")
|
||||
) {
|
||||
throw new Error(
|
||||
'H5 HostBridge facade must reject unsafe native app navigation targets before sending navigation.openNativePage',
|
||||
);
|
||||
}
|
||||
if (
|
||||
!h5HostBridgeSource.includes(
|
||||
'absolutizeHostSharePayloadUrls(params),',
|
||||
|
||||
@@ -1346,6 +1346,69 @@ describe('hostBridge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('原生 App 宿主导航请求发送前先拒绝明显不安全目标', async () => {
|
||||
const invoke = vi.fn(async (_command: string, args?: Record<string, unknown>) => {
|
||||
const request = (args as { request: { id: string } }).request;
|
||||
return {
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
id: request.id,
|
||||
ok: true,
|
||||
result: true,
|
||||
};
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
nativeAppPath(['navigation.openNativePage']),
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
navigateHostNativePage('https://example.com/works/detail?work=PZ-1'),
|
||||
).resolves.toBe(false);
|
||||
await expect(
|
||||
navigateHostNativePage('//example.com/works/detail?work=PZ-1'),
|
||||
).resolves.toBe(false);
|
||||
await expect(
|
||||
navigateHostNativePage('javascript:alert(1)'),
|
||||
).resolves.toBe(false);
|
||||
await expect(
|
||||
navigateHostNativePage('https://app.genarrative.world/\nnext'),
|
||||
).resolves.toBe(false);
|
||||
expect(invoke).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
navigateHostNativePage(
|
||||
'https://app.genarrative.world/works/detail?work=PZ-1',
|
||||
),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
navigateHostNativePage('works/detail?work=PZ-2'),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'navigation.openNativePage',
|
||||
payload: {
|
||||
url: 'https://app.genarrative.world/works/detail?work=PZ-1',
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'navigation.openNativePage',
|
||||
payload: {
|
||||
url: 'works/detail?work=PZ-2',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test('原生 App HostBridge 写剪贴板前按共享边界截断文本', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (_command: string, args?: Record<string, unknown>) => {
|
||||
|
||||
@@ -508,9 +508,13 @@ export async function navigateHostNativePage(
|
||||
if (!canUseNativeHostCapability('navigation.openNativePage')) {
|
||||
return false;
|
||||
}
|
||||
const normalizedUrl = normalizeNativeAppPageUrl(url);
|
||||
if (!normalizedUrl) {
|
||||
return false;
|
||||
}
|
||||
const result = await requestNativeHostBoolean(
|
||||
'navigation.openNativePage',
|
||||
{ url },
|
||||
{ url: normalizedUrl },
|
||||
);
|
||||
if (result) {
|
||||
options.beforeNavigate?.();
|
||||
@@ -604,6 +608,37 @@ function buildAbsoluteUrl(value: string) {
|
||||
return new URL(value, window.location.origin).href;
|
||||
}
|
||||
|
||||
function normalizeNativeAppPageUrl(url: string) {
|
||||
const trimmedUrl = url.trim();
|
||||
if (!trimmedUrl || /[\u0000-\u001f\u007f]/u.test(trimmedUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmedUrl.startsWith('//')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedUrl)) {
|
||||
try {
|
||||
const nativePageUrl = new URL(trimmedUrl);
|
||||
if (
|
||||
nativePageUrl.protocol !== 'http:' &&
|
||||
nativePageUrl.protocol !== 'https:'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (nativePageUrl.origin !== HOST_BRIDGE_PUBLIC_WEB_ORIGIN) {
|
||||
return null;
|
||||
}
|
||||
return nativePageUrl.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return trimmedUrl;
|
||||
}
|
||||
|
||||
function normalizeHostExternalUrlPayload(url: string) {
|
||||
const trimmedUrl = url.trim();
|
||||
if (!trimmedUrl) {
|
||||
|
||||
Reference in New Issue
Block a user