diff --git a/go.mod b/go.mod index 5652886e26..ead9311b79 100644 --- a/go.mod +++ b/go.mod @@ -179,7 +179,7 @@ require ( golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect @@ -205,7 +205,7 @@ require ( k8s.io/metrics v0.36.2 // indirect k8s.io/streaming v0.36.2 // indirect k8s.io/utils v0.0.0-20260617174310-a95e086a2553 // indirect - oras.land/oras-go/v2 v2.6.1 // indirect + oras.land/oras-go/v2 v2.6.2 // indirect sigs.k8s.io/controller-runtime v0.24.1 // indirect sigs.k8s.io/gateway-api v1.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index d25a9f0dbd..a3f0ebf8f3 100644 --- a/go.sum +++ b/go.sum @@ -556,8 +556,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -662,8 +662,8 @@ k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98= k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= k8s.io/utils v0.0.0-20260617174310-a95e086a2553 h1:hmGqDecjc8d7HVzWzRFl0QD9bYuYKbBEG7t8xwnVxfI= k8s.io/utils v0.0.0-20260617174310-a95e086a2553/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= -oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= +oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/gateway-api v1.6.0 h1:735YBRj5NXFrOGX0GoSjwzUIzbz8kiEOfADsqHFmHgE= diff --git a/vendor/golang.org/x/sync/semaphore/semaphore.go b/vendor/golang.org/x/sync/semaphore/semaphore.go index 040c5bc509..96a035aede 100644 --- a/vendor/golang.org/x/sync/semaphore/semaphore.go +++ b/vendor/golang.org/x/sync/semaphore/semaphore.go @@ -24,7 +24,7 @@ func NewWeighted(n int64) *Weighted { } // Weighted provides a way to bound concurrent access to a resource. -// The callers can request access with a given weight. +// The callers can request access with a given non-negative weight. type Weighted struct { size int64 cur int64 @@ -32,10 +32,13 @@ type Weighted struct { waiters list.List } -// Acquire acquires the semaphore with a weight of n, blocking until resources +// Acquire acquires the semaphore with a non-negative weight of n, blocking until resources // are available or ctx is done. On success, returns nil. On failure, returns // ctx.Err() and leaves the semaphore unchanged. func (s *Weighted) Acquire(ctx context.Context, n int64) error { + if n < 0 { + panic("semaphore: n < 0") + } done := ctx.Done() s.mu.Lock() @@ -106,9 +109,12 @@ func (s *Weighted) Acquire(ctx context.Context, n int64) error { } } -// TryAcquire acquires the semaphore with a weight of n without blocking. +// TryAcquire acquires the semaphore with a non-negative weight of n without blocking. // On success, returns true. On failure, returns false and leaves the semaphore unchanged. func (s *Weighted) TryAcquire(n int64) bool { + if n < 0 { + panic("semaphore: n < 0") + } s.mu.Lock() success := s.size-s.cur >= n && s.waiters.Len() == 0 if success { @@ -118,8 +124,11 @@ func (s *Weighted) TryAcquire(n int64) bool { return success } -// Release releases the semaphore with a weight of n. +// Release releases the semaphore with a non-negative weight of n. func (s *Weighted) Release(n int64) { + if n < 0 { + panic("semaphore: n < 0") + } s.mu.Lock() s.cur -= n if s.cur < 0 { diff --git a/vendor/modules.txt b/vendor/modules.txt index 2ee752725f..742812cb10 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1311,7 +1311,7 @@ golang.org/x/net/websocket ## explicit; go 1.25.0 golang.org/x/oauth2 golang.org/x/oauth2/internal -# golang.org/x/sync v0.21.0 +# golang.org/x/sync v0.22.0 ## explicit; go 1.25.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore @@ -1968,7 +1968,7 @@ k8s.io/utils/internal/third_party/forked/golang/net k8s.io/utils/net k8s.io/utils/ptr k8s.io/utils/trace -# oras.land/oras-go/v2 v2.6.1 +# oras.land/oras-go/v2 v2.6.2 ## explicit; go 1.25.0 oras.land/oras-go/v2 oras.land/oras-go/v2/content diff --git a/vendor/oras.land/oras-go/v2/content/reader.go b/vendor/oras.land/oras-go/v2/content/reader.go index def9ebde82..9bf852403f 100644 --- a/vendor/oras.land/oras-go/v2/content/reader.go +++ b/vendor/oras.land/oras-go/v2/content/reader.go @@ -16,6 +16,7 @@ limitations under the License. package content import ( + "bytes" "errors" "fmt" "io" @@ -24,11 +25,15 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// maxDescriptorSize is the upper-bound for descriptor sizes accepted by -// ReadAll. Descriptors sourced from attacker-supplied OCI layouts can carry -// arbitrarily large Size values; without this cap, make([]byte, desc.Size) -// triggers a runtime panic before any allocation occurs. -const maxDescriptorSize = 32 * 1024 * 1024 // 32 MiB +// maxInitialBufferSize bounds the buffer that ReadAll pre-allocates from +// desc.Size before any content is read. desc.Size is attacker-controllable: a +// crafted OCI layout index.json can declare an arbitrarily large Size (e.g. +// 2^62), and make([]byte, desc.Size) on such a value triggers a runtime panic +// ("makeslice: len out of range") before any allocation occurs. ReadAll caps +// the initial allocation at this value and grows the buffer as it reads, so the +// declared size is never trusted for allocation while legitimately large +// content (e.g. plugin or chart layers) is still read in full. +const maxInitialBufferSize = 32 * 1024 * 1024 // 32 MiB var ( // ErrInvalidDescriptorSize is returned by ReadAll() when @@ -125,22 +130,32 @@ func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { // The read content is verified against the size and the digest // using a VerifyReader. func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) { - if desc.Size < 0 || desc.Size > maxDescriptorSize { + if desc.Size < 0 { return nil, ErrInvalidDescriptorSize } - buf := make([]byte, desc.Size) vr := NewVerifyReader(r, desc) - if n, err := io.ReadFull(vr, buf); err != nil { + + // Do not pre-allocate desc.Size directly: it is attacker-controllable and a + // forged value (e.g. 2^62) would panic make(). Cap the initial allocation + // and let the buffer grow as content is read. The VerifyReader enforces the + // declared size and digest, so a size that does not match the actual content + // still fails verification rather than over-allocating. + initialCap := desc.Size + if initialCap > maxInitialBufferSize { + initialCap = maxInitialBufferSize + } + buf := bytes.NewBuffer(make([]byte, 0, initialCap)) + if _, err := buf.ReadFrom(vr); err != nil { if errors.Is(err, io.ErrUnexpectedEOF) { - return nil, fmt.Errorf("read failed: expected content size of %d, got %d, for digest %s: %w", desc.Size, n, desc.Digest.String(), err) + return nil, fmt.Errorf("read failed: expected content size of %d, got %d, for digest %s: %w", desc.Size, buf.Len(), desc.Digest.String(), err) } return nil, fmt.Errorf("read failed: %w", err) } if err := vr.Verify(); err != nil { return nil, err } - return buf, nil + return buf.Bytes(), nil } // ensureEOF ensures the read operation ends with an EOF and no diff --git a/vendor/oras.land/oras-go/v2/errdef/errors.go b/vendor/oras.land/oras-go/v2/errdef/errors.go index 7adb44b173..fe7859df17 100644 --- a/vendor/oras.land/oras-go/v2/errdef/errors.go +++ b/vendor/oras.land/oras-go/v2/errdef/errors.go @@ -26,6 +26,7 @@ var ( ErrMissingReference = errors.New("missing reference") ErrNotFound = errors.New("not found") ErrSizeExceedsLimit = errors.New("size exceeds limit") + ErrTooManyPages = errors.New("too many pages") ErrUnsupported = errors.New("unsupported") ErrUnsupportedVersion = errors.New("unsupported version") ) diff --git a/vendor/oras.land/oras-go/v2/registry/remote/repository.go b/vendor/oras.land/oras-go/v2/registry/remote/repository.go index bc649ec288..2f98f5fd9a 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/repository.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/repository.go @@ -127,6 +127,18 @@ type Repository struct { // Reference: https://github.com/oras-project/oras-go/issues/841 ReferrerListPageSize int + // TagListMaxPages limits the total number of pages fetched during tag + // listing, bounding server-driven pagination so a malicious or misbehaving + // registry cannot force unbounded requests. + // If zero, tag listing is unlimited. + TagListMaxPages int + + // ReferrerListMaxPages limits the total number of pages fetched during + // referrer listing, bounding server-driven pagination so a malicious or + // misbehaving registry cannot force unbounded requests. + // If zero, referrer listing is unlimited. + ReferrerListMaxPages int + // MaxMetadataBytes specifies a limit on how many response bytes are allowed // in the server's response to the metadata APIs, such as catalog list, tag // list, and referrers list. @@ -205,6 +217,8 @@ func (r *Repository) clone() *Repository { ManifestMediaTypes: slices.Clone(r.ManifestMediaTypes), TagListPageSize: r.TagListPageSize, ReferrerListPageSize: r.ReferrerListPageSize, + TagListMaxPages: r.TagListMaxPages, + ReferrerListMaxPages: r.ReferrerListMaxPages, MaxMetadataBytes: r.MaxMetadataBytes, SkipReferrersGC: r.SkipReferrersGC, HandleWarning: r.HandleWarning, @@ -400,7 +414,10 @@ func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []strin ctx = auth.AppendRepositoryScope(ctx, r.Reference, auth.ActionPull) url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference) var err error - for err == nil { + for page := 0; err == nil; page++ { + if r.TagListMaxPages > 0 && page >= r.TagListMaxPages { + return fmt.Errorf("tag listing exceeded %d pages: %w", r.TagListMaxPages, errdef.ErrTooManyPages) + } url, err = r.tags(ctx, last, fn, url) // clear `last` for subsequent pages last = "" @@ -512,7 +529,10 @@ func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor url := buildReferrersURL(r.PlainHTTP, ref, artifactType) var err error - for err == nil { + for page := 0; err == nil; page++ { + if r.ReferrerListMaxPages > 0 && page >= r.ReferrerListMaxPages { + return fmt.Errorf("referrer listing exceeded %d pages: %w", r.ReferrerListMaxPages, errdef.ErrTooManyPages) + } url, err = r.referrersPageByAPI(ctx, artifactType, fn, url) } if err == errNoLink { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/utils.go b/vendor/oras.land/oras-go/v2/registry/remote/utils.go index 797169f48f..481193d0e5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/utils.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/utils.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -55,9 +56,40 @@ func parseLink(resp *http.Response) (string, error) { if err != nil { return "", err } + // The Link header value is controlled by the (potentially malicious) + // registry. Restrict pagination to the same origin as the originating + // request so that a registry cannot redirect pagination to an arbitrary + // host and turn a listing call into a server-side request forgery. + if !isSameOrigin(resp.Request.URL, linkURL) { + return "", fmt.Errorf("invalid next link %q: not the same origin as %q", link, resp.Request.URL) + } return linkURL.String(), nil } +// isSameOrigin reports whether the two URLs share the same origin, that is the +// same scheme, host, and port (with the default port applied for http/https). +func isSameOrigin(a, b *url.URL) bool { + if !strings.EqualFold(a.Scheme, b.Scheme) { + return false + } + return canonicalHostPort(a) == canonicalHostPort(b) +} + +// canonicalHostPort returns the lower-cased "host:port" of u, filling in the +// default port for the http and https schemes when none is present. +func canonicalHostPort(u *url.URL) string { + port := u.Port() + if port == "" { + switch strings.ToLower(u.Scheme) { + case "https": + port = "443" + case "http": + port = "80" + } + } + return strings.ToLower(u.Hostname()) + ":" + port +} + // limitReader returns a Reader that reads from r but stops with EOF after n // bytes. If n is less than or equal to zero, defaultMaxMetadataBytes is used. func limitReader(r io.Reader, n int64) io.Reader {