fix(proxy): validate user-entered URLs via isValidUrl

Delay test inputs now reject non-http(s) URLs with a warning toast and
fall back to the default URL instead of erroring every node in the
core. Subscription import and profile card host parsing reuse the same
validator, fixing bare-domain URLs shown as local files and
credential-bearing URLs leaking into the card.
This commit is contained in:
Tunglies
2026-09-18 13:18:30 +08:00
parent f6fdb34745
commit c599f5547e
21 changed files with 67 additions and 5 deletions
+2 -1
View File
@@ -27,6 +27,7 @@ import { useNavigate } from 'react-router'
import { useAppRefreshers } from '@/providers/app-data-context'
import { updateProfile } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { isValidUrl } from '@/utils/network'
import { openExternalUrl } from '@/utils/open-external-url'
import parseTraffic from '@/utils/parse-traffic'
@@ -41,7 +42,7 @@ const round = keyframes`
// 辅助函数解析URL和过期时间
const parseUrl = (url?: string) => {
if (!url) return '-'
if (url.startsWith('http')) return new URL(url).host
if (isValidUrl(url)) return new URL(url).host
return 'local'
}
+2 -3
View File
@@ -43,6 +43,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 { isValidUrl } from '@/utils/network'
import { openExternalUrl } from '@/utils/open-external-url'
import parseTraffic from '@/utils/parse-traffic'
@@ -965,9 +966,7 @@ export const ProfileItem = memo(ProfileItemBase)
function parseUrl(url?: string) {
if (!url) return ''
const regex = /https?:\/\/(.+?)\//
const result = url.match(regex)
return result ? result[1] : 'local file'
return isValidUrl(url) ? new URL(url).host : 'local file'
}
function parseExpire(expire?: number) {
@@ -17,6 +17,8 @@ import { useTranslation } from 'react-i18next'
import { useVerge } from '@/hooks/use-verge'
import delayManager from '@/services/delay'
import { showNotice } from '@/services/notice-service'
import { isValidUrl } from '@/utils/network'
import { BaseSearchBox, type SearchState } from '../base'
@@ -174,6 +176,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
if (testUrl?.trim() && textState !== 'filter') {
onHeadState({ textState: 'url' })
}
if (testUrl?.trim() && !isValidUrl(testUrl)) {
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
return
}
onCheckDelay()
}}
>
+6
View File
@@ -18,7 +18,9 @@ import { useTranslation } from 'react-i18next'
import { BaseSearchBox } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import delayManager from '@/services/delay'
import { showNotice } from '@/services/notice-service'
import { debugLog } from '@/utils/debug'
import { isValidUrl } from '@/utils/network'
import type { ProxySortType } from './use-filter-sort'
import type { HeadState } from './use-head-state'
@@ -95,6 +97,10 @@ export const ProxyHead = ({
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
onHeadState({ textState: 'url' })
}
if (testUrl?.trim() && !isValidUrl(testUrl)) {
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
return
}
onCheckDelay()
}}
>
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} 업데이트 성공",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} успешно обновлён",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} 更新成功",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+2 -1
View File
@@ -59,6 +59,7 @@ import {
useThemeMode,
} from '@/services/states'
import { debugLog } from '@/utils/debug'
import { isValidUrl } from '@/utils/network'
// 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐
const PROFILE_UPDATE_WORKER_LIMIT = 8
@@ -214,7 +215,7 @@ const ProfilePage = () => {
const onImport = async () => {
if (!url) return
if (!/^https?:\/\//i.test(url)) {
if (!isValidUrl(url)) {
showNotice.error('profiles.page.feedback.errors.invalidUrl')
return
}
+6
View File
@@ -12,6 +12,7 @@ import {
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay'
import { isValidUrl } from '@/utils/network'
export type DelaySnapshot = {
of: (member: ResolvedProxyMember) => number
@@ -152,6 +153,11 @@ class DelayManager {
}
setUrl(group: string, url: string) {
if (!isValidUrl(url)) {
debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`)
this.urlMap.delete(group)
return
}
debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`)
this.urlMap.set(group, url)
}
+1
View File
@@ -324,6 +324,7 @@ export const translationKeys = [
'proxies.page.messages.directMode',
'proxies.page.title.default',
'proxies.page.title.chainMode',
'proxies.feedback.warnings.invalidTestUrl',
'proxies.feedback.notifications.provider.updateSuccess',
'proxies.feedback.notifications.provider.updateFailed',
'proxies.feedback.notifications.provider.genericError',
+3
View File
@@ -500,6 +500,9 @@ export interface TranslationResources {
updateSuccess: string
}
}
warnings: {
invalidTestUrl: string
}
}
page: {
actions: {