mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
fix(security): restrict external URL opening
Close: 7800
This commit is contained in:
Generated
+23
@@ -1170,6 +1170,7 @@ dependencies = [
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-mihomo",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
@@ -7462,6 +7463,28 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"open",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.20",
|
||||
"url",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-http": "~2.5.9",
|
||||
"@tauri-apps/plugin-shell": "2.3.5",
|
||||
"@tauri-apps/plugin-opener": "2.5.4",
|
||||
"@tauri-apps/plugin-updater": "2.10.1",
|
||||
"ahooks": "^3.9.7",
|
||||
"cidr-block": "^2.3.0",
|
||||
|
||||
Generated
+6
-6
@@ -53,9 +53,9 @@ importers:
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ~2.5.9
|
||||
version: 2.5.9
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: 2.3.5
|
||||
version: 2.3.5
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: 2.5.4
|
||||
version: 2.5.4
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: 2.10.1
|
||||
version: 2.10.1
|
||||
@@ -1874,8 +1874,8 @@ packages:
|
||||
'@tauri-apps/plugin-http@2.5.9':
|
||||
resolution: {integrity: sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==}
|
||||
|
||||
'@tauri-apps/plugin-shell@2.3.5':
|
||||
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
||||
'@tauri-apps/plugin-opener@2.5.4':
|
||||
resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.10.1':
|
||||
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||
@@ -5830,7 +5830,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-shell@2.3.5':
|
||||
'@tauri-apps/plugin-opener@2.5.4':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", rev = "
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.5"
|
||||
tauri-plugin-opener = "2.5.4"
|
||||
tauri-plugin-dialog = "2.7.2"
|
||||
tauri-plugin-fs = "2.5.1"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
|
||||
@@ -63,11 +63,10 @@
|
||||
"core:window:allow-set-maximizable",
|
||||
"core:window:allow-set-minimizable",
|
||||
"core:webview:allow-print",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-open",
|
||||
"shell:allow-kill",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-stdin-write",
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [{ "url": "http://*" }, { "url": "https://*" }]
|
||||
},
|
||||
"dialog:allow-open",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
@@ -77,7 +76,6 @@
|
||||
"process:allow-exit",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"shell:default",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@ pub async fn open_logs_dir() -> CmdResult<()> {
|
||||
open::that(log_dir).stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
open::that(url.as_str()).stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
|
||||
@@ -16,6 +16,17 @@ use tokio::fs;
|
||||
use reqwest_dav::re_exports::url::form_urlencoded;
|
||||
use tauri::Url;
|
||||
|
||||
pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
|
||||
let url = Url::parse(raw.trim()).ok()?;
|
||||
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return None;
|
||||
}
|
||||
|
||||
url.host_str()?;
|
||||
Some(url.to_string().into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct PrfItem {
|
||||
pub uid: Option<String>,
|
||||
@@ -332,13 +343,10 @@ impl PrfItem {
|
||||
},
|
||||
};
|
||||
|
||||
let home = match header.get("profile-web-page-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
Some(str_value.into())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let home = header
|
||||
.get("profile-web-page-url")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(normalize_profile_home_url);
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
PrfOption,
|
||||
prfitem::{PrfItem, PrfSelected},
|
||||
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
||||
};
|
||||
use crate::{
|
||||
core::{handle, tray::Tray},
|
||||
@@ -106,6 +106,7 @@ impl IProfiles {
|
||||
match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut profiles) => {
|
||||
let items = profiles.items.get_or_insert_with(Vec::new);
|
||||
let mut home_changed = false;
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
@@ -116,6 +117,19 @@ impl IProfiles {
|
||||
.allow_auto_update
|
||||
.get_or_insert(true);
|
||||
}
|
||||
|
||||
if item
|
||||
.home
|
||||
.as_deref()
|
||||
.is_some_and(|home| normalize_profile_home_url(home).is_none())
|
||||
{
|
||||
item.home = None;
|
||||
home_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if home_changed && let Err(err) = profiles.save_file().await {
|
||||
logging!(error, Type::Config, "无法保存已清理的 profiles.yaml: {err}");
|
||||
}
|
||||
profiles
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ mod app_init {
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(
|
||||
@@ -128,7 +129,6 @@ mod app_init {
|
||||
cmd::get_embedded_server_port,
|
||||
cmd::open_app_dir,
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_web_url,
|
||||
cmd::open_core_dir,
|
||||
cmd::get_portable_flag,
|
||||
cmd::get_network_interfaces,
|
||||
|
||||
@@ -25,8 +25,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import { useAppRefreshers } from '@/providers/app-data-context'
|
||||
import { openWebUrl, updateProfile } from '@/services/cmds'
|
||||
import { updateProfile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
import { EnhancedCard } from './enhanced-card'
|
||||
@@ -49,6 +50,10 @@ const parseExpire = (expire?: number) => {
|
||||
return dayjs(expire * 1000).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const openProfileHome = (url: string) => {
|
||||
void openExternalUrl(url).catch(showNotice.error)
|
||||
}
|
||||
|
||||
// 使用类型定义,而不是导入
|
||||
interface ProfileExtra {
|
||||
upload: number
|
||||
@@ -115,7 +120,7 @@ const ProfileDetails = ({
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
onClick={() => current.home && openProfileHome(current.home)}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -318,7 +323,7 @@ export const HomeProfileCard = ({
|
||||
<Link
|
||||
component="button"
|
||||
variant="h6"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
onClick={() => current.home && openProfileHome(current.home)}
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
MenuItem,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
@@ -47,6 +46,7 @@ import { showNotice } from '@/services/notice-service'
|
||||
import { useLoadingCache, useSetLoadingCache } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
import { ProfileBox } from './profile-box'
|
||||
@@ -316,7 +316,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const onOpenHome = () => {
|
||||
setAnchorEl(null)
|
||||
open(itemData.home ?? '')
|
||||
if (!itemData.home) return
|
||||
void openExternalUrl(itemData.home).catch(showNotice.error)
|
||||
}
|
||||
|
||||
const onEditInfo = () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { alpha, Box, Button, LinearProgress } from '@mui/material'
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
@@ -19,6 +18,7 @@ import { useUpdate } from '@/hooks/use-update'
|
||||
import { restartApp } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useSetUpdateState, useUpdateState } from '@/services/states'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
|
||||
type MarkdownNode = {
|
||||
type: string
|
||||
@@ -151,6 +151,10 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const downloadedRef = useRef(0)
|
||||
const totalRef = useRef(0)
|
||||
|
||||
const openUrlWithNotice = (url: string) => {
|
||||
void openExternalUrl(url).catch(showNotice.error)
|
||||
}
|
||||
|
||||
const progress = useMemo(() => {
|
||||
if (total <= 0) return 0
|
||||
return Math.min((downloaded / total) * 100, 100)
|
||||
@@ -276,7 +280,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
size="small"
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
onClick={() => {
|
||||
openUrl(
|
||||
openUrlWithNotice(
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
|
||||
)
|
||||
}}
|
||||
@@ -420,10 +424,25 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<LazyReactMarkdown
|
||||
remarkPlugins={[remarkGitHubAlertsPlugin]}
|
||||
components={{
|
||||
a: ({ ...props }) => {
|
||||
const { children } = props
|
||||
a: ({ href, children, ...props }) => {
|
||||
return (
|
||||
<a {...props} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
{...props}
|
||||
href={href ? '#' : undefined}
|
||||
target={undefined}
|
||||
rel={undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
if (!href) return
|
||||
openUrlWithNotice(href)
|
||||
}}
|
||||
onAuxClick={(event) => {
|
||||
event.preventDefault()
|
||||
if (event.button === 1 && href) {
|
||||
openUrlWithNotice(href)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
|
||||
import { useClashInfo } from '@/hooks/use-clash'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { openWebUrl } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
|
||||
import { WebUIItem } from './web-ui-item'
|
||||
|
||||
@@ -98,7 +98,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
)
|
||||
}
|
||||
|
||||
await openWebUrl(url)
|
||||
await openExternalUrl(url)
|
||||
} catch (e: any) {
|
||||
showNotice.error('settings.modals.webUI.errors.openFailed', e)
|
||||
}
|
||||
|
||||
+8
-4
@@ -34,7 +34,9 @@ import { HomeProfileCard } from '@/components/home/home-profile-card'
|
||||
import { ProxyTunCard } from '@/components/home/proxy-tun-card'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { entry_lightweight_mode, openWebUrl } from '@/services/cmds'
|
||||
import { entry_lightweight_mode } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
|
||||
const preloadTestCard = () =>
|
||||
import('@/components/home/test-card').then((module) => ({
|
||||
@@ -240,9 +242,11 @@ const HomePage = () => {
|
||||
(verge?.home_cards as HomeCardsSettings | undefined) ?? DEFAULT_HOME_CARDS
|
||||
|
||||
// 文档链接函数
|
||||
const toGithubDoc = useLockFn(() => {
|
||||
return openWebUrl('https://clash-verge-rev.github.io/index.html')
|
||||
})
|
||||
const toGithubDoc = useLockFn(() =>
|
||||
openExternalUrl('https://clash-verge-rev.github.io/index.html').catch(
|
||||
showNotice.error,
|
||||
),
|
||||
)
|
||||
|
||||
// 新增:打开设置弹窗
|
||||
const openSettings = useCallback(() => {
|
||||
|
||||
+14
-10
@@ -8,9 +8,9 @@ import SettingClash from '@/components/setting/setting-clash'
|
||||
import SettingSystem from '@/components/setting/setting-system'
|
||||
import SettingVergeAdvanced from '@/components/setting/setting-verge-advanced'
|
||||
import SettingVergeBasic from '@/components/setting/setting-verge-basic'
|
||||
import { openWebUrl } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
|
||||
const SettingPage = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -19,17 +19,21 @@ const SettingPage = () => {
|
||||
showNotice.error(err)
|
||||
}
|
||||
|
||||
const toGithubRepo = useLockFn(() => {
|
||||
return openWebUrl('https://github.com/clash-verge-rev/clash-verge-rev')
|
||||
})
|
||||
const toGithubRepo = useLockFn(() =>
|
||||
openExternalUrl('https://github.com/clash-verge-rev/clash-verge-rev').catch(
|
||||
onError,
|
||||
),
|
||||
)
|
||||
|
||||
const toGithubDoc = useLockFn(() => {
|
||||
return openWebUrl('https://clash-verge-rev.github.io/index.html')
|
||||
})
|
||||
const toGithubDoc = useLockFn(() =>
|
||||
openExternalUrl('https://clash-verge-rev.github.io/index.html').catch(
|
||||
onError,
|
||||
),
|
||||
)
|
||||
|
||||
const toTelegramChannel = useLockFn(() => {
|
||||
return openWebUrl('https://t.me/clash_verge_re')
|
||||
})
|
||||
const toTelegramChannel = useLockFn(() =>
|
||||
openExternalUrl('https://t.me/clash_verge_re').catch(onError),
|
||||
)
|
||||
|
||||
const mode = useThemeMode()
|
||||
const isDark = mode === 'light' ? false : true
|
||||
|
||||
@@ -241,14 +241,6 @@ export async function openLogsDir() {
|
||||
return invoke<void>('open_logs_dir').catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export const openWebUrl = async (url: string) => {
|
||||
try {
|
||||
await invoke('open_web_url', { url })
|
||||
} catch (err: any) {
|
||||
showNotice.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>('test_delay', { url })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
export async function openExternalUrl(raw: string) {
|
||||
const url = new URL(raw.trim())
|
||||
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error('Only HTTP and HTTPS URLs are allowed')
|
||||
}
|
||||
|
||||
await openUrl(url.toString())
|
||||
}
|
||||
Reference in New Issue
Block a user