{"record":{"id":"d549c7cbcb65d0a6","repo":"iflytek/astron-agent","slug":"outbound-hostname-is-blocked","errorCode":null,"errorMessage":"Outbound hostname is blocked","messagePattern":"Outbound hostname is blocked","errorType":"exception","errorClass":"OutboundPolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/link/infra/tool_exector/ssrf_guard.py","lineNumber":84,"sourceCode":"            const.IP_WHITE_LIST_KEY,\n        )\n        blocked_domains = _parse_domains(os.getenv(const.DOMAIN_BLACK_LIST_KEY, \"\"))\n        allowed_private_endpoints = _parse_private_endpoints(\n            os.getenv(const.PRIVATE_ENDPOINT_ALLOW_LIST_KEY, \"\")\n        )\n        return cls(\n            blocked_networks,\n            allowed_literal_networks,\n            blocked_domains,\n            allowed_private_endpoints,\n        )\n\n    def validate_url(self, url: str) -> SplitResult:\n        \"\"\"Validate URL syntax and any literal destination before DNS resolution.\"\"\"\n        parsed = _parse_http_url(url)\n        normalized_host = _normalize_hostname(parsed.hostname or \"\")\n        if self.is_domain_blocked(normalized_host):\n            raise OutboundPolicyError(\"Outbound hostname is blocked\")\n\n        literal = _parse_ip(normalized_host)\n        if literal is not None:\n            self.validate_address(\n                literal,\n                allow_private_endpoint=self.is_private_endpoint_allowed(parsed),\n                allow_literal_exception=True,\n            )\n        return parsed\n\n    def is_private_endpoint_allowed(self, parsed: SplitResult) -> bool:\n        \"\"\"Return whether deployment configuration authorizes this exact private endpoint.\"\"\"\n        # Private exceptions intentionally support only exact plain paths. Keeping\n        # semicolons in ``SplitResult.path`` prevents matrix parameters (including\n        # a trailing empty ``;``) from comparing equal to the configured path.\n        return (\n            \";\" not in parsed.path\n            and _endpoint(parsed) in self.allowed_private_endpoints","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/link/infra/tool_exector/ssrf_guard.py#L66-L102","documentation":"OutboundPolicy.validate_url checks the URL's hostname against the configured domain blacklist before DNS resolution. If the normalized hostname equals a blacklisted rule or is a subdomain of one (matching on label boundaries, including wildcard '*.rule' entries), it raises OutboundPolicyError('Outbound hostname is blocked'). This is the first line of the SSRF/egress defense for tool HTTP calls.","triggerScenarios":"Calling create_socket_factory or HttpRun._validate_destination with a URL whose host matches an entry in DOMAIN_BLACK_LIST (env), including any subdomain — e.g. blacklist 'evil.com' blocks 'api.evil.com' too.","commonSituations":"Ops blacklisted a domain that a tool legitimately needs; a suffix collision (e.g. blacklist contains 'notreal.com' and tool uses 'real.com' is NOT matched, but 'x.notreal.com' would be); tool was registered to a domain later added to the blacklist.","solutions":["Check DOMAIN_BLACK_LIST (and any '*.domain' wildcard entries) in the link-service environment for the hostname from the tool URL","If the domain is safe and required, remove or narrow the blacklist entry, or re-register the tool against an allowed domain","Verify hostname normalization (case, trailing dot, IDNA) is not causing an unintended match"],"exampleFix":"// before (env)\nDOMAIN_BLACK_LIST=internal.example.com\n// after (allow a required subdomain)\nDOMAIN_BLACK_LIST=other-internal.example.com","handlingStrategy":"validation","validationCode":"def domain_allowed(host, blocked_domains):\n    h = host.lower().rstrip('.')\n    return not any(h == d or h.endswith('.' + d) for d in blocked_domains)","typeGuard":null,"tryCatchPattern":"try:\n    result = await run.do_call(span)\nexcept CallThirdApiException as e:\n    if 'hostname is blocked' in str(e.err):\n        raise DomainBlockedError(e.err) from e\n    raise","preventionTips":["Review the tool's domain against DOMAIN_BLACK_LIST at registration time","Document the blacklist for teams registering new tools","Use exact, narrow blacklist entries and review wildcard rules periodically"],"tags":["security","ssrf","config"],"backgroundTag":"ssrf-blocked","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"}