{"record":{"id":"8760c35d3292a3aa","repo":"iflytek/astron-agent","slug":"remote-resource-address-is-unsafe","errorCode":null,"errorMessage":"Remote resource address is unsafe","messagePattern":"Remote resource address is unsafe","errorType":"exception","errorClass":"RemoteResourcePolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/aitools/common/clients/safe_download.py","lineNumber":263,"sourceCode":"        return None\n\n\ndef _validate_destination_address(\n    address: IpAddress,\n    *,\n    allow_private_storage: bool,\n) -> None:\n    canonical = _canonical_address(address)\n    unsafe_properties = (\n        canonical.is_unspecified,\n        canonical.is_loopback,\n        canonical.is_link_local,\n        canonical.is_multicast,\n        canonical.is_reserved,\n        bool(getattr(canonical, \"is_site_local\", False)),\n    )\n    if any(unsafe_properties) or _matches_any(address, _NEVER_CONNECT_NETWORKS):\n        raise RemoteResourcePolicyError(\"Remote resource address is unsafe\")\n    if not allow_private_storage and not canonical.is_global:\n        raise RemoteResourcePolicyError(\"Remote resource address is unsafe\")\n\n\ndef _is_configured_storage_url(candidate: SplitResult) -> bool:\n    \"\"\"Authorize only objects under the server-configured S3 download origin/bucket.\"\"\"\n    if os.getenv(\"OSS_TYPE\", \"ifly_gateway_storage\").strip().lower() != \"s3\":\n        return False\n    origin_value = os.getenv(\"OSS_DOWNLOAD_HOST\", \"\").strip()\n    buckets = {\n        value\n        for setting in (\"OSS_BUCKET_NAME\", \"OSS_BUCKET_CONSOLE\")\n        if (value := os.getenv(setting, \"\").strip())\n        and value == value.lower()\n        and _S3_BUCKET_PATTERN.fullmatch(value) is not None\n    }\n    if not origin_value or not buckets:\n        return False","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/aitools/common/clients/safe_download.py#L245-L281","documentation":"RemoteResourcePolicyError raised by _validate_destination_address when the target IP is in an explicitly forbidden range: unspecified (0.0.0.0/::), loopback, link-local, multicast, reserved, IPv6 site-local, or any network in _NEVER_CONNECT_NETWORKS (documentation ranges, 6to4, NAT64, etc.). This is the core SSRF guard — these addresses could reach internal infrastructure or loop back to the server itself.","triggerScenarios":"fetch_public_resource called with a URL whose host resolves (or is a literal) to a forbidden address, e.g. 'http://127.0.0.1:8000', 'http://169.254.169.254/latest/meta-data' (cloud metadata), 'http://10.0.0.5/file', 'http://[::1]/', or DNS resolving a public-looking hostname to a private IP (caught in the socket_factory at connect time).","commonSituations":"Pointing the downloader at a local dev server during testing; cloud-metadata SSRF attempts via attacker-supplied URLs; internal service URLs mistakenly given to a public-download API; DNS rebinding where a hostname resolves to a private IP.","solutions":["Use a genuinely public URL (the resolved address must be a global unicast address)","If downloading from your own S3/object storage is intended, configure OSS_TYPE=s3 with OSS_DOWNLOAD_HOST and OSS_BUCKET_NAME/OSS_BUCKET_CONSOLE so private storage origins are whitelisted via _is_configured_storage_url","Expose the internal resource through a public endpoint instead of the private address","For local testing, use a public test server (e.g. an HTTPS URL on a public host) rather than localhost"],"exampleFix":"// before\nawait fetch_public_resource(\"http://127.0.0.1:9000/files/report.pdf\")  # loopback\n// after\n# serve through the configured public storage origin\nawait fetch_public_resource(\"https://s3.example.com/my-bucket/report.pdf\")\n# with OSS_TYPE=s3, OSS_DOWNLOAD_HOST=https://s3.example.com, OSS_BUCKET_NAME=my-bucket","handlingStrategy":"try-catch","validationCode":"import ipaddress, socket\ndef resolves_to_public(url: str) -> bool:\n    from urllib.parse import urlsplit\n    host = urlsplit(url).hostname or \"\"\n    try:\n        infos = socket.getaddrinfo(host, None)\n    except socket.gaierror:\n        return False\n    for info in infos:\n        addr = ipaddress.ip_address(info[4][0])\n        if not addr.is_global or addr.is_loopback or addr.is_link_local:\n            return False\n    return True","typeGuard":null,"tryCatchPattern":"from plugin.aitools.common.clients.safe_download import RemoteResourcePolicyError\nfrom plugin.aitools.common.exceptions.exceptions import HTTPClientException\ntry:\n    data = await fetch_public_resource(url)\nexcept HTTPClientException as e:\n    # fetch_public_resource wraps RemoteResourcePolicyError into HTTPClientException\n    log.warning(\"Download target rejected (SSRF policy): %s\", e)\n    return None","preventionTips":["Never pass localhost, 169.254.169.254, or private IPs to the public download API","Beware hostnames that resolve to private IPs (DNS rebinding); the socket-level check will reject them at connect time","Serve internal files through a public endpoint or the configured storage origin instead"],"tags":["ssrf","security","network","ip-validation","python"],"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"}