收口宿主壳外链打开协议
共享 HostBridge 契约新增外链 URL 协议白名单校验 Expo 移动壳打开外链前拒绝危险协议 Tauri 桌面壳打开外链前拒绝危险协议 补充共享契约、移动壳和桌面壳外链校验测试 更新宿主壳方案和团队共享决策记录
This commit is contained in:
27
packages/shared/src/contracts/hostBridge.test.ts
Normal file
27
packages/shared/src/contracts/hostBridge.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { normalizeHostBridgeExternalUrl } from './hostBridge';
|
||||
|
||||
describe('HostBridge shared contract helpers', () => {
|
||||
test('只允许明确的外链协议交给宿主打开', () => {
|
||||
expect(normalizeHostBridgeExternalUrl(' https://example.com/a ')).toBe(
|
||||
'https://example.com/a',
|
||||
);
|
||||
expect(normalizeHostBridgeExternalUrl('mailto:hi@example.com')).toBe(
|
||||
'mailto:hi@example.com',
|
||||
);
|
||||
expect(normalizeHostBridgeExternalUrl('tel:+12345678')).toBe(
|
||||
'tel:+12345678',
|
||||
);
|
||||
});
|
||||
|
||||
test('拒绝空值、控制字符和危险协议', () => {
|
||||
expect(normalizeHostBridgeExternalUrl('')).toBeNull();
|
||||
expect(normalizeHostBridgeExternalUrl('javascript:alert(1)')).toBeNull();
|
||||
expect(normalizeHostBridgeExternalUrl('file:///etc/passwd')).toBeNull();
|
||||
expect(
|
||||
normalizeHostBridgeExternalUrl('https://example.com/\nnext'),
|
||||
).toBeNull();
|
||||
expect(normalizeHostBridgeExternalUrl('/relative/path')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -87,6 +87,49 @@ export type OpenExternalUrlPayload = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS = [
|
||||
'http:',
|
||||
'https:',
|
||||
'mailto:',
|
||||
'tel:',
|
||||
] as const;
|
||||
|
||||
export type HostBridgeExternalUrlProtocol =
|
||||
(typeof HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS)[number];
|
||||
|
||||
function hasHostBridgeControlCharacter(value: string) {
|
||||
return [...value].some((character) => {
|
||||
const codePoint = character.codePointAt(0) ?? 0;
|
||||
return codePoint <= 31 || codePoint === 127;
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeExternalUrl(rawUrl: unknown) {
|
||||
if (typeof rawUrl !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlText = rawUrl.trim();
|
||||
if (!urlText || hasHostBridgeControlCharacter(urlText)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(urlText);
|
||||
if (
|
||||
!HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS.includes(
|
||||
url.protocol as HostBridgeExternalUrlProtocol,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type ClipboardWriteTextPayload = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user