{"record":{"id":"44c6c6d7c9c42267","repo":"abhigyanpatwari/GitNexus","slug":"insecure-http-llm-base-urls-are-only-allowed-fo","errorCode":null,"errorMessage":"Insecure http:// LLM base URLs are only allowed for localhost/127.0.0.1 or hosts listed by --allow-insecure-connection / GITNEXUS_ALLOW_INSECURE_CONNECTION. Use https:// for remote endpoints (got ${parsed.origin})","messagePattern":"Insecure http:// LLM base URLs are only allowed for localhost/127\\.0\\.0\\.1 or hosts listed by --allow-insecure-connection / GITNEXUS_ALLOW_INSECURE_CONNECTION\\. Use https:// for remote endpoints \\(got (.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/wiki/llm-client.ts","lineNumber":262,"sourceCode":"    parsed = new URL(baseUrl);\n  } catch {\n    // Do not include the raw input in the message — it may contain credentials.\n    throw new Error('Invalid LLM base URL: must be a well-formed http:// or https:// URL');\n  }\n\n  if (!['https:', 'http:'].includes(parsed.protocol)) {\n    // Use parsed.protocol only (scheme), not the full URL, to avoid leaking credentials.\n    throw new Error(`LLM base URL must use http:// or https:// (got ${parsed.protocol})`);\n  }\n\n  if (parsed.protocol === 'http:') {\n    // Node's URL parser preserves IPv6 brackets in hostname (e.g. \"[::1]\"),\n    // so strip them before comparing to bare address literals.\n    const host = parsed.hostname.toLowerCase().replace(/^\\[|\\]$/g, '');\n    const allowedHosts = new Set(allowedInsecureHttpHosts.map(normalizeAllowedInsecureHttpHost));\n    if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1' && !allowedHosts.has(host)) {\n      // Use parsed.origin (scheme+host+port, no credentials) instead of the full URL.\n      throw new Error(\n        `Insecure http:// LLM base URLs are only allowed for localhost/127.0.0.1 ` +\n          `or hosts listed by --allow-insecure-connection / ${LLM_ALLOW_INSECURE_CONNECTION_ENV}. ` +\n          `Use https:// for remote endpoints (got ${parsed.origin})`,\n      );\n    }\n  }\n}\n\n/**\n * Returns true if the given base URL is an Azure OpenAI endpoint.\n * Uses proper hostname matching to avoid spoofed URLs like\n * \"https://myresource.openai.azure.com.evil.com/v1\".\n */\nexport function isAzureProvider(baseUrl: string): boolean {\n  try {\n    const { hostname } = new URL(baseUrl);\n    return hostname.endsWith('.openai.azure.com') || hostname.endsWith('.services.ai.azure.com');\n  } catch {","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/core/wiki/llm-client.ts#L244-L280","documentation":"The base URL uses http:// (plaintext) and its host is not localhost, 127.0.0.1, or ::1, and is not in the allowlist built from `--allow-insecure-connection` / GITNEXUS_ALLOW_INSECURE_CONNECTION. By default plaintext LLM traffic is restricted to loopback to prevent leaking API keys and to block SSRF against internal hosts; remote http:// endpoints must be explicitly opted into per hostname. The message shows parsed.origin (scheme+host+port, no credentials).","triggerScenarios":"Setting `--base-url http://192.168.1.10:8080/v1` or `http://llm.internal.corp/v1` for a LAN/self-hosted LLM (Ollama, LiteLLM proxy, vLLM on another machine) without a matching allowlist entry; hostname comparison fails after normalization (host:port entries in the allowlist are rejected upstream, trailing-case mismatches are handled but a different host string is not).","commonSituations":"Self-hosted LLM on a LAN box or Docker host (http://host.docker.internal, NAS IP); corporate internal endpoint that is http-only; forgetting that the allowlist matches the hostname exactly, not a domain suffix.","solutions":["Best: switch the endpoint to https:// (add TLS/terminating proxy) — no allowlist needed","Otherwise allowlist the exact host: `--allow-insecure-connection 192.168.1.10` or GITNEXUS_ALLOW_INSECURE_CONNECTION=llm.internal.corp","If the server is actually on this machine, use http://localhost:<port> or http://127.0.0.1:<port> which are always allowed","Confirm the allowlisted hostname matches the base URL host character-for-character (no port, no scheme, no path)"],"exampleFix":"# before\ngitnexus wiki --provider custom --base-url http://192.168.1.10:8080/v1\n\n# after\ngitnexus wiki --provider custom --base-url http://192.168.1.10:8080/v1 --allow-insecure-connection 192.168.1.10","handlingStrategy":"validation","validationCode":"function assertInsecureHttpAllowed(baseUrl: string, allowlist: readonly string[]): void {\n  const u = new URL(baseUrl);\n  if (u.protocol !== 'http:') return;\n  const host = u.hostname.toLowerCase().replace(/^\\[|\\]$/g, '');\n  if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return;\n  if (!allowlist.map(h => h.trim().toLowerCase()).includes(host)) {\n    throw new Error(`http://${host} not allowlisted — use https or add --allow-insecure-connection ${host}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  validateLLMBaseUrl(baseUrl, allowHosts);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Insecure http:// LLM base URLs')) {\n    // extract host via new URL(baseUrl).hostname and prompt user to allowlist it or upgrade to https\n  }\n}","preventionTips":["Prefer https endpoints; treat the allowlist as an exception mechanism only","Store allowlist entries in config next to the base URL so they stay in sync","For LAN inference boxes, terminate TLS locally (e.g. Caddy/nginx) instead of allowlisting plaintext"],"tags":["llm","ssrf-protection","http","security","configuration","gitnexus"],"backgroundTag":"insecure-connection-blocked","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}