Skip to content

transport: validate Bearer realm URL to prevent SSRF#2243

Merged
Subserial merged 3 commits intogoogle:mainfrom
evilgensec:fix/ssrf-bearer-realm-validation
Mar 31, 2026
Merged

transport: validate Bearer realm URL to prevent SSRF#2243
Subserial merged 3 commits intogoogle:mainfrom
evilgensec:fix/ssrf-bearer-realm-validation

Conversation

@evilgensec
Copy link
Copy Markdown
Contributor

@evilgensec evilgensec commented Mar 26, 2026

Summary

fromChallenge() stored the realm value from a WWW-Authenticate: Bearer header verbatim, with no validation of scheme or destination host. When the client subsequently fetched a token, it made an HTTP request to whatever URL the registry supplied — including private/link-local addresses and non-HTTP schemes.

Root cause:

realm, ok := pr.Parameters["realm"]  // from attacker-controlled header
// ... stored directly, no validation
return &bearerTransport{realm: realm, ...}

Attack scenario: An attacker-controlled registry (or MITM on an HTTP registry connection) returns:

WWW-Authenticate: Bearer realm="http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role",service="x"

The client sends a GET to the AWS/GCP instance metadata endpoint, leaking IAM credentials. The basic credentials sent with the token request are also leaked to the attacker-controlled realm.

This affects all tools using go-containerregistry for pulls: crane, ko, Kubernetes admission controllers, and CI/CD pipelines.

Fix

Add validateRealmURL() called from fromChallenge() before storing realm:

  • Scheme: only https for secure registries; http additionally allowed for insecure registries
  • IP literals: loopback, link-local unicast/multicast, and private-range addresses are rejected (blocks direct SSRF to RFC 1918 and cloud IMDS endpoints)

Related security report: https://issuetracker.google.com/issues/495960898

fromChallenge() stored the realm value from a WWW-Authenticate header
verbatim without validation. A malicious or MITM'd registry could supply
a realm pointing at a private/link-local address (e.g. 169.254.169.254)
or use a non-HTTP scheme, causing the client to make token-fetch requests
to internal services when pulling images.

Add validateRealmURL() which enforces:
- Scheme allowlist: only https is accepted for secure registries; http
  is additionally accepted when the registry is marked insecure.
- IP literal blocklist: loopback, link-local unicast/multicast, and
  private-range addresses are rejected. This blocks direct SSRF to cloud
  instance metadata services and RFC 1918 networks. DNS-based SSRF is
  out of scope and should be handled at the network layer.
@Subserial
Copy link
Copy Markdown
Contributor

Subserial commented Mar 26, 2026

The security report URL seems to be incorrect. The referenced issue seems unrelated.

@evilgensec
Copy link
Copy Markdown
Contributor Author

Dear @Subserial,

I have updated the report url: https://issuetracker.google.com/issues/495960898

As the security tab of the program instructs to report vulnerabilities through google bug hunter, I have done so. If anything else is needed please let me know.

@evilgensec
Copy link
Copy Markdown
Contributor Author

@Subserial Hope you are doing well. Is there any update on the issue?

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 33.33333% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.96%. Comparing base (8b3c303) to head (f014b61).
⚠️ Report is 82 commits behind head on main.

Files with missing lines Patch % Lines
pkg/v1/remote/transport/bearer.go 33.33% 8 Missing and 4 partials ⚠️

❗ There is a different number of reports uploaded between BASE (8b3c303) and HEAD (f014b61). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (8b3c303) HEAD (f014b61)
2 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2243       +/-   ##
===========================================
- Coverage   71.67%   52.96%   -18.72%     
===========================================
  Files         123      165       +42     
  Lines        9935    11215     +1280     
===========================================
- Hits         7121     5940     -1181     
- Misses       2115     4561     +2446     
- Partials      699      714       +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Subserial Subserial merged commit a2f47d4 into google:main Mar 31, 2026
17 checks passed
AlexJohnWilcox pushed a commit to AlexJohnWilcox/go-containerregistry that referenced this pull request Apr 9, 2026
Foreign layer descriptors in image manifests can contain arbitrary URLs
in the "urls" field. When the registry blob endpoint returns 404, the
client fetches these URLs with no validation, allowing a malicious
registry to trigger requests to internal services (SSRF).

Add validateForeignURL() to reject foreign layer URLs that use
non-HTTP(S) schemes or reference private, loopback, link-local, or
unspecified IP addresses. This is consistent with the existing
validateRealmURL() protection added in PR google#2243 for Bearer auth
realm URLs.

DNS-based SSRF remains out of scope, matching the design decision
in validateRealmURL().
AlexJohnWilcox added a commit to AlexJohnWilcox/go-containerregistry that referenced this pull request Apr 9, 2026
Foreign layer descriptors in image manifests can contain arbitrary URLs
in the "urls" field. When the registry blob endpoint returns 404, the
client fetches these URLs with no validation, allowing a malicious
registry to trigger requests to internal services (SSRF).

Add validateForeignURL() to reject foreign layer URLs that use
non-HTTP(S) schemes or reference private, loopback, link-local, or
unspecified IP addresses. This is consistent with the existing
validateRealmURL() protection added in PR google#2243 for Bearer auth
realm URLs.

DNS-based SSRF remains out of scope, matching the design decision
in validateRealmURL().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants