{"record":{"id":"44b218d47810be9a","repo":"unslothai/unsloth","slug":"provider-base-url-points-at-a-private-address-whi","errorCode":null,"errorMessage":"Provider base URL points at a private address, which is disabled on this server (UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS=1).","messagePattern":"Provider base URL points at a private address, which is disabled on this server \\(UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS=1\\)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"studio/backend/core/inference/providers.py","lineNumber":848,"sourceCode":"        if resolved is None:\n            # This path blocked on an unbounded getaddrinfo before the metadata\n            # check existed, and a resolver slower than that check's deadline is\n            # ordinary (the Linux default is 5s per server, twice). Falling back\n            # to the same unbounded call keeps a slow-but-working resolver from\n            # turning into a refusal here, where \"no answer\" fails closed.\n            import socket\n            try:\n                infos = socket.getaddrinfo(\n                    _transport_host(hostname),\n                    port or (443 if scheme == \"https\" else 80),\n                    type = socket.SOCK_STREAM,\n                )\n            except (OSError, UnicodeError) as exc:\n                raise ValueError(\"Provider base URL hostname could not be resolved.\") from exc\n            resolved = tuple(str(info[4][0]) for info in infos)\n        addresses = [ipaddress.ip_address(address.split(\"%\", 1)[0]) for address in resolved]\n    if not addresses or any(not ip.is_global for ip in addresses):\n        raise ValueError(\n            \"Provider base URL points at a private address, which is disabled on this \"\n            f\"server ({_BLOCK_PRIVATE_ENV}=1).\"\n        )\n\n\ndef validate_provider_base_url(base_url: str) -> str:\n    \"\"\"Return a normalized provider base URL, or raise ``ValueError``.\n\n    The backend issues outbound requests to this URL with the caller's decrypted\n    API key attached, so it is caller-controlled server-side egress. Only shapes\n    that can never be a real provider endpoint are refused: a non-http(s) scheme,\n    control characters, a missing host, and cloud metadata services. Plain http,\n    loopback, LAN hosts, odd ports, query strings and basic-auth userinfo all\n    stay valid -- Ollama, llama.cpp, vLLM and custom gateways rely on them. A\n    caller-supplied hostname is resolved far enough to apply the metadata block\n    to DNS aliases of it; rejecting other private addresses stays opt-in.\n\n    Normalization is strip + trailing-slash removal only (what the client did","sourceCodeStart":830,"sourceCodeEnd":866,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/providers.py#L830-L866","documentation":"Raised when UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS=1 and the resolved addresses of the provider base URL include a non-global IP (loopback, LAN, link-local, or an empty resolution list). This is an opt-in SSRF hardening: the backend sends the caller's decrypted API key to this URL, so private targets are refused. Plain http, odd ports, and userinfo remain allowed — only address reachability class is checked.","triggerScenarios":"Setting a provider base URL to http://192.168.1.10:11434/v1 (Ollama), http://localhost:8080 (llama.cpp), or a public name that resolves to a private IP while the block env var is set to 1.","commonSituations":"Self-hosted studios pointing at LAN LLM servers with the hardening flag inherited from a public deployment config; DNS rebinding where a public hostname resolves internally; 169.254.x addresses.","solutions":["If the private endpoint is intentional and trusted, unset UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS (or set to 0) — the check is explicitly opt-in.","Otherwise point the base URL at a genuinely public provider endpoint.","If a public hostname resolves privately (split-horizon DNS), use an endpoint that resolves publicly from this host.","Document which environments run with the flag on so LAN gateways are not configured there."],"exampleFix":"# before (flag on, LAN gateway refused)\nvalidate_provider_base_url(\"http://192.168.1.10:11434/v1\")\n# after (trust the LAN gateway)\n# export UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS=0\nvalidate_provider_base_url(\"http://192.168.1.10:11434/v1\")","handlingStrategy":"validation","validationCode":"import ipaddress, socket, os\n\ndef url_is_public(url: str) -> bool:\n    from urllib.parse import urlsplit\n    h = urlsplit(url).hostname\n    if not h:\n        return False\n    try:\n        infos = socket.getaddrinfo(h, None, type=socket.SOCK_STREAM)\n    except OSError:\n        return False\n    return all(ipaddress.ip_address(i[4][0]).is_global for i in infos)","typeGuard":null,"tryCatchPattern":"try:\n    validate_provider_base_url(url)\nexcept ValueError as e:\n    if \"private address\" in str(e):\n        if trusted_internal_gateway(url):\n            os.environ.pop(\"UNSLOTH_STUDIO_BLOCK_PRIVATE_PROVIDER_URLS\")\n            url = validate_provider_base_url(url)","preventionTips":["Decide per-deployment whether the private-URL block should be on; document it.","Never point production instances (flag on) at LAN gateways — they will be refused by design.","Watch for split-horizon DNS where public names resolve privately on the server."],"tags":["network","ssrf","validation","provider-config","security"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}