updated vendor

This commit is contained in:
2026-06-16 08:02:19 +02:00
parent 2f7f99d3f0
commit 77299d0c64
1283 changed files with 67302 additions and 208958 deletions
+122 -6
View File
@@ -19,6 +19,7 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
@@ -40,8 +41,9 @@ const (
// fetcher implements methods for reading from a registry.
type fetcher struct {
target resource
client *http.Client
target resource
client *http.Client
limiter *pullLimiter
}
func makeFetcher(ctx context.Context, target resource, o *options) (*fetcher, error) {
@@ -69,10 +71,40 @@ func makeFetcher(ctx context.Context, target resource, o *options) (*fetcher, er
}
return &fetcher{
target: target,
client: &http.Client{Transport: tr},
client: &http.Client{
Transport: tr,
CheckRedirect: checkRedirectSSRF,
},
limiter: o.limiter,
}, nil
}
// checkRedirectSSRF rejects HTTP redirects that cross from a public host to a
// private or link-local IP literal. This prevents a malicious registry from
// issuing a 302 to a cloud instance metadata service (e.g. 169.254.169.254)
// or another internal network address during blob or manifest downloads.
//
// Same-host redirects and redirects to non-IP hostnames (including DNS names
// that may resolve to private addresses) are allowed. The first redirect in
// the chain uses the original request URL as the "origin host" via
// req.Response.Request, falling back to req.URL when no prior response exists.
func checkRedirectSSRF(req *http.Request, via []*http.Request) error {
if len(via) == 0 || req.Response == nil {
return nil
}
origHost := via[0].URL.Hostname()
destHost := req.URL.Hostname()
if destHost == origHost {
return nil // same-host redirect is always allowed
}
if ip := net.ParseIP(destHost); ip != nil {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() || ip.IsUnspecified() {
return fmt.Errorf("SSRF protection: redirect from %q to private/link-local host %q denied", origHost, destHost)
}
}
return nil
}
func (f *fetcher) Do(req *http.Request) (*http.Response, error) {
return f.client.Do(req)
}
@@ -162,11 +194,20 @@ func (f *fetcher) fetchManifest(ctx context.Context, ref name.Reference, accepta
}
var artifactType string
var annotations map[string]string
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
artifactType = string(mf.Config.MediaType)
if mf != nil {
// Per the OCI distribution spec, artifactType on the descriptor is
// set to the manifest's artifactType if present, otherwise it falls
// back to the config descriptor's mediaType.
if mf.ArtifactType != "" {
artifactType = mf.ArtifactType
} else {
artifactType = string(mf.Config.MediaType)
}
annotations = mf.Annotations
}
// Do nothing for tags; I give up.
@@ -183,6 +224,7 @@ func (f *fetcher) fetchManifest(ctx context.Context, ref name.Reference, accepta
Size: size,
MediaType: mediaType,
ArtifactType: artifactType,
Annotations: annotations,
}
return manifest, &desc, nil
@@ -247,18 +289,30 @@ func (f *fetcher) headManifest(ctx context.Context, ref name.Reference, acceptab
func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) {
u := f.url("blobs", h.String())
return f.fetchBlobURL(ctx, u, size, h)
}
func (f *fetcher) fetchBlobURL(ctx context.Context, u url.URL, size int64, h v1.Hash) (io.ReadCloser, error) {
release, err := f.limiter.acquire(ctx)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
release()
return nil, err
}
resp, err := f.client.Do(req.WithContext(ctx))
if err != nil {
release()
return nil, redact.Error(err)
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
release()
return nil, err
}
@@ -269,11 +323,22 @@ func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.Read
if size == verify.SizeUnknown {
size = hsize
} else if hsize != size {
resp.Body.Close()
release()
return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size)
}
}
return verify.ReadCloser(resp.Body, size, h)
rc, err := verify.ReadCloser(resp.Body, size, h)
if err != nil {
resp.Body.Close()
release()
return nil, err
}
return &limitedReadCloser{
ReadCloser: rc,
release: release,
}, nil
}
func (f *fetcher) headBlob(ctx context.Context, h v1.Hash) (*http.Response, error) {
@@ -296,6 +361,57 @@ func (f *fetcher) headBlob(ctx context.Context, h v1.Hash) (*http.Response, erro
return resp, nil
}
// validateForeignURL rejects foreign layer URLs that use a disallowed scheme
// or resolve to a private / link-local IP address (SSRF protection). DNS-based
// SSRF is out of scope, matching transport.validateRealmURL.
func validateForeignURL(rawURL string, insecure bool) error {
u, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("parsing foreign layer URL %q: %w", rawURL, err)
}
switch u.Scheme {
case "https":
case "http":
if !insecure {
return fmt.Errorf("foreign layer URL scheme %q not allowed for a secure registry; use https", u.Scheme)
}
default:
return fmt.Errorf("foreign layer URL scheme %q not allowed; must be https (or http for insecure registries)", u.Scheme)
}
host := u.Hostname()
if ip := net.ParseIP(host); ip != nil {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() || ip.IsUnspecified() {
return fmt.Errorf("foreign layer URL host %q is a private or link-local address", host)
}
}
return nil
}
// fetchForeignBlobURL fetches a foreign-layer blob, validating every redirect
// destination through validateForeignURL (SSRF protection).
func (f *fetcher) fetchForeignBlobURL(ctx context.Context, u url.URL, size int64, h v1.Hash, insecure bool) (io.ReadCloser, error) {
safeClient := &http.Client{
Transport: f.client.Transport,
CheckRedirect: func(req *http.Request, _ []*http.Request) error {
return validateForeignURL(req.URL.String(), insecure)
},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := safeClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("GET %s: unexpected status %s", u.String(), resp.Status)
}
return verify.ReadCloser(resp.Body, size, h)
}
func (f *fetcher) blobExists(ctx context.Context, h v1.Hash) (bool, error) {
u := f.url("blobs", h.String())
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+19 -21
View File
@@ -18,7 +18,6 @@ import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"sync"
@@ -27,7 +26,6 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@@ -172,7 +170,7 @@ func (rl *remoteImageLayer) Digest() (v1.Hash, error) {
// Compressed implements partial.CompressedLayer
func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
urls := []url.URL{rl.ri.fetcher.url("blobs", rl.digest.String())}
u := rl.ri.fetcher.url("blobs", rl.digest.String())
// Add alternative layer sources from URLs (usually none).
d, err := partial.BlobDescriptor(rl, rl.digest)
@@ -187,38 +185,38 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
// We don't want to log binary layers -- this can break terminals.
ctx := redact.NewContext(rl.ctx, "omitting binary blobs from logs")
for _, s := range d.URLs {
u, err := url.Parse(s)
if err != nil {
return nil, err
}
urls = append(urls, *u)
}
insecure := rl.ri.fetcher.target.Scheme() == "http"
// The lastErr for most pulls will be the same (the first error), but for
// foreign layers we'll want to surface the last one, since we try to pull
// from the registry first, which would often fail.
// TODO: Maybe we don't want to try pulling from the registry first?
var lastErr error
for _, u := range urls {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
rc, err := rl.ri.fetcher.fetchBlobURL(ctx, u, d.Size, rl.digest)
if err == nil {
return rc, nil
}
lastErr = err
foreignURLs := make([]url.URL, 0, len(d.URLs))
for _, s := range d.URLs {
if err := validateForeignURL(s, insecure); err != nil {
return nil, err
}
fu, err := url.Parse(s)
if err != nil {
return nil, err
}
foreignURLs = append(foreignURLs, *fu)
}
resp, err := rl.ri.fetcher.Do(req.WithContext(ctx))
for _, fu := range foreignURLs {
rc, err := rl.ri.fetcher.fetchForeignBlobURL(ctx, fu, d.Size, rl.digest, insecure)
if err != nil {
lastErr = err
continue
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
lastErr = err
continue
}
return verify.ReadCloser(resp.Body, d.Size, rl.digest)
return rc, nil
}
return nil, lastErr
+6 -2
View File
@@ -225,8 +225,12 @@ func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform)
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
child.ArtifactType = string(mf.Config.MediaType)
if mf != nil {
if mf.ArtifactType != "" {
child.ArtifactType = mf.ArtifactType
} else {
child.ArtifactType = string(mf.Config.MediaType)
}
}
}
@@ -0,0 +1,182 @@
// Copyright 2014 Docker, Inc.
// Copyright 2021-2026 The Distribution contributors
// Copyright 2026 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authchallenge
import (
"net/http"
"strings"
)
// Octet types from RFC 2616.
type octetType byte
var octetTypes [256]octetType
const (
isToken octetType = 1 << iota
isSpace
)
func init() {
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := range 256 {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c))
if strings.ContainsRune(" \t\r\n", rune(c)) {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {
t |= isToken
}
octetTypes[c] = t
}
}
// Challenge carries information from a WWW-Authenticate response header.
// See RFC 2617.
type Challenge struct {
// Scheme is the auth-scheme according to RFC 2617
Scheme string
// Parameters are the auth-params according to RFC 2617
Parameters map[string]string
}
// ResponseChallenges returns a list of authorization challenges
// for the given http Response. Challenges are only checked if
// the response status code was a 401.
func ResponseChallenges(resp *http.Response) []Challenge {
if resp.StatusCode == http.StatusUnauthorized {
// Parse the WWW-Authenticate Header and store the challenges
// on this endpoint object.
return parseAuthHeader(resp.Header)
}
return nil
}
func parseAuthHeader(header http.Header) []Challenge {
challenges := []Challenge{}
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
v, p := parseValueAndParams(h)
if v != "" {
challenges = append(challenges, Challenge{Scheme: v, Parameters: p})
}
}
return challenges
}
func parseValueAndParams(header string) (value string, params map[string]string) {
params = make(map[string]string)
value, s := expectToken(header)
if value == "" {
return
}
value = strings.ToLower(value)
s = "," + skipSpace(s)
for strings.HasPrefix(s, ",") {
var pkey string
pkey, s = expectToken(skipSpace(s[1:]))
if pkey == "" {
return
}
if !strings.HasPrefix(s, "=") {
return
}
var pvalue string
pvalue, s = expectTokenOrQuoted(s[1:])
if pvalue == "" {
return
}
pkey = strings.ToLower(pkey)
params[pkey] = pvalue
s = skipSpace(s)
}
return
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
break
}
}
return s[i:]
}
func expectToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isToken == 0 {
break
}
}
return s[:i], s[i:]
}
func expectTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return expectToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j++
}
}
return "", ""
}
}
return "", ""
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2026 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"context"
"io"
"sync"
)
type pullLimiter struct {
tokens chan struct{}
}
func newPullLimiter(jobs int) *pullLimiter {
return &pullLimiter{
tokens: make(chan struct{}, jobs),
}
}
func (l *pullLimiter) acquire(ctx context.Context) (func(), error) {
if l == nil {
return func() {}, nil
}
select {
case l.tokens <- struct{}{}:
return func() { <-l.tokens }, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
type limitedReadCloser struct {
io.ReadCloser
release func()
once sync.Once
}
func (l *limitedReadCloser) Close() error {
err := l.ReadCloser.Close()
l.once.Do(l.release)
return err
}
@@ -45,6 +45,7 @@ type options struct {
retryBackoff Backoff
retryPredicate retry.Predicate
retryStatusCodes []int
limiter *pullLimiter
// Only these options can overwrite Reuse()d options.
platform v1.Platform
@@ -92,6 +93,7 @@ var fastBackoff = Backoff{
var defaultRetryStatusCodes = []int{
http.StatusRequestTimeout,
http.StatusTooManyRequests, // 429: OCI distribution-spec rate limit; TooManyRequestsErrorCode is already classified temporary in transport/error.go
http.StatusInternalServerError,
http.StatusBadGateway,
http.StatusServiceUnavailable,
@@ -142,6 +144,7 @@ func makeOptions(opts ...Option) (*options, error) {
return nil, err
}
}
o.limiter = newPullLimiter(o.jobs)
switch {
case o.auth != nil && o.keychain != nil:
+6 -2
View File
@@ -156,7 +156,11 @@ func (p *Pusher) Upload(ctx context.Context, repo name.Repository, l v1.Layer) e
}
func (p *Pusher) Delete(ctx context.Context, ref name.Reference) error {
w, err := p.writer(ctx, ref.Context(), p.o)
// Use a transport scoped for delete. Requesting DeleteScope (which
// includes the "delete" action) allows registries that require an
// explicit delete permission—such as IBM Cloud Container Registry—to
// grant access.
client, err := makeDeleteClient(ctx, ref.Context(), p.o)
if err != nil {
return err
}
@@ -172,7 +176,7 @@ func (p *Pusher) Delete(ctx context.Context, ref name.Reference) error {
return err
}
resp, err := w.w.client.Do(req.WithContext(ctx))
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
return err
}
+1 -1
View File
@@ -67,7 +67,7 @@ func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string,
var b []byte
if resp.StatusCode == http.StatusOK && resp.Header.Get("Content-Type") == string(types.OCIImageIndex) {
b, err = io.ReadAll(resp.Body)
b, err = io.ReadAll(io.LimitReader(resp.Body, manifestLimit))
if err != nil {
return nil, err
}
@@ -26,14 +26,17 @@ import (
"strings"
"sync"
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/internal/authchallenge"
)
// maxTokenBodySize limits bearer token response body reads to prevent OOM
// when a token endpoint returns an unexpectedly large body.
const maxTokenBodySize = 64 * 1024 // 64 KiB
type Token struct {
Token string `json:"token"`
AccessToken string `json:"access_token,omitempty"`
@@ -83,6 +86,13 @@ func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTrip
if !ok {
return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters)
}
// Validate the realm URL before storing it. A malicious or compromised
// registry can supply a realm pointing at an internal service or cloud
// metadata endpoint (e.g. 169.254.169.254), causing SSRF when the client
// subsequently fetches a token.
if err := validateRealmURL(realm, reg.RegistryStr(), pr.Insecure); err != nil {
return nil, fmt.Errorf("invalid realm in www-authenticate: %w", err)
}
service := pr.Parameters["service"]
scheme := "https"
if pr.Insecure {
@@ -99,6 +109,56 @@ func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTrip
}, nil
}
// realmRedirectCheck mimics the default http.Client redirect policy but also
// validates each redirect URL with validateRealmURL.
func realmRedirectCheck(registryHost string, insecure bool) func(*http.Request, []*http.Request) error {
return func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects")
}
if err := validateRealmURL(req.URL.String(), registryHost, insecure); err != nil {
return fmt.Errorf("refusing token-server redirect to %q: %w", req.URL, err)
}
return nil
}
}
// validateRealmURL returns an error if the realm URL uses a disallowed scheme
// or resolves to a private / link-local IP address. Realm URLs matching the
// registry host:port are always allowed. See #2258.
func validateRealmURL(realm, registryHost string, insecure bool) error {
u, err := url.Parse(realm)
if err != nil {
return fmt.Errorf("parsing realm %q: %w", realm, err)
}
switch u.Scheme {
case "https":
// always allowed
case "http":
if !insecure {
return fmt.Errorf("realm scheme %q not allowed for a secure registry; use https", u.Scheme)
}
default:
return fmt.Errorf("realm scheme %q not allowed; must be https (or http for insecure registries)", u.Scheme)
}
// Always allow realms matching the registry host:port.
if registryHost != "" && u.Host == registryHost {
return nil
}
// Reject IP literals that resolve to private or link-local ranges.
// This blocks direct references to RFC 1918 addresses, loopback, and
// link-local ranges including the cloud instance metadata service
// (169.254.169.254 / fd00:ec2::254). DNS-based SSRF is out of scope
// here; callers should apply network-level controls if needed.
host := u.Hostname()
if ip := net.ParseIP(host); ip != nil {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() || ip.IsUnspecified() {
return fmt.Errorf("realm host %q is a private or link-local address", host)
}
}
return nil
}
type bearerTransport struct {
mx sync.RWMutex
// Wrapped by bearerTransport.
@@ -336,7 +396,8 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
v.Set("access_type", "offline")
}
client := http.Client{Transport: bt.inner}
allowInsecure := bt.scheme == "http"
client := http.Client{Transport: bt.inner, CheckRedirect: realmRedirectCheck(bt.registry.RegistryStr(), allowInsecure)}
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode()))
if err != nil {
return nil, err
@@ -359,7 +420,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
return nil, err
}
return io.ReadAll(resp.Body)
return io.ReadAll(io.LimitReader(resp.Body, maxTokenBodySize))
}
// https://docs.docker.com/registry/spec/auth/token/
@@ -373,7 +434,8 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
auth: bt.basic,
target: u.Host,
}
client := http.Client{Transport: b}
allowInsecure := bt.scheme == "http"
client := http.Client{Transport: b, CheckRedirect: realmRedirectCheck(bt.registry.RegistryStr(), allowInsecure)}
v := u.Query()
bt.mx.RLock()
@@ -403,5 +465,5 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
return nil, err
}
return io.ReadAll(resp.Body)
return io.ReadAll(io.LimitReader(resp.Body, maxTokenBodySize))
}
@@ -15,6 +15,7 @@
package transport
import (
"bytes"
"encoding/json"
"fmt"
"io"
@@ -112,6 +113,10 @@ type ErrorCode string
// The set of error conditions a registry may return:
// https://github.com/distribution/distribution/blob/aac2f6c8b7c5a6c60190848bab5cbeed2b5ba0a9/docs/spec/api.md#errors-2
// maxErrorBodySize limits HTTP error response body reads to prevent OOM when
// a registry returns an unexpectedly large error body.
const maxErrorBodySize = 64 * 1024 // 64 KiB
const (
BlobUnknownErrorCode ErrorCode = "BLOB_UNKNOWN"
BlobUploadInvalidErrorCode ErrorCode = "BLOB_UPLOAD_INVALID"
@@ -146,6 +151,7 @@ var temporaryErrorCodes = map[ErrorCode]struct{}{
var temporaryStatusCodes = map[int]struct{}{
http.StatusRequestTimeout: {},
http.StatusTooManyRequests: {}, // matches TooManyRequestsErrorCode in temporaryErrorCodes
http.StatusInternalServerError: {},
http.StatusBadGateway: {},
http.StatusServiceUnavailable: {},
@@ -161,7 +167,7 @@ func CheckError(resp *http.Response, codes ...int) error {
}
}
b, err := io.ReadAll(resp.Body)
b, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize))
if err != nil {
return err
}
@@ -185,11 +191,16 @@ func makeError(resp *http.Response, body []byte) *Error {
}
func retryError(resp *http.Response) error {
b, err := io.ReadAll(resp.Body)
b, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize))
if err != nil {
return err
}
// Restore the body so that a subsequent CheckError call (after the
// retry loop exhausts its retries) can still read and parse the
// structured registry error from the response.
resp.Body = io.NopCloser(bytes.NewReader(b))
rerr := makeError(resp, b)
rerr.temporary = true
return rerr
@@ -24,9 +24,9 @@ import (
"strings"
"time"
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/internal/authchallenge"
)
// 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
@@ -18,7 +18,9 @@ package transport
const (
PullScope string = "pull"
PushScope string = "push,pull"
// For now DELETE is PUSH, which is the read/write ACL.
DeleteScope string = PushScope
// DeleteScope requests "delete" in addition to push/pull so that
// registries requiring an explicit delete action (e.g. IBM Cloud
// Container Registry) grant the necessary access.
DeleteScope string = "push,pull,delete"
CatalogScope string = "catalog"
)
+55 -8
View File
@@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sort"
@@ -66,6 +67,25 @@ type writer struct {
scopes []string
}
// makeDeleteClient returns an HTTP client whose token includes the "delete"
// action so that registries requiring an explicit delete permission grant
// access for manifest deletion.
func makeDeleteClient(ctx context.Context, repo name.Repository, o *options) (*http.Client, error) {
auth := o.auth
if o.keychain != nil {
kauth, err := authn.Resolve(ctx, o.keychain, repo)
if err != nil {
return nil, err
}
auth = kauth
}
tr, err := transport.NewWithContext(ctx, repo.Registry, auth, o.transport, []string{repo.Scope(transport.DeleteScope)})
if err != nil {
return nil, err
}
return &http.Client{Transport: tr}, nil
}
func makeWriter(ctx context.Context, repo name.Repository, ls []v1.Layer, o *options) (*writer, error) {
auth := o.auth
if o.keychain != nil {
@@ -148,7 +168,29 @@ func (w *writer) nextLocation(resp *http.Response) (string, error) {
// If the location header returned is just a url path, then fully qualify it.
// We cannot simply call w.url, since there might be an embedded query string.
return resp.Request.URL.ResolveReference(u).String(), nil
resolved := resp.Request.URL.ResolveReference(u)
// Reject Location headers that redirect to a DIFFERENT host that resolves to
// a private or link-local IP literal. A malicious or compromised registry can
// respond to a blob upload initiation (POST /v2/.../blobs/uploads/) with a
// crafted Location header pointing at an internal service, causing the client
// to send subsequent PATCH/PUT requests (including the layer data as the body)
// to that internal address. Pre-signed blob URLs from cloud storage providers
// (GCS, S3, Azure Blob) use public hostnames, so legitimate cross-host
// redirects are unaffected.
//
// Same-host redirects (e.g. a different path on the registry itself) are
// always allowed regardless of whether the registry IP is private.
origHost := resp.Request.URL.Hostname()
if destHost := resolved.Hostname(); destHost != origHost {
if ip := net.ParseIP(destHost); ip != nil {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() || ip.IsUnspecified() {
return "", fmt.Errorf("SSRF protection: Location header redirects to private/link-local host %q", destHost)
}
}
}
return resolved.String(), nil
}
// checkExistingBlob checks if a blob exists already in the repository by making a
@@ -555,9 +597,10 @@ func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Refere
return err
}
var mf struct {
MediaType types.MediaType `json:"mediaType"`
Subject *v1.Descriptor `json:"subject,omitempty"`
Config struct {
MediaType types.MediaType `json:"mediaType"`
Subject *v1.Descriptor `json:"subject,omitempty"`
ArtifactType string `json:"artifactType,omitempty"`
Config struct {
MediaType types.MediaType `json:"mediaType"`
} `json:"config"`
}
@@ -599,10 +642,14 @@ func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Refere
return err
}
desc := v1.Descriptor{
ArtifactType: string(mf.Config.MediaType),
MediaType: mf.MediaType,
Digest: h,
Size: size,
MediaType: mf.MediaType,
Digest: h,
Size: size,
}
if mf.ArtifactType != "" {
desc.ArtifactType = mf.ArtifactType
} else {
desc.ArtifactType = string(mf.Config.MediaType)
}
if err := w.commitSubjectReferrers(ctx,
ref.Context().Digest(mf.Subject.Digest.String()),