Skip to content

Fix SSRF via jwks_uri#4000

Open
strehle wants to merge 6 commits into
developfrom
fix/security-finding-4-jwks-uri-ssrf
Open

Fix SSRF via jwks_uri#4000
strehle wants to merge 6 commits into
developfrom
fix/security-finding-4-jwks-uri-ssrf

Conversation

@strehle

@strehle strehle commented Jul 25, 2026

Copy link
Copy Markdown
Member

Problem: UAA allows operators and clients to configure a jwks_uri pointing to an external JWKS endpoint (used for private_key_jwt client authentication). There was no enforcement preventing a jwks_uri from resolving to internal/private network addresses, making UAA a vector for Server-Side Request Forgery (SSRF) and DNS rebinding
attacks — an attacker could point the URI at cloud instance-metadata endpoints (e.g. 169.254.169.254), internal services, or loopback addresses.

Fix — two layers of defense:

  1. Validation-time check (PrivateNetworkGuard): When a client registers or updates a jwks_uri, ClientJwtConfiguration now resolves the hostname at validation time and rejects any URI whose host resolves to a private, loopback, link-local, multicast, or cloud-metadata address (including 169.254.169.254). Unresolvable hosts are allowed through at validation time — the fetch will simply fail later.
  2. Fetch-time check (PrivateNetworkBlockingDnsResolver + safeRestTemplate): A new PrivateNetworkBlockingDnsResolver wraps the system DNS resolver and blocks private addresses at connection time. UaaHttpRequestUtils.createSafeRequestFactory builds a dedicated RestTemplate using this resolver. OidcMetadataFetcher uses this safeRestTemplate when fetching JWKS content for client JWT authentication, providing defense-in-depth against DNS rebinding (where a hostname resolves to a public IP at validation time but rebinds to a private IP at fetch time).

 Problem: UAA allows operators and clients to configure a jwks_uri pointing to an external JWKS endpoint (used for private_key_jwt client authentication). There was no enforcement preventing a jwks_uri from resolving to internal/private network addresses, making UAA a vector for Server-Side Request Forgery (SSRF) and DNS rebinding
  attacks — an attacker could point the URI at cloud instance-metadata endpoints (e.g. 169.254.169.254), internal services, or loopback addresses.

  Fix — two layers of defense:

  1. Validation-time check (PrivateNetworkGuard): When a client registers or updates a jwks_uri, ClientJwtConfiguration now resolves the hostname at validation time and rejects any URI whose host resolves to a private, loopback, link-local, multicast, or cloud-metadata address (including 169.254.169.254). Unresolvable hosts are allowed
  through at validation time — the fetch will simply fail later.
  2. Fetch-time check (PrivateNetworkBlockingDnsResolver + safeRestTemplate): A new PrivateNetworkBlockingDnsResolver wraps the system DNS resolver and blocks private addresses at connection time. UaaHttpRequestUtils.createSafeRequestFactory builds a dedicated RestTemplate using this resolver. OidcMetadataFetcher uses this
  safeRestTemplate when fetching JWKS content for client JWT authentication, providing defense-in-depth against DNS rebinding (where a hostname resolves to a public IP at validation time but rebinds to a private IP at fetch time).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces defense-in-depth protections against SSRF / DNS rebinding via operator- or client-supplied jwks_uri (used for private_key_jwt) by adding validation-time private-network checks and fetch-time DNS blocking in the HTTP client used to retrieve JWKS content.

Changes:

  • Add PrivateNetworkGuard + tests to detect/block private/loopback/link-local/multicast/metadata IP targets during jwks_uri validation.
  • Add PrivateNetworkBlockingDnsResolver and wire a new safeRestTemplate (with a DNS-blocking request factory) into OidcMetadataFetcher for JWKS fetching.
  • Update tests and bootstrap configuration paths to accommodate the new behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientJwtConfiguration.java Adds validation-time resolution and blocking for jwks_uri (HTTPS path) via PrivateNetworkGuard.
server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OidcMetadataFetcher.java Uses a new injected safeRestTemplate when fetching JWKS for ClientJwtConfiguration.
server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java Adds createSafeRequestFactory using a DNS resolver that blocks private addresses.
server/src/main/java/org/cloudfoundry/identity/uaa/util/PrivateNetworkGuard.java New utility implementing “blocked address” detection and assertPublic(URI) resolution logic.
server/src/main/java/org/cloudfoundry/identity/uaa/util/PrivateNetworkBlockingDnsResolver.java New HttpClient5 DnsResolver wrapper that rejects blocked/private addresses at resolve-time.
server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/RestTemplateConfig.java Adds a Spring @Bean for safeRestTemplate.
server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlBeansConfiguration.java Injects safeRestTemplate into the OidcMetadataFetcher bean.
server/src/test/java/org/cloudfoundry/identity/uaa/util/PrivateNetworkGuardTest.java New unit tests covering blocked vs public IPs and URI host handling.
server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientJwtConfigurationTest.java Adds a regression test ensuring private IP jwks_uri values are rejected.
server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java Updates bootstrap test JWKS URI example value.
Comments suppressed due to low confidence (1)

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientJwtConfiguration.java:256

  • _ cannot be used as an identifier in Java 25+. This catch parameter will not compile; use a named variable (or omit the binding by catching and ignoring with a named variable).
            } catch (java.net.UnknownHostException _) {

Comment thread server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientJwtConfiguration.java:247

  • validateJwksUri still allows http://localhost and skips the private-network guard for HTTP. Since JWKS fetching is now done via the safeRestTemplate (which blocks loopback/private addresses at connection time), this creates a configuration that can pass validation but will fail at runtime, and it also contradicts the PR description’s goal of rejecting loopback/private targets at validation time.

Consider requiring HTTPS and applying PrivateNetworkGuard.assertPublic(...) for all allowed URIs so misconfigurations fail fast.

        if (!"https".equals(validateJwksUri.getScheme()) && !"http".equals(validateJwksUri.getScheme())) {
            throw new InvalidClientDetailsException("Invalid private_key_jwt: jwks_uri must be either using https or http");
        }
        if ("http".equals(validateJwksUri.getScheme()) && !"localhost".equals(validateJwksUri.getHost())) {
            throw new InvalidClientDetailsException("Invalid private_key_jwt: jwks_uri with http is not on localhost");

server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java:262

  • This test configures jwks_uri as http://localhost..., which becomes invalid (or at least non-functional) once jwks_uri is restricted to public HTTPS endpoints to prevent SSRF/DNS rebinding. Using a non-local HTTPS URL here keeps the test focused on bootstrap persistence without relying on loopback exceptions.
        map.put("jwks_uri", "http://localhost:8080/uaa");

server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/RestTemplateConfig.java:42

  • safeRestTemplate() is built using createSafeRequestFactory(timeout), which hardcodes the default pool/keep-alive/retry settings rather than using the configurable rest.template.* properties already present in this config class. If operators tune maxTotal, maxPerRoute, maxKeepAlive, etc., those settings won’t apply to JWKS fetches (the new security-critical path).
    @Bean
    public RestTemplate safeRestTemplate() {
        return new RestTemplate(UaaHttpRequestUtils.createSafeRequestFactory(timeout));
    }

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (2)

server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java:108

  • createSafeRequestFactory() disables redirect handling entirely. This is a behavioral change from the other RestTemplates (which use DefaultRedirectStrategy) and will cause jwks_uri fetches to fail if an endpoint relies on HTTP redirects. If redirects must be supported, consider a redirect strategy that re-validates each redirect target (scheme + PrivateNetworkGuard + DNS blocking) rather than disabling redirects globally.
        HttpClientBuilder builder = HttpClients.custom()
                .useSystemProperties()
                .setUserTokenHandler(NoopUserTokenHandler.INSTANCE)
                .disableRedirectHandling();
        PoolingHttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()

uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ClientAdminEndpointsIntegrationTests.java:665

  • This integration test now uses an external hostname for jwks_uri, which will trigger DNS lookups during validation and can make tests sensitive to DNS/network availability. Prefer a public IP literal to avoid DNS dependencies.
        ClientJwtChangeRequest def = new ClientJwtChangeRequest(null, null, null);
        def.setJsonWebKeyUri("https://login.example.com/token_keys");
        def.setClientId("admin");

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OidcMetadataFetcher.java:88

  • jwks_uri fetches to localhost use nonTrustingRestTemplate, which is configured with DefaultRedirectStrategy (redirects enabled). This means a localhost JWKS endpoint could redirect to a private IP literal and the request would follow it, undermining the “redirects disabled” SSRF defense used by safeRestTemplate. Consider using a dedicated RestTemplate for localhost fetches with redirects disabled (but without private-network DNS blocking), or implement redirect validation/blocking for this code path.
            String jwksUri = clientJwtConfiguration.getJwksUri();
            RestTemplate template = isLocalhost(jwksUri) ? nonTrustingRestTemplate : safeRestTemplate;
            byte[] rawContents = getJsonBody(jwksUri, false, true, null, template);

uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ClientAdminEndpointsIntegrationTests.java:664

  • This test uses an external hostname (login.example.com) which will trigger DNS resolution during jwks_uri validation. In environments with restricted or slow DNS, this can make tests flaky or slow; using a public IP literal keeps the test deterministic while still exercising the HTTPS path.
        def.setJsonWebKeyUri("https://login.example.com/token_keys");

server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientJwtConfigurationTest.java:33

  • This unit test now triggers DNS resolution during jwks_uri validation (HTTPS paths call InetAddress.getAllByName). Using a public IP literal instead avoids dependence on external DNS and prevents slow/flaky runs in restricted CI environments.
        assertThat(ClientJwtConfiguration.parse("https://any.domain.net/openid/jwks-uri")).isNotNull();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants