{"record":{"id":"b206423124d4a681","repo":"mihomo-party-org/clash-party","slug":"transient","errorCode":"transient","errorMessage":"endpoint escaped gateway origin","messagePattern":"endpoint escaped gateway origin","errorType":"exception","errorClass":"GatewayError","httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/gateway.ts","lineNumber":43,"sourceCode":"  constructor(kind: GatewayErrorKind, message: string, status?: number) {\n    super(message)\n    this.name = 'GatewayError'\n    this.kind = kind\n    this.status = status\n  }\n}\n\ninterface RawResult {\n  status: number\n  json: Record<string, unknown> | undefined\n  text: string\n}\n\nfunction urlOf(t: GatewayTarget, ep: keyof IGatewayEndpoints): string {\n  const u = new URL(t.endpoints[ep], t.gateway)\n  // 第二道防线：拼出的 URL 必须仍落在网关 origin 上（防端点逃逸到其它 host，如反斜杠/编码技巧）。\n  if (u.origin !== new URL(t.gateway).origin) {\n    throw new GatewayError('transient', 'endpoint escaped gateway origin')\n  }\n  return u.toString()\n}\n\nfunction lookupFor(net: GatewayNet): LookupFunction | undefined {\n  return net.proxy ? undefined : (net.lookup ?? createGuardedLookup())\n}\n\n// DNS 解析失败 / 连接拒绝 / TLS 失败 → 缓存网关“不可达/已退役”信号（spec §5），交由编排层重新发现。\n// 超时（'Request timed out' / ETIMEDOUT）、5xx、429、SSRF/重定向/大小拦截仍按瞬时失败退避，不在此列。\nconst UNREACHABLE_CODES = new Set([\n  'ENOTFOUND',\n  'EAI_AGAIN',\n  'ECONNREFUSED',\n  'ECONNRESET',\n  'EHOSTUNREACH',\n  'ENETUNREACH',\n  'EHOSTDOWN',","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/gateway.ts#L25-L61","documentation":"urlOf() builds the request URL by resolving a gateway endpoint against the gateway base URL and then verifies the resulting origin still matches the gateway's origin (the 'second line of defense'). If an endpoint value — often supplied by the server or a redirect — escapes to a different host (via backslash tricks, '@' userinfo, or encoding), this GatewayError('transient', ...) is thrown to block SSRF-style endpoint escape.","triggerScenarios":"A GatewayTarget.endpoints entry contains a value that, when fed to new URL(ep, gateway), resolves to a different origin — e.g. an absolute URL like 'https://evil.example/x', a protocol-relative '//evil.example/', or 'https://gw.example\\@evil.example'.","commonSituations":"A misconfigured or compromised gateway returning hostile endpoint URLs, or a local config with a typo'd absolute endpoint string instead of a relative path.","solutions":["Fix the endpoint entry in your gateway target config to be a relative path (e.g. '/v1/config').","If the gateway itself supplies endpoints, report/verify the gateway — a different origin means it is misbehaving or compromised.","This is classified 'transient'; retrying may help only if the gateway has been fixed, otherwise the endpoint config must change."],"exampleFix":"// before\nendpoints: { config: 'https://other-host.example/config' }\n// after\nendpoints: { config: '/config' } // relative, stays on gateway origin","handlingStrategy":"try-catch","validationCode":"function endpointsStayOnOrigin(t: GatewayTarget): boolean {\n  const origin = new URL(t.gateway).origin\n  return Object.values(t.endpoints).every(\n    (ep) => { try { return new URL(ep, t.gateway).origin === origin } catch { return false } }\n  )\n}","typeGuard":null,"tryCatchPattern":"try {\n  const url = urlOf(target, 'config')\n} catch (e) {\n  if (e instanceof GatewayError && e.code === 'transient' && e.message === 'endpoint escaped gateway origin') {\n    // reject the target config / quarantine the gateway — do not follow the escaped URL\n  } else throw e\n}","preventionTips":["Keep endpoint values relative paths in config; never absolute URLs to other hosts.","Treat endpoint escape as a security signal: investigate the gateway, don't just retry.","Re-validate endpoints whenever they come from server-supplied discovery data."],"tags":["security","ssrf","url","gateway"],"backgroundTag":"url-origin-mismatch","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}