{"record":{"id":"363ba1c526f71aac","repo":"iflytek/astron-agent","slug":"outbound-url-origin-is-invalid","errorCode":null,"errorMessage":"Outbound URL origin is invalid","messagePattern":"Outbound URL origin is invalid","errorType":"exception","errorClass":"OutboundPolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/link/infra/tool_exector/ssrf_guard.py","lineNumber":175,"sourceCode":"            address,\n            allow_private_endpoint=allow_private_endpoint,\n            allow_literal_exception=literal_host,\n        )\n        return socket.socket(family=family, type=type_, proto=proto)\n\n    return socket_factory\n\n\ndef _origin(url: str) -> Tuple[str, str, int]:\n    try:\n        parsed = urlsplit(url)\n        scheme = parsed.scheme.lower()\n        hostname = _normalize_hostname(parsed.hostname or \"\")\n        port = parsed.port\n    except (TypeError, ValueError) as exc:\n        raise OutboundPolicyError(\"Outbound URL is malformed\") from exc\n    if scheme not in _ALLOWED_SCHEMES or not hostname:\n        raise OutboundPolicyError(\"Outbound URL origin is invalid\")\n    if parsed.username is not None or parsed.password is not None:\n        raise OutboundPolicyError(\"Outbound URL must not include user information\")\n    normalized_port = port if port is not None else (443 if scheme == \"https\" else 80)\n    return scheme, hostname, normalized_port\n\n\ndef _parse_http_url(url: str) -> SplitResult:\n    if not isinstance(url, str):\n        raise OutboundPolicyError(\"Outbound URL is malformed\")\n    _validate_url_characters(url)\n    try:\n        parsed = urlsplit(url)\n        port = parsed.port\n    except (TypeError, ValueError) as exc:\n        raise OutboundPolicyError(\"Outbound URL is malformed\") from exc\n    _validate_parsed_http_url(parsed, port)\n    return parsed\n","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/link/infra/tool_exector/ssrf_guard.py#L157-L193","documentation":"`_origin` rejects URLs whose scheme is not exactly `http` or `https`, or whose hostname normalizes to empty, raising `OutboundPolicyError('Outbound URL origin is invalid')`. The library only treats http/https origins as safe for outbound tool calls; anything else (ftp://, file://, ws://) or a URL without a usable host is refused before any network I/O.","triggerScenarios":"`ensure_same_origin(base, candidate)` or `_endpoint(parsed)` receiving a URL like `ftp://host/path`, a scheme-less string (`example.com/api` with no `https://`), or `https:///path` (empty host). Also fires when `_normalize_hostname` returns an empty string because `parsed.hostname` was empty/whitespace.","commonSituations":"Config values written without the scheme (`TOOL_URL=api.example.com`); scheme accidentally stripped by a proxy/normalization step; relative URLs passed where absolute ones are required; `file://` or custom-scheme URLs smuggled into a tool definition.","solutions":["Prefix the scheme explicitly: ensure the URL starts with `http://` or `https://` before passing it to the guard.","Check that a hostname is present after `//` in the URL; a trailing path with no authority (`https:///path`) is rejected.","If a non-HTTP protocol is genuinely required, this guard intentionally forbids it — route through an approved HTTP gateway instead of weakening the check.","Validate with `urlsplit(url)` in your own code: assert `url.scheme in ('http','https')` and `url.hostname` is non-empty before invoking the tool."],"exampleFix":"// before\ntool_url = \"api.example.com/chat\"   # no scheme -> origin invalid\n// after\ntool_url = \"https://api.example.com/chat\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\n\ndef has_http_origin(url) -> bool:\n    if not isinstance(url, str):\n        return False\n    p = urlsplit(url)\n    return p.scheme.lower() in (\"http\", \"https\") and bool(p.hostname)","typeGuard":"def is_http_url(value) -> bool:\n    if not isinstance(value, str):\n        return False\n    p = urlsplit(value)\n    return p.scheme in (\"http\", \"https\") and bool(p.hostname)","tryCatchPattern":"try:\n    ensure_same_origin(base_url, candidate_url)\nexcept OutboundPolicyError:\n    raise ValueError(f\"Tool URL must be an absolute http(s) URL with a hostname: {candidate_url!r}\")","preventionTips":["Store tool endpoints as full absolute URLs including scheme in config and env.","Add a config-load-time assertion that every URL field starts with http:// or https://.","Never accept relative or scheme-less URLs from user input; normalize with urljoin against a trusted base.","Reject file://, ftp://, and custom schemes at the ingestion boundary."],"tags":["security","ssrf","url-parsing","scheme"],"backgroundTag":"invalid-url","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"}