Skip to content

remote: block SSRF via private-IP Location headers in blob uploads#2295

Merged
Subserial merged 8 commits into
google:mainfrom
adilburaksen:fix/ssrf-location-header-upload
May 15, 2026
Merged

remote: block SSRF via private-IP Location headers in blob uploads#2295
Subserial merged 8 commits into
google:mainfrom
adilburaksen:fix/ssrf-location-header-upload

Conversation

@adilburaksen

Copy link
Copy Markdown
Contributor

Summary

nextLocation() in write.go extracts the Location header from
registry HTTP responses during blob upload and uses it verbatim as the
target for the next request in the upload sequence (POST → PATCH → PUT).

A malicious or compromised registry can return a Location header
pointing at a private or link-local IP address (e.g. 169.254.169.254
for cloud instance metadata, 10.x.x.x/192.168.x.x for internal
services), causing the client to send PATCH/PUT requests — including
the full container layer blob as the request body — to internal network
endpoints that are not directly reachable by the attacker.

Attack Scenario

  1. Attacker controls or compromises a registry at registry.example.com.
  2. Victim runs crane push registry.example.com/image:tag (or any tool
    using go-containerregistry to push images).
  3. Client sends POST /v2/image/blobs/uploads/ to initiate a blob upload.
  4. Attacker's registry responds:
    HTTP/1.1 202 Accepted
    Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/role
    
  5. nextLocation() returns the IMDS URL without any validation.
  6. Client sends PATCH http://169.254.169.254/... with the layer blob
    as the body — making a request to the cloud metadata service from
    the client's network context.

Fix

Add an IP-literal blocklist in nextLocation() that rejects cross-host
redirects targeting loopback, link-local, private (RFC 1918), and
unspecified addresses. Same-host redirects (different path on the same
registry) are always allowed so that test servers using 127.0.0.1
and self-referential registries are unaffected.

This mirrors the validateRealmURL() fix from #2243 for the
WWW-Authenticate realm SSRF, extended to cover the blob upload
Location header code path.

Test

Added TestNextLocationSSRFProtection which verifies that:

  • Cross-host redirects to private/link-local IPs are rejected.
  • Same-host redirects (including to loopback, for local test registries)
    are allowed.

nextLocation() extracts the Location header from registry responses
during blob upload (POST→PATCH→PUT sequence) and uses it verbatim as
the target for subsequent requests. A malicious or compromised registry
can return a Location pointing at a private or link-local IP address
(e.g. 169.254.169.254 for cloud IMDS, 10.x.x.x for internal services),
causing the client to send HTTP PATCH and PUT requests—including the
full layer blob as the body—to internal network endpoints that are
not directly reachable by the attacker.

Add an IP-literal blocklist to nextLocation() that rejects cross-host
redirects targeting loopback, link-local, private, and unspecified
addresses. Same-host redirects (different path on the same registry)
are always allowed so that test servers using 127.0.0.1 and self-
referential registries are unaffected.

This is analogous to the validateRealmURL() fix introduced in #2243
for WWW-Authenticate realm SSRF, extended to cover the upload location
code path.
makeFetcher() created an http.Client with no CheckRedirect policy,
so http.Client.Do() would silently follow 302 redirects issued by a
malicious registry to private or link-local IP addresses during any
pull operation (manifest fetch, blob download).

Add checkRedirectSSRF() as the CheckRedirect handler on the fetcher
client. It rejects cross-host redirects whose destination hostname
is a private or link-local IP literal (loopback, RFC 1918,
link-local unicast/multicast, unspecified), mirroring the check
already applied to blob upload Location headers in the previous
commit. Same-host redirects are always permitted.

This is the download-path counterpart to the upload-path fix
introduced in the previous commit for nextLocation().
@adilburaksen

Copy link
Copy Markdown
Contributor Author

Gentle ping — happy to address any review feedback or adjust the fix if needed.

@codecov-commenter

codecov-commenter commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.83%. Comparing base (6dad820) to head (153179a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2295      +/-   ##
==========================================
+ Coverage   56.75%   56.83%   +0.07%     
==========================================
  Files         165      165              
  Lines       11299    11319      +20     
==========================================
+ Hits         6413     6433      +20     
  Misses       4121     4121              
  Partials      765      765              

☔ 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.

adilburaksen and others added 5 commits May 14, 2026 20:29
Add unit tests for checkRedirectSSRF in fetcher.go:
- Cross-host redirects to private/link-local/loopback IPs are rejected
- Same-host redirects (including loopback for local test registries) are allowed
- Public IP and DNS hostname redirects are allowed
- Early-exit paths (empty via, nil Response) are covered
@Subserial Subserial merged commit e5983f2 into google:main May 15, 2026
17 checks passed
Subserial pushed a commit to Subserial/go-containerregistry that referenced this pull request May 15, 2026
…oogle#2295)

* remote: block SSRF via private-IP Location headers in blob uploads

nextLocation() extracts the Location header from registry responses
during blob upload (POST→PATCH→PUT sequence) and uses it verbatim as
the target for subsequent requests. A malicious or compromised registry
can return a Location pointing at a private or link-local IP address
(e.g. 169.254.169.254 for cloud IMDS, 10.x.x.x for internal services),
causing the client to send HTTP PATCH and PUT requests—including the
full layer blob as the body—to internal network endpoints that are
not directly reachable by the attacker.

Add an IP-literal blocklist to nextLocation() that rejects cross-host
redirects targeting loopback, link-local, private, and unspecified
addresses. Same-host redirects (different path on the same registry)
are always allowed so that test servers using 127.0.0.1 and self-
referential registries are unaffected.

This is analogous to the validateRealmURL() fix introduced in google#2243
for WWW-Authenticate realm SSRF, extended to cover the upload location
code path.

* remote: block SSRF via private-IP redirects in blob/manifest downloads

makeFetcher() created an http.Client with no CheckRedirect policy,
so http.Client.Do() would silently follow 302 redirects issued by a
malicious registry to private or link-local IP addresses during any
pull operation (manifest fetch, blob download).

Add checkRedirectSSRF() as the CheckRedirect handler on the fetcher
client. It rejects cross-host redirects whose destination hostname
is a private or link-local IP literal (loopback, RFC 1918,
link-local unicast/multicast, unspecified), mirroring the check
already applied to blob upload Location headers in the previous
commit. Same-host redirects are always permitted.

This is the download-path counterpart to the upload-path fix
introduced in the previous commit for nextLocation().

* remote: add TestCheckRedirectSSRF to cover fetcher SSRF protection

Add unit tests for checkRedirectSSRF in fetcher.go:
- Cross-host redirects to private/link-local/loopback IPs are rejected
- Same-host redirects (including loopback for local test registries) are allowed
- Public IP and DNS hostname redirects are allowed
- Early-exit paths (empty via, nil Response) are covered

* remote: fix goimports formatting in fetcher.go and fetcher_test.go

* remote: fix goimports formatting in write_test.go
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