From 4a904ddbe95a2ab169aa968337b8eccf7c2530ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:09:26 +0000 Subject: [PATCH] chore(deps): bump github.com/google/go-containerregistry Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.21.6 to 0.21.7. - [Release notes](https://github.com/google/go-containerregistry/releases) - [Commits](https://github.com/google/go-containerregistry/compare/v0.21.6...v0.21.7) --- updated-dependencies: - dependency-name: github.com/google/go-containerregistry dependency-version: 0.21.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +- go.sum | 16 ++--- .../go-containerregistry/internal/gzip/zip.go | 18 +++-- .../internal/limit/limit.go | 36 ++++++++++ .../go-containerregistry/pkg/name/registry.go | 4 +- .../pkg/name/repository.go | 9 +-- .../pkg/registry/blobs.go | 38 +++++----- .../pkg/registry/blobs_disk.go | 7 +- .../pkg/v1/google/list.go | 37 +++++++++- .../pkg/v1/layout/blob.go | 46 +++++++++++- .../pkg/v1/mutate/mutate.go | 15 ++++ .../pkg/v1/remote/catalog.go | 2 +- .../pkg/v1/remote/fetcher.go | 3 +- .../pkg/v1/remote/list.go | 70 ++++++++++++++++++- .../pkg/v1/remote/referrers.go | 4 +- .../pkg/v1/remote/transport/bearer.go | 24 +++++-- .../pkg/v1/remote/transport/error.go | 5 +- .../pkg/v1/tarball/image.go | 5 +- .../pkg/v1/tarball/write.go | 28 +++++--- vendor/golang.org/x/sync/errgroup/errgroup.go | 2 +- .../golang.org/x/sync/semaphore/semaphore.go | 10 +-- .../x/sync/singleflight/singleflight.go | 8 +-- vendor/modules.txt | 7 +- 23 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 vendor/github.com/google/go-containerregistry/internal/limit/limit.go diff --git a/go.mod b/go.mod index a91d0d8655..6697d96955 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fatih/color v1.19.0 github.com/go-jose/go-jose/v4 v4.1.4 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.21.6 + github.com/google/go-containerregistry v0.21.7 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.2 @@ -294,10 +294,10 @@ require ( gocloud.dev/docstore/mongodocstore v0.43.0 // indirect gocloud.dev/pubsub/kafkapubsub v0.43.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/go.sum b/go.sum index 35939638ef..362d4cbb52 100644 --- a/go.sum +++ b/go.sum @@ -354,8 +354,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= -github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20260414223304-7a662782a11f h1:31syNEUHTz7kt9AmH9SyXgV+Y6LMd62ZjaEcPWCy9z0= github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20260414223304-7a662782a11f/go.mod h1:iMEl9wsO8BHbAcfXcIFCN03G1odxdJXkdAaskB4pHxg= github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f h1:GJRzEBoJv/A/E7JbTekq1Q0jFtAfY7TIxUFAK89Mmic= @@ -871,8 +871,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -906,8 +906,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -972,8 +972,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go b/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go index 018c0f8c0f..98100c0246 100644 --- a/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go +++ b/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go @@ -51,33 +51,31 @@ func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser { // Returns err so we can pw.CloseWithError(err) go func() error { - // TODO(go1.14): Just defer {pw,gw,r}.Close like you'd expect. - // Context: https://golang.org/issue/24283 + // Always close the source reader when the goroutine exits, + // regardless of which error path is taken. This prevents + // resource leaks (e.g. pullLimiter token slots held by + // limitedReadCloser wrappers around r). + defer r.Close() + gw, err := gzip.NewWriterLevel(bw, level) if err != nil { return pw.CloseWithError(err) } + defer gw.Close() + defer pw.Close() if _, err := io.Copy(gw, r); err != nil { - defer r.Close() - defer gw.Close() return pw.CloseWithError(err) } - // Close gzip writer to Flush it and write gzip trailers. if err := gw.Close(); err != nil { return pw.CloseWithError(err) } - // Flush bufio writer to ensure we write out everything. if err := bw.Flush(); err != nil { return pw.CloseWithError(err) } - // We don't really care if these fail. - defer pw.Close() - defer r.Close() - return nil }() diff --git a/vendor/github.com/google/go-containerregistry/internal/limit/limit.go b/vendor/github.com/google/go-containerregistry/internal/limit/limit.go new file mode 100644 index 0000000000..29d2b08ace --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/internal/limit/limit.go @@ -0,0 +1,36 @@ +// 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 limit provides bounded reads from io.Readers. +package limit + +import ( + "fmt" + "io" +) + +// ReadAll reads from r until EOF and returns the data. If r produces more +// than max bytes, it returns an error instead of a silently truncated +// slice. Use this in preference to io.ReadAll(io.LimitReader(r, max)) +// when callers must distinguish a complete read from a truncated one. +func ReadAll(r io.Reader, max int64) ([]byte, error) { + b, err := io.ReadAll(io.LimitReader(r, max+1)) + if err != nil { + return nil, err + } + if int64(len(b)) > max { + return nil, fmt.Errorf("body exceeds %d byte limit", max) + } + return b, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/name/registry.go b/vendor/github.com/google/go-containerregistry/pkg/name/registry.go index 7531d2426b..478021486c 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/name/registry.go +++ b/vendor/github.com/google/go-containerregistry/pkg/name/registry.go @@ -28,10 +28,10 @@ import ( var reLocal = regexp.MustCompile(`.*\.localhost(?::\d{1,5})?$`) // Detect the loopback IP (127.0.0.1) -var reLoopback = regexp.MustCompile(regexp.QuoteMeta("127.0.0.1")) +var reLoopback = regexp.MustCompile(`^127\.0\.0\.1(?::\d{1,5})?$`) // Detect the loopback IPV6 (::1) -var reipv6Loopback = regexp.MustCompile(regexp.QuoteMeta("::1")) +var reipv6Loopback = regexp.MustCompile(`^(::1|\[::1\](?::\d{1,5})?)$`) // Registry stores a docker registry name in a structured form. type Registry struct { diff --git a/vendor/github.com/google/go-containerregistry/pkg/name/repository.go b/vendor/github.com/google/go-containerregistry/pkg/name/repository.go index 290797575e..efde6e869d 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/name/repository.go +++ b/vendor/github.com/google/go-containerregistry/pkg/name/repository.go @@ -85,11 +85,12 @@ func NewRepository(name string, opts ...Option) (Repository, error) { var registry string repo := name parts := strings.SplitN(name, regRepoDelimiter, 2) - if len(parts) == 2 && (strings.ContainsRune(parts[0], '.') || strings.ContainsRune(parts[0], ':')) { + maybeRegistry := parts[0] + if len(parts) == 2 && (maybeRegistry == "localhost" || strings.ContainsAny(maybeRegistry, ".:")) { // The first part of the repository is treated as the registry domain - // iff it contains a '.' or ':' character, otherwise it is all repository - // and the domain defaults to Docker Hub. - registry = parts[0] + // if it is localhost or contains a '.' or ':' character, otherwise it + // is all repository and the domain defaults to Docker Hub. + registry = maybeRegistry repo = parts[1] } diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go index 8aaa86fb3a..5b666ddbf2 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go @@ -59,7 +59,7 @@ type BlobHandler interface { // backend that can serve metadata about blobs. type BlobStatHandler interface { // Stat returns the size of the blob, or errNotFound if the blob wasn't - // found, or redirectError if the blob can be found elsewhere. + // found, or RedirectError if the blob can be found elsewhere. Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) } @@ -82,10 +82,10 @@ type BlobDeleteHandler interface { Delete(ctx context.Context, repo string, h v1.Hash) error } -// redirectError represents a signal that the blob handler doesn't have the blob +// RedirectError represents a signal that the blob handler doesn't have the blob // contents, but that those contents are at another location which registry // clients should redirect to. -type redirectError struct { +type RedirectError struct { // Location is the location to find the contents. Location string @@ -101,10 +101,10 @@ func (r *bytesCloser) Close() error { return nil } -func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } +func (e RedirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } -// errNotFound represents an error locating the blob. -var errNotFound = errors.New("not found") +// ErrNotFound represents an error locating the blob. +var ErrNotFound = errors.New("not found") type memHandler struct { m map[string][]byte @@ -119,7 +119,7 @@ func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) b, found := m.m[h.String()] if !found { - return 0, errNotFound + return 0, ErrNotFound } return int64(len(b)), nil } @@ -130,7 +130,7 @@ func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, b, found := m.m[h.String()] if !found { - return nil, errNotFound + return nil, ErrNotFound } return &bytesCloser{bytes.NewReader(b)}, nil } @@ -153,7 +153,7 @@ func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error { defer m.lock.Unlock() if _, found := m.m[h.String()]; !found { - return errNotFound + return ErrNotFound } delete(m.m, h.String()) @@ -206,10 +206,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { var size int64 if bsh, ok := b.blobHandler.(BlobStatHandler); ok { size, err = bsh.Stat(req.Context(), repo, h) - if errors.Is(err, errNotFound) { + if errors.Is(err, ErrNotFound) { return regErrBlobUnknown } else if err != nil { - var rerr redirectError + var rerr RedirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil @@ -218,10 +218,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } } else { rc, err := b.blobHandler.Get(req.Context(), repo, h) - if errors.Is(err, errNotFound) { + if errors.Is(err, ErrNotFound) { return regErrBlobUnknown } else if err != nil { - var rerr redirectError + var rerr RedirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil @@ -254,10 +254,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { var r io.Reader if bsh, ok := b.blobHandler.(BlobStatHandler); ok { size, err = bsh.Stat(req.Context(), repo, h) - if errors.Is(err, errNotFound) { + if errors.Is(err, ErrNotFound) { return regErrBlobUnknown } else if err != nil { - var rerr redirectError + var rerr RedirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil @@ -266,10 +266,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } rc, err := b.blobHandler.Get(req.Context(), repo, h) - if errors.Is(err, errNotFound) { + if errors.Is(err, ErrNotFound) { return regErrBlobUnknown } else if err != nil { - var rerr redirectError + var rerr RedirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil @@ -283,10 +283,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } else { tmp, err := b.blobHandler.Get(req.Context(), repo, h) - if errors.Is(err, errNotFound) { + if errors.Is(err, ErrNotFound) { return regErrBlobUnknown } else if err != nil { - var rerr redirectError + var rerr RedirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs_disk.go b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs_disk.go index 331aade7f1..fe1c7c7897 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs_disk.go +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs_disk.go @@ -38,7 +38,7 @@ func (m *diskHandler) blobHashPath(h v1.Hash) string { func (m *diskHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) { f, err := os.Open(m.blobHashPath(h)) if errors.Is(err, os.ErrNotExist) { - return 0, errNotFound + return 0, ErrNotFound } else if err != nil { return 0, err } @@ -49,13 +49,15 @@ func (m *diskHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error return 0, err } if got != h { - return 0, fmt.Errorf("%w: blob %s has digest %s", errNotFound, h, got) + return 0, fmt.Errorf("%w: blob %s has digest %s", ErrNotFound, h, got) } return size, nil } + func (m *diskHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) { return os.Open(m.blobHashPath(h)) } + func (m *diskHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error { // Put the temp file in the same directory to avoid cross-device problems // during the os.Rename. The filenames cannot conflict. @@ -76,6 +78,7 @@ func (m *diskHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClos } return os.Rename(f.Name(), m.blobHashPath(h)) } + func (m *diskHandler) Delete(_ context.Context, _ string, h v1.Hash) error { return os.Remove(m.blobHashPath(h)) } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go index cfb7143472..2b86b004e1 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go @@ -17,6 +17,7 @@ package google import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -143,7 +144,7 @@ func (l *lister) list(repo name.Repository) (*Tags, error) { // This isn't GCR, just append the tags and keep paginating. tags.Tags = append(tags.Tags, parsed.Tags...) - uri, err = getNextPageURL(resp) + uri, err = getNextPageURL(resp, repo) if err != nil { return nil, err } @@ -159,8 +160,11 @@ func (l *lister) list(repo name.Repository) (*Tags, error) { // getNextPageURL checks if there is a Link header in a http.Response which // contains a link to the next page. If yes it returns the url.URL of the next -// page otherwise it returns nil. -func getNextPageURL(resp *http.Response) (*url.URL, error) { +// page otherwise it returns nil. It validates that the resolved URL points +// to the same registry to prevent SSRF attacks where a malicious registry +// could redirect pagination requests to internal services like cloud metadata +// endpoints. +func getNextPageURL(resp *http.Response, repo name.Repository) (*url.URL, error) { link := resp.Header.Get("Link") if link == "" { return nil, nil @@ -175,6 +179,9 @@ func getNextPageURL(resp *http.Response) (*url.URL, error) { return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link) } link = link[1:end] + if link == "" { + return nil, nil + } linkURL, err := url.Parse(link) if err != nil { @@ -184,9 +191,33 @@ func getNextPageURL(resp *http.Response) (*url.URL, error) { return nil, nil } linkURL = resp.Request.URL.ResolveReference(linkURL) + + // Validate that the pagination URL points to the same registry to prevent SSRF. + if err := validatePaginationURL(linkURL, repo); err != nil { + return nil, err + } + return linkURL, nil } +// validatePaginationURL checks that a pagination URL is safe to follow. +// It ensures the URL points to the same registry host to prevent SSRF attacks +// where a malicious registry could include a Link header pointing to internal +// services (e.g., cloud metadata endpoints at 169.254.169.254). +func validatePaginationURL(u *url.URL, repo name.Repository) error { + // Must use same scheme as the registry + if u.Scheme != repo.Scheme() { + return fmt.Errorf("pagination URL scheme %q does not match registry scheme %q", u.Scheme, repo.Scheme()) + } + + // Must point to the same registry host + if u.Host != repo.RegistryStr() { + return errors.New("pagination URL host does not match registry host: potential SSRF attack") + } + + return nil +} + type rawManifestInfo struct { Size string `json:"imageSizeBytes"` MediaType string `json:"mediaType"` diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go index 2e5f4358dd..48d5022392 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go @@ -15,6 +15,7 @@ package layout import ( + "fmt" "io" "os" @@ -23,15 +24,56 @@ import ( // Blob returns a blob with the given hash from the Path. func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) { - return os.Open(l.blobPath(h)) + return l.openBlob(h) } // Bytes is a convenience function to return a blob from the Path as // a byte slice. func (l Path) Bytes(h v1.Hash) ([]byte, error) { - return os.ReadFile(l.blobPath(h)) + f, err := l.openBlob(h) + if err != nil { + return nil, err + } + defer f.Close() + return io.ReadAll(f) } func (l Path) blobPath(h v1.Hash) string { return l.path("blobs", h.Algorithm, h.Hex) } + +func (l Path) openBlob(h v1.Hash) (*os.File, error) { + p := l.blobPath(h) + info, err := os.Lstat(p) + if err != nil { + return nil, err + } + if info.Mode()&os.ModeSymlink != 0 { + return nil, fmt.Errorf("layout blob %s is a symlink", h) + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("layout blob %s is not a regular file", h) + } + f, err := os.Open(p) + if err != nil { + return nil, err + } + closeFile := true + defer func() { + if closeFile { + f.Close() + } + }() + stat, err := f.Stat() + if err != nil { + return nil, err + } + if !stat.Mode().IsRegular() { + return nil, fmt.Errorf("layout blob %s is not a regular file", h) + } + if !os.SameFile(info, stat) { + return nil, fmt.Errorf("layout blob %s changed while opening", h) + } + closeFile = false + return f, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go index a6186b3756..09458e96cb 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go @@ -365,6 +365,15 @@ func extractLayer(tarWriter *tar.Writer, fileMap map[string]bool, layer v1.Layer } } } + + // Drain any bytes the tar.Reader did not consume (trailing data after the + // end-of-archive marker) so the underlying verifying reader reaches io.EOF + // and the layer's digest is verified. Without this, a layer whose contents + // do not match the manifest's layer digest is extracted without error. + // pkg/v1/validate/layer.go performs the same drain. + if _, err := io.Copy(io.Discard, layerReader); err != nil { + return fmt.Errorf("verifying layer: %w", err) + } return nil } @@ -501,6 +510,12 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) { } } + // Drain trailing bytes so the underlying verifying reader reaches io.EOF + // and the layer digest is verified (see extractLayer). + if _, err := io.Copy(io.Discard, layerReader); err != nil { + return nil, fmt.Errorf("verifying layer: %w", err) + } + if err := tarWriter.Close(); err != nil { return nil, err } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/catalog.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/catalog.go index a0281b9fd2..7b9cc0e247 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/catalog.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/catalog.go @@ -122,7 +122,7 @@ func (f *fetcher) catalogPage(ctx context.Context, reg name.Registry, next strin return nil, err } - uri, err := getNextPageURL(resp) + uri, err := getNextPageURLForRegistry(resp, reg) if err != nil { return nil, err } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/fetcher.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/fetcher.go index 3bda1d09c4..4b238d1290 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/fetcher.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/fetcher.go @@ -24,6 +24,7 @@ import ( "net/url" "strings" + "github.com/google/go-containerregistry/internal/limit" "github.com/google/go-containerregistry/internal/redact" "github.com/google/go-containerregistry/internal/verify" "github.com/google/go-containerregistry/pkg/authn" @@ -168,7 +169,7 @@ func (f *fetcher) fetchManifest(ctx context.Context, ref name.Reference, accepta return nil, nil, err } - manifest, err := io.ReadAll(io.LimitReader(resp.Body, manifestLimit)) + manifest, err := limit.ReadAll(resp.Body, manifestLimit) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/list.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/list.go index 910d2a94c7..9beff7dc24 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/list.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/list.go @@ -17,6 +17,7 @@ package remote import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -85,7 +86,7 @@ func (f *fetcher) listPage(ctx context.Context, repo name.Repository, next strin return nil, err } - uri, err := getNextPageURL(resp) + uri, err := getNextPageURL(resp, repo) if err != nil { return nil, err } @@ -99,8 +100,9 @@ func (f *fetcher) listPage(ctx context.Context, repo name.Repository, next strin // getNextPageURL checks if there is a Link header in a http.Response which // contains a link to the next page. If yes it returns the url.URL of the next -// page otherwise it returns nil. -func getNextPageURL(resp *http.Response) (*url.URL, error) { +// page otherwise it returns nil. It validates that the resolved URL points +// to the same registry to prevent SSRF attacks. +func getNextPageURL(resp *http.Response, repo name.Repository) (*url.URL, error) { link := resp.Header.Get("Link") if link == "" { return nil, nil @@ -115,6 +117,9 @@ func getNextPageURL(resp *http.Response) (*url.URL, error) { return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link) } link = link[1:end] + if link == "" { + return nil, nil + } linkURL, err := url.Parse(link) if err != nil { @@ -124,9 +129,31 @@ func getNextPageURL(resp *http.Response) (*url.URL, error) { return nil, nil } linkURL = resp.Request.URL.ResolveReference(linkURL) + + // Validate that the pagination URL points to the same registry to prevent SSRF. + if err := validatePaginationURL(linkURL, repo); err != nil { + return nil, err + } + return linkURL, nil } +// validatePaginationURL checks that a pagination URL is safe to follow. +func validatePaginationURL(u *url.URL, repo name.Repository) error { + return validatePaginationURLHost(u, repo.Scheme(), repo.RegistryStr()) +} + +// validatePaginationURLHost checks that a pagination URL is safe to follow. +func validatePaginationURLHost(u *url.URL, scheme, host string) error { + if u.Scheme != scheme { + return fmt.Errorf("pagination URL scheme %q does not match registry scheme %q", u.Scheme, scheme) + } + if u.Host != host { + return errors.New("pagination URL host does not match registry host: potential SSRF attack") + } + return nil +} + type Lister struct { f *fetcher repo name.Repository @@ -150,3 +177,40 @@ func (l *Lister) Next(ctx context.Context) (*Tags, error) { func (l *Lister) HasNext() bool { return l.page != nil && (!l.needMore || l.page.Next != "") } + +// getNextPageURLForRegistry is like getNextPageURL but for name.Registry. +func getNextPageURLForRegistry(resp *http.Response, reg name.Registry) (*url.URL, error) { + link := resp.Header.Get("Link") + if link == "" { + return nil, nil + } + + if link[0] != '<' { + return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link) + } + + end := strings.Index(link, ">") + if end == -1 { + return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link) + } + link = link[1:end] + if link == "" { + return nil, nil + } + + linkURL, err := url.Parse(link) + if err != nil { + return nil, err + } + if resp.Request == nil || resp.Request.URL == nil { + return nil, nil + } + linkURL = resp.Request.URL.ResolveReference(linkURL) + + // Validate that the pagination URL points to the same registry to prevent SSRF. + if err := validatePaginationURLHost(linkURL, reg.Scheme(), reg.RegistryStr()); err != nil { + return nil, err + } + + return linkURL, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/referrers.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/referrers.go index 17e9c26ed0..c23e1d83b6 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/referrers.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/referrers.go @@ -18,10 +18,10 @@ import ( "bytes" "context" "errors" - "io" "net/http" "strings" + "github.com/google/go-containerregistry/internal/limit" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" @@ -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(io.LimitReader(resp.Body, manifestLimit)) + b, err = limit.ReadAll(resp.Body, manifestLimit) if err != nil { return nil, err } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go index f845dd1fba..cbc2b1d4f9 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go @@ -19,13 +19,13 @@ import ( "encoding/json" "errors" "fmt" - "io" "net" "net/http" "net/url" "strings" "sync" + "github.com/google/go-containerregistry/internal/limit" "github.com/google/go-containerregistry/internal/redact" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/logs" @@ -247,7 +247,23 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) { if err = bt.refresh(in.Context()); err != nil { return nil, err } - return sendRequest() + // Re-attach the freshly fetched token, but only when the request is + // still talking to the registry we authenticated against. matchesHost + // guards against forwarding the Authorization header across an + // http.Client-level redirect to a different host: a malicious or + // compromised registry can 302 the request to an attacker-controlled + // host, answer the follow-up with a Bearer challenge, and harvest the + // token if we re-attach it unconditionally. For a cross-host request + // fall back to sendRequest(), which omits the credential, rather than + // leaking it to a host we never logged in to. + if !matchesHost(bt.registry.RegistryStr(), in, bt.scheme) { + return sendRequest() + } + bt.mx.RLock() + tok := bt.bearer.RegistryToken + bt.mx.RUnlock() + in.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tok)) + return bt.inner.RoundTrip(in) } return res, err @@ -420,7 +436,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) { return nil, err } - return io.ReadAll(io.LimitReader(resp.Body, maxTokenBodySize)) + return limit.ReadAll(resp.Body, maxTokenBodySize) } // https://docs.docker.com/registry/spec/auth/token/ @@ -465,5 +481,5 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) { return nil, err } - return io.ReadAll(io.LimitReader(resp.Body, maxTokenBodySize)) + return limit.ReadAll(resp.Body, maxTokenBodySize) } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go index 934484db8d..7b14b8c413 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go @@ -22,6 +22,7 @@ import ( "net/http" "strings" + "github.com/google/go-containerregistry/internal/limit" "github.com/google/go-containerregistry/internal/redact" ) @@ -167,7 +168,7 @@ func CheckError(resp *http.Response, codes ...int) error { } } - b, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize)) + b, err := limit.ReadAll(resp.Body, maxErrorBodySize) if err != nil { return err } @@ -191,7 +192,7 @@ func makeError(resp *http.Response, body []byte) *Error { } func retryError(resp *http.Response) error { - b, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize)) + b, err := limit.ReadAll(resp.Body, maxErrorBodySize) if err != nil { return err } diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go index 96c0cce49d..9b0350b121 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go @@ -248,7 +248,7 @@ func followLinks(opener Opener, filePath string, visited map[string]bool) (io.Re if err != nil { return nil, err } - if hdr.Name == filePath { + if path.Clean(hdr.Name) == path.Clean(filePath) { if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink { currentDir := filepath.Dir(filePath) return followLinks(opener, path.Join(currentDir, path.Clean(hdr.Linkname)), visited) @@ -374,6 +374,9 @@ func (c *compressedImage) Manifest() (*v1.Manifest, error) { if err != nil { return nil, err } + if i >= len(cfg.RootFS.DiffIDs) { + return nil, fmt.Errorf("tarball manifest references %d layer(s) but config has %d rootfs.diff_ids; the config may not describe a runnable image (for example, a buildkit cacheconfig)", len(c.imgDescriptor.Layers), len(cfg.RootFS.DiffIDs)) + } diffid := cfg.RootFS.DiffIDs[i] if d, ok := c.imgDescriptor.LayerSources[diffid]; ok { // If it's a foreign layer, just append the descriptor so we can avoid diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/write.go index e607df164a..062268a5b9 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/write.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/write.go @@ -191,16 +191,7 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w } seenLayerDigests[hex] = struct{}{} - r, err := l.Compressed() - if err != nil { - return sendProgressWriterReturn(pw, err) - } - blobSize, err := l.Size() - if err != nil { - return sendProgressWriterReturn(pw, err) - } - - if err := writeTarEntry(tf, layerFiles[i], r, blobSize); err != nil { + if err := writeLayer(tf, layerFiles[i], l); err != nil { return sendProgressWriterReturn(pw, err) } } @@ -380,6 +371,23 @@ func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error { return err } +// writeLayer streams a layer's compressed blob into the tar writer and closes +// the reader before returning. The close releases any pull-limiter slot held +// by a remote-backed layer (remote.WithJobs); leaving it open would deadlock +// the write loop after defaultJobs layers. +func writeLayer(tf *tar.Writer, name string, l v1.Layer) error { + r, err := l.Compressed() + if err != nil { + return err + } + defer r.Close() + blobSize, err := l.Size() + if err != nil { + return err + } + return writeTarEntry(tf, name, r, blobSize) +} + // ComputeManifest get the manifest.json that will be written to the tarball // for multiple references func ComputeManifest(refToImage map[name.Reference]v1.Image) (Manifest, error) { diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index f69fd75468..c261a8ebbd 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -109,7 +109,7 @@ func (g *Group) TryGo(f func() error) bool { if g.sem != nil { select { case g.sem <- token{}: - // Note: this allows barging iff channels in general allow barging. + // Note: this allows barging if and only if channels in general allow barging. default: return false } diff --git a/vendor/golang.org/x/sync/semaphore/semaphore.go b/vendor/golang.org/x/sync/semaphore/semaphore.go index b618162aab..040c5bc509 100644 --- a/vendor/golang.org/x/sync/semaphore/semaphore.go +++ b/vendor/golang.org/x/sync/semaphore/semaphore.go @@ -83,7 +83,7 @@ func (s *Weighted) Acquire(ctx context.Context, n int64) error { default: isFront := s.waiters.Front() == elem s.waiters.Remove(elem) - // If we're at the front and there're extra tokens left, notify other waiters. + // If we're at the front and there are extra tokens left, notify other waiters. if isFront && s.size > s.cur { s.notifyWaiters() } @@ -139,15 +139,15 @@ func (s *Weighted) notifyWaiters() { w := next.Value.(waiter) if s.size-s.cur < w.n { - // Not enough tokens for the next waiter. We could keep going (to try to + // Not enough tokens for the next waiter. We could keep going (to try to // find a waiter with a smaller request), but under load that could cause // starvation for large requests; instead, we leave all remaining waiters // blocked. // // Consider a semaphore used as a read-write lock, with N tokens, N - // readers, and one writer. Each reader can Acquire(1) to obtain a read - // lock. The writer can Acquire(N) to obtain a write lock, excluding all - // of the readers. If we allow the readers to jump ahead in the queue, + // readers, and one writer. Each reader can Acquire(1) to obtain a read + // lock. The writer can Acquire(N) to obtain a write lock, excluding all + // of the readers. If we allow the readers to jump ahead in the queue, // the writer will starve — there is always one token available for every // reader. break diff --git a/vendor/golang.org/x/sync/singleflight/singleflight.go b/vendor/golang.org/x/sync/singleflight/singleflight.go index 90ca138af3..32e40bdc87 100644 --- a/vendor/golang.org/x/sync/singleflight/singleflight.go +++ b/vendor/golang.org/x/sync/singleflight/singleflight.go @@ -15,12 +15,12 @@ import ( "sync" ) -// errGoexit indicates the runtime.Goexit was called in -// the user given function. +// errGoexit indicates runtime.Goexit was called in +// the user-given function. var errGoexit = errors.New("runtime.Goexit was called") // A panicError is an arbitrary value recovered from a panic -// with the stack trace during the execution of given function. +// with the stack trace during the execution of the given function. type panicError struct { value any stack []byte @@ -204,7 +204,7 @@ func (g *Group) doCall(c *call, key string, fn func() (any, error)) { } } -// Forget tells the singleflight to forget about a key. Future calls +// Forget tells the singleflight to forget about a key. Future calls // to Do for this key will call the function rather than waiting for // an earlier call to complete. func (g *Group) Forget(key string) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 352b39c995..b6575a5bde 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -733,12 +733,13 @@ github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function github.com/google/go-cmp/cmp/internal/value -# github.com/google/go-containerregistry v0.21.6 +# github.com/google/go-containerregistry v0.21.7 ## explicit; go 1.25.0 github.com/google/go-containerregistry/internal/and github.com/google/go-containerregistry/internal/compression github.com/google/go-containerregistry/internal/gzip github.com/google/go-containerregistry/internal/httptest +github.com/google/go-containerregistry/internal/limit github.com/google/go-containerregistry/internal/redact github.com/google/go-containerregistry/internal/retry github.com/google/go-containerregistry/internal/retry/wait @@ -1822,7 +1823,7 @@ golang.org/x/crypto/ssh/terminal ## explicit; go 1.23.0 golang.org/x/exp/maps golang.org/x/exp/slices -# golang.org/x/mod v0.36.0 +# golang.org/x/mod v0.37.0 ## explicit; go 1.25.0 golang.org/x/mod/semver golang.org/x/mod/sumdb/dirhash @@ -1852,7 +1853,7 @@ golang.org/x/oauth2/google/internal/stsexchange golang.org/x/oauth2/internal golang.org/x/oauth2/jws golang.org/x/oauth2/jwt -# golang.org/x/sync v0.20.0 +# golang.org/x/sync v0.21.0 ## explicit; go 1.25.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore