{"record":{"id":"00b83f5642bea275","repo":"mihomo-party-org/clash-party","slug":"plugin-url-must-not-contain-userinfo","errorCode":null,"errorMessage":"Plugin URL must not contain userinfo","messagePattern":"Plugin URL must not contain userinfo","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/remote.ts","lineNumber":14,"sourceCode":"import { getAppConfig } from '../../config/app'\nimport { MAX_PLUGIN_FILE_BYTES } from './constants'\nimport { requestOnce } from './http-client'\nimport { createGuardedLookup, isForbiddenHost } from './net-guard'\n\nfunction parseDownloadUrl(url: string): URL {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    throw new Error('Invalid plugin URL')\n  }\n  if (parsed.protocol !== 'https:') throw new Error('Plugin URL must use https')\n  if (parsed.username || parsed.password) throw new Error('Plugin URL must not contain userinfo')\n  if (parsed.hash) throw new Error('Plugin URL must not contain a fragment')\n  if (isForbiddenHost(parsed.hostname)) throw new Error('Plugin URL must use a public host')\n  return parsed\n}\n\nexport async function fetchRemotePlugin(url: string): Promise<string> {\n  const parsed = parseDownloadUrl(url)\n  const { subscriptionTimeout = 30000, pluginUseProxy } = await getAppConfig()\n  let proxy: { host: string; port: number } | undefined\n  if (pluginUseProxy) {\n    const { getControledMihomoConfig } = await import('../../config/controledMihomo')\n    const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()\n    proxy = { host: '127.0.0.1', port }\n  }\n\n  const response = await requestOnce(parsed.toString(), {\n    method: 'GET',\n    headers: { Accept: 'application/json, application/octet-stream' },","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/remote.ts#L1-L32","documentation":"parseDownloadUrl rejects URLs that embed userinfo (user:password@host) with 'Plugin URL must not contain userinfo'. Credentials in URLs leak into logs, error messages, and proxies, so they are forbidden as a security measure.","triggerScenarios":"Passing a URL of the form https://user:pass@example.com/plugin.yaml — e.g. someone put basic-auth credentials into the URL for an authenticated mirror.","commonSituations":"Copying an authenticated download link from a tool that embeds tokens in the URL; internal artifacts servers fronted with basic auth in the URL; shared bookmarks containing embedded credentials.","solutions":["Remove the user:password@ part and rely on publicly accessible https hosting.","If auth is required, put credentials in request headers/credentials handling rather than the URL, or host the file behind a tokenized (non-userinfo) signed link.","Ask the provider for a signed https URL (query-token or header-token) instead of basic-auth-in-URL."],"exampleFix":"// before\nawait fetchRemotePlugin('https://user:secret@files.example.com/plugin.yaml')\n// after\nawait fetchRemotePlugin('https://files.example.com/plugin.yaml') // host the file without URL-embedded credentials","handlingStrategy":"validation","validationCode":"const u = new URL(input)\nif (u.username || u.password) throw new Error('remove user:password@ from the URL')","typeGuard":"const hasNoUserinfo = (s: string): boolean => {\n  try { const u = new URL(s); return !u.username && !u.password } catch { return false }\n}","tryCatchPattern":"try {\n  await fetchRemotePlugin(input)\n} catch (e) {\n  if (e.message === 'Plugin URL must not contain userinfo') {\n    showUrlInputError('Credentials in the URL are not allowed; use a public or signed https link')\n  } else throw e\n}","preventionTips":["Sanitize pasted URLs: strip user:pass@ before storing","Prefer signed/tokenized https links over basic-auth-in-URL","Redact credentials from logs if a userinfo URL slips into config","Educate users that authenticated mirrors must expose a public https link"],"tags":["url","security","userinfo","validation"],"backgroundTag":"url-userinfo-not-allowed","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}