{"record":{"id":"814ba3073342b34b","repo":"iflytek/astron-agent","slug":"remote-resource-url-must-include-a-hostname","errorCode":null,"errorMessage":"Remote resource URL must include a hostname","messagePattern":"Remote resource URL must include a hostname","errorType":"exception","errorClass":"RemoteResourcePolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/aitools/common/clients/safe_download.py","lineNumber":213,"sourceCode":"\n\ndef _validate_url_characters(url: str) -> None:\n    if not isinstance(url, str) or any(\n        ord(character) < 0x20 or ord(character) == 0x7F for character in url\n    ):\n        raise RemoteResourcePolicyError(\"Remote resource URL is malformed\")\n\n\ndef _validate_parsed_resource_url(\n    parsed: SplitResult,\n    port: Optional[int],\n) -> None:\n    if parsed.scheme.lower() not in _ALLOWED_SCHEMES:\n        raise RemoteResourcePolicyError(\n            \"Only HTTP and HTTPS remote resources are allowed\"\n        )\n    if not parsed.hostname:\n        raise RemoteResourcePolicyError(\"Remote resource URL must include a hostname\")\n    if parsed.username is not None or parsed.password is not None:\n        raise RemoteResourcePolicyError(\n            \"Remote resource URL must not include user information\"\n        )\n    if \"\\\\\" in parsed.netloc:\n        raise RemoteResourcePolicyError(\"Remote resource URL authority is invalid\")\n    if parsed.fragment:\n        raise RemoteResourcePolicyError(\n            \"Remote resource URL must not include a fragment\"\n        )\n    if port is not None and not 1 <= port <= 65535:\n        raise RemoteResourcePolicyError(\"Remote resource URL port is invalid\")\n\n\ndef _normalize_hostname(hostname: str) -> str:\n    value = hostname.strip().lower().rstrip(\".\")\n    if _parse_ip(value) is not None:\n        return value","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/aitools/common/clients/safe_download.py#L195-L231","documentation":"_validate_parsed_resource_url requires a non-empty hostname. URLs whose netloc is empty or consist only of a scheme+path (e.g. 'https:///path' or 'https://file.pdf') are rejected because there is no remote host to validate against, making SSRF/destination checks impossible.","triggerScenarios":"URL like 'https:///file' or 'http://:8080/x'; string slicing bugs that drop the host; passing a path-only string after accidentally splitting off the domain; 'https://?query=1'.","commonSituations":"Template concatenation bugs (f'{base}{path}' where base is empty); joining URL parts with a helper that lost the host; config value for a base URL missing the domain (e.g. OSS download host configured as just '/bucket').","solutions":["Include the full authority in the URL: 'https://host.example.com/path'.","If building from parts, use urllib.parse.urljoin(base, path) with a complete absolute base URL.","Check the config/env value supplying the base URL contains the hostname."],"exampleFix":"// before\nurl = \"/bucket/file.pdf\"  # no host\nawait fetch_public_resource(url)\n// after\nfrom urllib.parse import urljoin\nurl = urljoin(\"https://files.example.com\", \"/bucket/file.pdf\")\nawait fetch_public_resource(url)","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\nassert bool(urlsplit(url).hostname), \"URL must include a hostname\"","typeGuard":"def has_hostname(u):\n    try:\n        return bool(urlsplit(u).hostname)\n    except (TypeError, ValueError):\n        return False","tryCatchPattern":"try:\n    data = await fetch_public_resource(url)\nexcept HTTPClientException as e:\n    if \"hostname\" in str(e):\n        ...  # fix URL construction / base-url config","preventionTips":["Use urljoin with an absolute base URL","Verify configured base URLs contain the domain","Reject path-only inputs at your API boundary"],"tags":["url","validation","hostname","python"],"backgroundTag":"invalid-url-format","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}