{"record":{"id":"221ff8c9322e4157","repo":"spring-projects/spring-ai","slug":"connection-to-blocked-internal-address-rejected-f","errorCode":null,"errorMessage":"Connection to blocked internal address  rejected for host ''","messagePattern":"Connection to blocked internal address  rejected for host ''","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":237,"sourceCode":"\t\tCloseableHttpClient httpClient = HttpClients.custom()\n\t\t\t.setConnectionManager(connectionManager)\n\t\t\t.disableRedirectHandling()\n\t\t\t.build();\n\t\treturn RestClient.builder().requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)).build();\n\t}\n\n\t/**\n\t * Checks the resolved {@link InetAddress} in {@code remoteAddress} and throws\n\t * {@link SecurityException} if it is a blocked internal address. Called by both\n\t * socket factories at connect time — after DNS resolution — so it catches raw IP\n\t * literals that bypass the {@link SsrfSafeDnsResolver}. Thrown as an unchecked\n\t * {@link RuntimeException} so it propagates through Spring RestClient without being\n\t * wrapped in {@link org.springframework.web.client.ResourceAccessException}.\n\t */\n\tprivate static void assertNotBlockedAddress(InetSocketAddress remoteAddress, HttpHost host) {\n\t\tInetAddress address = remoteAddress.getAddress();\n\t\tif (address != null && URLValidator.isBlockedAddress(address)) {\n\t\t\tthrow new SecurityException(\"Connection to blocked internal address \" + address.getHostAddress()\n\t\t\t\t\t+ \" rejected for host '\" + host.getHostName() + \"'\");\n\t\t}\n\t}\n\n\t/**\n\t * Plain-HTTP socket factory that blocks connections to internal addresses at connect\n\t * time. Extends {@link PlainConnectionSocketFactory} and delegates to it after the\n\t * address check, preserving all default socket behaviour.\n\t */\n\tprivate static final class SsrfBlockingPlainSocketFactory extends PlainConnectionSocketFactory {\n\n\t\t@Override\n\t\tpublic Socket connectSocket(TimeValue connectTimeout, Socket socket, HttpHost host,\n\t\t\t\tInetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context)\n\t\t\t\tthrows IOException {\n\t\t\tassertNotBlockedAddress(remoteAddress, host);\n\t\t\treturn super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);\n\t\t}","sourceCodeStart":219,"sourceCodeEnd":255,"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#L219-L255","documentation":"During media URL fetching, MediaFetcher installs a socket factory whose connect hook (assertNotBlockedAddress) inspects the remote address the connection is about to use. If that IP is classified by URLValidator.isBlockedAddress as internal (loopback, link-local, site-local/private, wildcard, or other internal ranges), the connection is rejected with this SecurityException. This is an SSRF guard: it stops a user-supplied URL from making the server talk to internal infrastructure.","triggerScenarios":"A media URL host resolves (or redirects at connect time) to an internal IP such as 127.0.0.1, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x (cloud metadata endpoint) or ::1, and the HTTP client attempts the TCP connect; assertNotBlockedAddress fires before the socket opens.","commonSituations":"Testing against localhost-hosted media servers while running the real app; DNS that returns private IPs (internal-only services, split-horizon DNS, stale DNS records); SSRF attempts using AWS metadata IP 169.254.169.254; redirects to internal hosts after an initially public URL.","solutions":["Serve the media from a genuinely public host that resolves to a public IP; move files to S3/CDN with public accessibility.","If you legitimately need internal media, fetch the bytes yourself and pass them inline (Media with a byte[]/Resource) instead of a URL.","Audit the DNS resolution for the host (nslookup/dig) — if it returns private addresses unintentionally, fix DNS records or the service's networking.","Do not attempt to weaken isBlockedAddress in production; only adjust the blocked-address policy consciously via URLValidator configuration if your environment allows it."],"exampleFix":"// before\nMedia media = new Media(MimeTypeUtils.IMAGE_PNG, new URL(\"http://169.254.169.254/latest/meta-data/...\"));\n// after - public URL\nMedia media = new Media(MimeTypeUtils.IMAGE_PNG, new URL(\"https://cdn.example.com/images/photo.png\"));\n// or inline the bytes fetched from an internal service:\n// byte[] bytes = internalClient.download(path);\n// Media media = new Media(MimeTypeUtils.IMAGE_PNG, bytes);","handlingStrategy":"validation","validationCode":"// Java: resolve the host yourself and reject private/internal IPs before calling the model\nfor (InetAddress a : InetAddress.getAllByName(url.getHost())) {\n    if (a.isLoopbackAddress() || a.isLinkLocalAddress() || a.isSiteLocalAddress() || a.isAnyLocalAddress()) {\n        throw new IllegalArgumentException(\"Media URL host resolves to internal address: \" + a.getHostAddress());\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    model.call(prompt);\n} catch (SecurityException e) {\n    if (e.getMessage().startsWith(\"Connection to blocked internal address\")) {\n        // refuse the media URL; do not retry — it is a policy rejection\n    } else { throw e; }\n}","preventionTips":["Never accept user URLs pointing at localhost, private ranges, or 169.254.169.254.","Serve media from public S3/CDN endpoints; pass internal content inline as bytes.","Validate/normalize URLs at your API boundary with the same private-IP rules before invoking the model.","Remember redirects can re-target internally — rely on the library's connect-time check rather than only pre-validating the initial host."],"tags":["security","ssrf","network","internal-address","bedrock"],"backgroundTag":"permission-denied","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"}