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

  1. Serve the media from a genuinely public host that resolves to a public IP; move files to S3/CDN with public accessibility.
  2. If you legitimately need internal media, fetch the bytes yourself and pass them inline (Media with a byte[]/Resource) instead of a URL.
  3. Audit the DNS resolution for the host (nslookup/dig) — if it returns private addresses unintentionally, fix DNS records or the service's networking.
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/221ff8c9322e4157. Report an issue: GitHub.