mihomo-party-org/clash-party · error
Plugin URL must use a public host
Error message
Plugin URL must use a public host
What it means
parseDownloadUrl applies isForbiddenHost to the hostname and rejects non-public hosts with 'Plugin URL must use a public host'. This is an SSRF guard: downloads must not target localhost, private/loopback/link-local ranges, or other internal addresses reachable from the host machine.
Source
Thrown at src/main/resolve/plugin/remote.ts:16
import { getAppConfig } from '../../config/app'
import { MAX_PLUGIN_FILE_BYTES } from './constants'
import { requestOnce } from './http-client'
import { createGuardedLookup, isForbiddenHost } from './net-guard'
function parseDownloadUrl(url: string): URL {
let parsed: URL
try {
parsed = new URL(url)
} catch {
throw new Error('Invalid plugin URL')
}
if (parsed.protocol !== 'https:') throw new Error('Plugin URL must use https')
if (parsed.username || parsed.password) throw new Error('Plugin URL must not contain userinfo')
if (parsed.hash) throw new Error('Plugin URL must not contain a fragment')
if (isForbiddenHost(parsed.hostname)) throw new Error('Plugin URL must use a public host')
return parsed
}
export async function fetchRemotePlugin(url: string): Promise<string> {
const parsed = parseDownloadUrl(url)
const { subscriptionTimeout = 30000, pluginUseProxy } = await getAppConfig()
let proxy: { host: string; port: number } | undefined
if (pluginUseProxy) {
const { getControledMihomoConfig } = await import('../../config/controledMihomo')
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
proxy = { host: '127.0.0.1', port }
}
const response = await requestOnce(parsed.toString(), {
method: 'GET',
headers: { Accept: 'application/json, application/octet-stream' },
timeout: subscriptionTimeout,
maxBytes: MAX_PLUGIN_FILE_BYTES,View on GitHub (pinned to 911e090537)
Solutions
- Host the plugin file on a genuinely public https endpoint and use that URL.
- For development, expose the local server via a public https tunnel (e.g. a tunneling service) instead of the raw private address.
- Audit the URL host against the same forbidden-host rules (loopback, RFC1918, link-local) before calling.
- If an internal mirror is legitimate, front it with a public https gateway.
Example fix
// before
await fetchRemotePlugin('https://192.168.1.10:8080/plugin.yaml') // SSRF-guarded: rejected
// after
await fetchRemotePlugin('https://plugins.example.com/plugin.yaml') Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(input)
const host = u.hostname
const blocked =
host === 'localhost' || host === '0.0.0.0' || host === '::1' ||
/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) ||
/^169\.254\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host)
if (blocked) throw new Error('host must be a public address') Type guard
const isPublicHost = (s: string): boolean => {
try {
const h = new URL(s).hostname
return !(h === 'localhost' || h === '::1' || /^127\.|^10\.|^192\.168\.|^169\.254\.|^172\.(1[6-9]|2\d|3[01])\./.test(h))
} catch { return false }
} Try / catch
try {
await fetchRemotePlugin(input)
} catch (e) {
if (e.message === 'Plugin URL must use a public host') {
showUrlInputError('Private/loopback hosts are blocked (SSRF guard); use a public https endpoint')
} else throw e
} Prevention
- Never point downloads at localhost or RFC1918 addresses, even in dev — use a public tunnel instead
- Validate hostnames against private/loopback/link-local ranges at input time
- Treat SSRF rejection as a security signal, not a bug to bypass
- Keep internal mirrors behind a public https gateway if external access is needed
When it happens
Trigger: Passing a URL whose hostname resolves into a forbidden class — localhost/127.0.0.1, ::1, 10.x/172.16-31.x/192.168.x, 169.254.x link-local, 0.0.0.0, internal DNS names — e.g. 'https://localhost:8080/plugin.yaml' or 'https://192.168.1.10/p.yaml'.
Common situations: Testing the plugin downloader against a local dev server; an internal mirror configured with a private IP; an SSRF attempt via a crafted URL; a hostname that resolves to a private address on the current network.
Related errors
- transient
- Plugin URL must use https
- Plugin URL must not contain userinfo
- Invalid core path: directory traversal detected
- Invalid latest.yml from update source
AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30).
Data as JSON: /api/errors/e506a6d2a376506e.
Report an issue: GitHub.