remote: block SSRF via private-IP Location headers in blob uploads#2295
Merged
Subserial merged 8 commits intoMay 15, 2026
Merged
Conversation
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().
Contributor
Author
|
Gentle ping — happy to address any review feedback or adjust the fix if needed. |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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
approved these changes
May 15, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nextLocation()inwrite.goextracts theLocationheader fromregistry 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
Locationheaderpointing at a private or link-local IP address (e.g.
169.254.169.254for cloud instance metadata,
10.x.x.x/192.168.x.xfor internalservices), 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
registry.example.com.crane push registry.example.com/image:tag(or any toolusing go-containerregistry to push images).
POST /v2/image/blobs/uploads/to initiate a blob upload.nextLocation()returns the IMDS URL without any validation.PATCH http://169.254.169.254/...with the layer blobas 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-hostredirects 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.1and self-referential registries are unaffected.
This mirrors the
validateRealmURL()fix from #2243 for theWWW-Authenticaterealm SSRF, extended to cover the blob uploadLocationheader code path.Test
Added
TestNextLocationSSRFProtectionwhich verifies that:are allowed.