{"record":{"id":"ed9a1174762f62de","repo":"hiyouga/LlamaFactory","slug":"invalid-url-hostname","errorCode":null,"errorMessage":"Invalid URL hostname.","messagePattern":"Invalid URL hostname\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"src/llamafactory/api/common.py","lineNumber":79,"sourceCode":"\n        if not real_path.startswith(safe_path):\n            raise HTTPException(\n                status_code=status.HTTP_403_FORBIDDEN, detail=\"File access is restricted to the safe media directory.\"\n            )\n    except Exception:\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Invalid or inaccessible file path.\")\n\n\ndef check_ssrf_url(url: str) -> None:\n    \"\"\"Checks if a given URL is vulnerable to SSRF. Raises HTTPException if unsafe.\"\"\"\n    try:\n        parsed_url = urlparse(url)\n        if parsed_url.scheme not in [\"http\", \"https\"]:\n            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Only HTTP/HTTPS URLs are allowed.\")\n\n        hostname = parsed_url.hostname\n        if not hostname:\n            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Invalid URL hostname.\")\n\n        ip_info = socket.getaddrinfo(hostname, parsed_url.port)\n        ip_address_str = ip_info[0][4][0]\n        ip = ipaddress.ip_address(ip_address_str)\n\n        if not ip.is_global:\n            raise HTTPException(\n                status_code=status.HTTP_403_FORBIDDEN,\n                detail=\"Access to private or reserved IP addresses is not allowed.\",\n            )\n\n    except socket.gaierror:\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST, detail=f\"Could not resolve hostname: {parsed_url.hostname}\"\n        )\n    except Exception as e:\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f\"Invalid URL: {e}\")\n","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/api/common.py#L61-L97","documentation":"Raised as HTTP 400 by check_ssrf_url when urlparse(url).hostname is empty — the URL parses but has no host component. This catches malformed URLs like 'http://', 'https:///path', or scheme-only strings before getaddrinfo would be called with None.","triggerScenarios":"url = 'http://', 'https:///img.png', or a URL where the host was stripped by templating bugs (e.g. `${HOST}/img.png` with HOST unset).","commonSituations":"Environment-variable interpolation leaving an empty host; string concatenation bugs dropping the host; placeholder URLs left in config during testing.","solutions":["Fix the URL to include a real hostname, e.g. https://example.com/img.png.","Check templating/interpolation that builds media URLs for empty variables.","Add client-side validation that new URL(url).hostname is non-empty before sending."],"exampleFix":"// before\nurl: `${process.env.MEDIA_HOST}/img.png`  // MEDIA_HOST unset -> https:///img.png\n// after\nconst host = process.env.MEDIA_HOST; if (!host) throw new Error('MEDIA_HOST required');\nurl: `${host}/img.png`","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\ndef has_host(u):\n    return bool(urlparse(u).hostname)\n\nassert has_host(url), f\"URL missing host: {url!r}\"","typeGuard":"const hasHost = (u) => Boolean(new URL(u).hostname);","tryCatchPattern":"catch (e) { if (e.status === 400 && e.detail === 'Invalid URL hostname.') { fail fast on templating bug that produced the URL; } throw e; }","preventionTips":["Assert interpolated env vars are non-empty before building URLs.","Add URL sanity checks to config linting.","Test URL builders with unset-variable cases."],"tags":["security","ssrf","url-validation","http-400"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}