spring-projects/spring-ai · critical · java.lang.SecurityException
Connection to blocked internal address rejected for host ''
Error message
Connection to blocked internal address rejected for host ''
What it means
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.
Source
Thrown at models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/api/MediaFetcher.java:237
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.disableRedirectHandling()
.build();
return RestClient.builder().requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)).build();
}
/**
* Checks the resolved {@link InetAddress} in {@code remoteAddress} and throws
* {@link SecurityException} if it is a blocked internal address. Called by both
* socket factories at connect time — after DNS resolution — so it catches raw IP
* literals that bypass the {@link SsrfSafeDnsResolver}. Thrown as an unchecked
* {@link RuntimeException} so it propagates through Spring RestClient without being
* wrapped in {@link org.springframework.web.client.ResourceAccessException}.
*/
private static void assertNotBlockedAddress(InetSocketAddress remoteAddress, HttpHost host) {
InetAddress address = remoteAddress.getAddress();
if (address != null && URLValidator.isBlockedAddress(address)) {
throw new SecurityException("Connection to blocked internal address " + address.getHostAddress()
+ " rejected for host '" + host.getHostName() + "'");
}
}
/**
* Plain-HTTP socket factory that blocks connections to internal addresses at connect
* time. Extends {@link PlainConnectionSocketFactory} and delegates to it after the
* address check, preserving all default socket behaviour.
*/
private static final class SsrfBlockingPlainSocketFactory extends PlainConnectionSocketFactory {
@Override
public Socket connectSocket(TimeValue connectTimeout, Socket socket, HttpHost host,
InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context)
throws IOException {
assertNotBlockedAddress(remoteAddress, host);
return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
}View on GitHub (pinned to 98a7beda4f)
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.
Example fix
// before
Media media = new Media(MimeTypeUtils.IMAGE_PNG, new URL("http://169.254.169.254/latest/meta-data/..."));
// after - public URL
Media media = new Media(MimeTypeUtils.IMAGE_PNG, new URL("https://cdn.example.com/images/photo.png"));
// or inline the bytes fetched from an internal service:
// byte[] bytes = internalClient.download(path);
// Media media = new Media(MimeTypeUtils.IMAGE_PNG, bytes); Defensive patterns
Strategy: validation
Validate before calling
// Java: resolve the host yourself and reject private/internal IPs before calling the model
for (InetAddress a : InetAddress.getAllByName(url.getHost())) {
if (a.isLoopbackAddress() || a.isLinkLocalAddress() || a.isSiteLocalAddress() || a.isAnyLocalAddress()) {
throw new IllegalArgumentException("Media URL host resolves to internal address: " + a.getHostAddress());
}
} Try / catch
try {
model.call(prompt);
} catch (SecurityException e) {
if (e.getMessage().startsWith("Connection to blocked internal address")) {
// refuse the media URL; do not retry — it is a policy rejection
} else { throw e; }
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Host '' is not in the allowed hosts list. Configure MediaFet
- Host '' resolves to a blocked internal address:
- Media URL response exceeds maximum allowed size of bytes:
- URL host '' resolves to a blocked internal address:
- Failed to read media data from URL:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/221ff8c9322e4157.
Report an issue: GitHub.