CodeQL 2.26.1 added Spring WebFlux WebClient.UriSpec.uri as a server-side request forgery sink. Teams may therefore see new Java SSRF alerts after upgrading CodeQL even though the application code did not change.

The alert deserves investigation when any part of a request destination comes from a user, webhook, imported document, database field, or third-party API. The central question is simple: can untrusted input make the server connect somewhere the application owner did not intend?

Quick answer

Do not accept a complete outbound URL from a client. Map a small business identifier to a server-owned base URL, keep the path relative, reject redirects when they are unnecessary, and enforce network egress controls as a second boundary.

private static final Map<String, String> SERVICE_BASE_URLS = Map.of(
    "billing", "https://billing.internal.example",
    "catalog", "https://catalog.internal.example"
);

Mono<String> fetch(String service, String itemId) {
    String baseUrl = SERVICE_BASE_URLS.get(service);
    if (baseUrl == null || !itemId.matches("[A-Za-z0-9_-]{1,64}")) {
        return Mono.error(new IllegalArgumentException("Invalid request"));
    }

    return WebClient.create(baseUrl)
        .get()
        .uri(uri -> uri.path("/items/{id}").build(itemId))
        .retrieve()
        .bodyToMono(String.class);
}

The client chooses billing, not http://169.254.169.254/. The server owns the mapping and the scheme.

Why WebClient becomes an SSRF boundary

An outbound HTTP client runs with the server’s network position and identity. It may reach cloud metadata, internal dashboards, databases exposed over HTTP, or services that are invisible to the public internet. If user input controls the destination, an attacker can use your server as a network proxy.

String filtering alone is fragile. URLs have user-info sections, encoded characters, alternative IP formats, redirects, IPv6 literals, and DNS names whose answers can change. A check that looks correct on a few examples may parse differently from the HTTP client.

Trace the complete data flow

Start at the CodeQL source and follow transformations into uri(...). Check controller parameters, JSON bodies, headers, database records, queue messages, and webhook payloads. Data retrieved from your own database is not automatically trusted if a user originally wrote it.

Record what each variable is allowed to represent:

  • service identifier;
  • relative path segment;
  • query value;
  • complete origin;
  • redirect destination.

A relative search term needs escaping and length limits. A complete origin needs a much stronger policy and usually should not be user-controlled at all.

Prefer construction over URL validation

Build the request from server-owned components. Allowlist exact origins when dynamic destinations are an unavoidable product requirement.

URI parsed = URI.create(candidate);
boolean allowed = parsed.getScheme().equals("https")
    && parsed.getPort() == -1
    && ALLOWED_HOSTS.contains(parsed.getHost())
    && parsed.getUserInfo() == null;

if (!allowed) {
    throw new IllegalArgumentException("Destination is not allowed");
}

Resolve and evaluate IP destinations if arbitrary customer domains must be supported. Reject loopback, link-local, private, multicast, and reserved address ranges for every resolved address. Re-check redirects instead of approving only the first URL.

Add a network-level backstop

Application validation can fail. Restrict the workload’s egress with a proxy, firewall, Kubernetes network policy, or cloud security control. Permit only required destinations where the architecture allows it.

Protect cloud metadata with the provider’s current recommended controls. Do not assume blocking one familiar IPv4 address covers IPv6, DNS rebinding, redirects, or another infrastructure platform.

Test the fix with hostile destinations

Use a controlled test service rather than probing real internal systems. Verify rejection of:

  • localhost and loopback IP literals;
  • private and link-local ranges;
  • IPv6 loopback and mapped addresses;
  • a public URL redirecting to a blocked destination;
  • user-info and unusual port forms;
  • encoded path data that changes URL interpretation.

Also prove that the intended public or internal destination still works. A security change that silently breaks legitimate callbacks will be removed under operational pressure.

Review or dismiss the CodeQL result

If the destination is constant and the alert follows only a relative path, show that the base origin cannot change. If a shared helper performs strict validation, point the dismissal to that helper and its tests.

Do not dismiss the alert because the endpoint requires authentication. Authenticated users and compromised accounts can still exploit SSRF, and background jobs may process stored values later with broader credentials.

Primary sources

Treat the new sink model as a prompt to document outbound trust. Server-owned destinations, safe URI construction, redirect checks, hostile tests, and egress policy together provide stronger evidence than a regex placed in front of a user-supplied URL.