{"record":{"id":"8aebefefa6cf385b","repo":"spring-projects/spring-ai","slug":"host-resolves-to-a-blocked-internal-address","errorCode":null,"errorMessage":"Host '' resolves to a blocked internal address: ","messagePattern":"Host '' resolves to a blocked internal address: ","errorType":"exception","errorClass":"java.lang.SecurityException","httpStatus":null,"severity":"critical","filePath":"models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/api/MediaFetcher.java","lineNumber":311,"sourceCode":"\n\t/**\n\t * DNS resolver that rejects hostnames resolving to internal addresses. Acts as an\n\t * early-rejection layer for hostname-based requests, complementing the socket-level\n\t * check in {@link SsrfBlockingPlainSocketFactory} and\n\t * {@link SsrfBlockingSSLSocketFactory} which covers raw IP literals that skip DNS\n\t * resolution entirely.\n\t */\n\tprivate static final class SsrfSafeDnsResolver implements DnsResolver {\n\n\t\t@Override\n\t\tpublic InetAddress[] resolve(String host) throws UnknownHostException {\n\t\t\tInetAddress[] addresses = SystemDefaultDnsResolver.INSTANCE.resolve(host);\n\t\t\tfor (InetAddress address : addresses) {\n\t\t\t\tif (URLValidator.isBlockedAddress(address)) {\n\t\t\t\t\t// Throw SecurityException (RuntimeException) rather than\n\t\t\t\t\t// UnknownHostException so it propagates through Spring RestClient\n\t\t\t\t\t// without being wrapped in ResourceAccessException.\n\t\t\t\t\tthrow new SecurityException(\n\t\t\t\t\t\t\t\"Host '\" + host + \"' resolves to a blocked internal address: \" + address.getHostAddress());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn addresses;\n\t\t}\n\n\t\t@Override\n\t\tpublic String resolveCanonicalHostname(String host) throws UnknownHostException {\n\t\t\treturn SystemDefaultDnsResolver.INSTANCE.resolveCanonicalHostname(host);\n\t\t}\n\n\t}\n\n}\n","sourceCodeStart":293,"sourceCodeEnd":326,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/api/MediaFetcher.java#L293-L326","documentation":"MediaFetcher's custom DNS resolver wraps the system resolver and, immediately after resolving the media URL's host, checks every returned InetAddress against URLValidator.isBlockedAddress. If any resolved address is internal (loopback/private/link-local), resolution itself is aborted with this SecurityException so the request never proceeds to a connection. Like the other checks here it is a defense against SSRF via DNS.","triggerScenarios":"fetch() of a media URL whose hostname's DNS A/AAAA records include (or resolve solely to) a blocked internal address — checked in resolve() before any socket is opened.","commonSituations":"Split-horizon DNS where an internal name resolves to 10.x/192.168.x addresses; running the app inside a cluster where public-looking hostnames resolve to private IPs; localhost aliases or /etc/hosts entries mapping test domains to 127.0.0.1; stale DNS records repointed to private infrastructure.","solutions":["Point the media URL at a host that resolves to public addresses (public S3 bucket/CDN endpoint).","If the media is internal, download it yourself with your own authorized client and pass the bytes inline to Media instead of a URL.","Check DNS with dig/nslookup: if the host unexpectedly resolves to private IPs, correct the DNS record or use the correct public hostname.","In test environments, host the media on an externally resolvable endpoint or embed it inline rather than using loopback URLs."],"exampleFix":"// before\nMedia media = new Media(MimeTypeUtils.IMAGE_JPEG, new URL(\"http://internal-artifacts.corp/media/cat.jpg\"));\n// after: inline bytes fetched by your own internal client\nbyte[] bytes = artifactsClient.download(\"/media/cat.jpg\");\nMedia media = new Media(MimeTypeUtils.IMAGE_JPEG, bytes);","handlingStrategy":"validation","validationCode":"// Java: mirror the library's DNS-level check before submitting the URL\nInetAddress[] addrs = InetAddress.getAllByName(mediaUrl.getHost());\nfor (InetAddress a : addrs) {\n    if (a.isLoopbackAddress() || a.isLinkLocalAddress() || a.isSiteLocalAddress() || a.isAnyLocalAddress()) {\n        throw new IllegalArgumentException(\"Host \" + mediaUrl.getHost() + \" resolves to internal address \" + a.getHostAddress());\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    model.call(prompt);\n} catch (SecurityException e) {\n    if (e.getMessage().contains(\"resolves to a blocked internal address\")) {\n        // treat as rejected URL input; surface to user, no retry\n    } else { throw e; }\n}","preventionTips":["Check the host's DNS resolution (dig/nslookup) when testing media URLs from non-production networks.","Use public hostnames for media; avoid internal-only DNS names that resolve to private IPs.","Audit /etc/hosts and split-horizon DNS entries that could map test domains to loopback or private addresses.","Pre-fetch internal media in your own service and submit bytes inline instead of URLs."],"tags":["security","ssrf","dns","internal-address","bedrock"],"backgroundTag":"invalid-url","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}