From a1b339144db669022a4cdec261a5a325a077f3b4 Mon Sep 17 00:00:00 2001 From: "red-hat-konflux[bot]" <126015336+red-hat-konflux[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:22:13 +0000 Subject: [PATCH] chore(deps): update module oras.land/oras-go/v2 to v2.6.1 Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com> --- go.mod | 4 +- go.sum | 4 +- vendor/modules.txt | 4 +- vendor/oras.land/oras-go/v2/.goreleaser.yaml | 26 +++++ vendor/oras.land/oras-go/v2/CODEOWNERS | 2 +- vendor/oras.land/oras-go/v2/Makefile | 2 - vendor/oras.land/oras-go/v2/OWNERS.md | 6 +- vendor/oras.land/oras-go/v2/README.md | 2 +- vendor/oras.land/oras-go/v2/RELEASES.md | 108 ++++++++++++++++++ .../oras.land/oras-go/v2/content/file/file.go | 60 +++++++++- .../oras-go/v2/content/oci/storage.go | 2 +- vendor/oras.land/oras-go/v2/content/reader.go | 8 +- .../oras-go/v2/internal/cas/memory.go | 2 +- .../oras-go/v2/internal/graph/memory.go | 64 +++++------ .../oras-go/v2/internal/syncutil/once.go | 4 +- .../oras-go/v2/registry/remote/auth/cache.go | 2 +- .../oras-go/v2/registry/remote/auth/client.go | 102 ++++++++++++++++- .../oras-go/v2/registry/remote/auth/scope.go | 2 +- .../credentials/internal/config/config.go | 9 +- .../registry/remote/credentials/registry.go | 6 +- .../oras-go/v2/registry/remote/manifest.go | 8 +- .../oras-go/v2/registry/remote/referrers.go | 4 +- .../oras-go/v2/registry/remote/repository.go | 29 +++++ 23 files changed, 392 insertions(+), 68 deletions(-) create mode 100644 vendor/oras.land/oras-go/v2/.goreleaser.yaml create mode 100644 vendor/oras.land/oras-go/v2/RELEASES.md diff --git a/go.mod b/go.mod index ad69c7a62..d703a9172 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/argoproj/argo-cd/v3 v3.3.10 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250308055145-5fe7bb3edc86 sigs.k8s.io/controller-tools v0.16.4 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -204,13 +205,12 @@ require ( k8s.io/kubectl v0.35.1 // indirect k8s.io/kubernetes v1.34.2 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect - oras.land/oras-go/v2 v2.6.0 // indirect + oras.land/oras-go/v2 v2.6.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1-0.20251003215857-446d8398e19c // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index baf24bfae..603ae9032 100644 --- a/go.sum +++ b/go.sum @@ -702,8 +702,8 @@ k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI= k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +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= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250308055145-5fe7bb3edc86 h1:96TA+X7D58V3065duUfj+p+Pp17q8U02+cSCmE3IsaU= diff --git a/vendor/modules.txt b/vendor/modules.txt index 0c4e7f7e5..97de96a35 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1762,8 +1762,8 @@ k8s.io/utils/lru k8s.io/utils/net k8s.io/utils/ptr k8s.io/utils/trace -# oras.land/oras-go/v2 v2.6.0 -## explicit; go 1.23.0 +# oras.land/oras-go/v2 v2.6.1 +## explicit; go 1.25.0 oras.land/oras-go/v2 oras.land/oras-go/v2/content oras.land/oras-go/v2/content/file diff --git a/vendor/oras.land/oras-go/v2/.goreleaser.yaml b/vendor/oras.land/oras-go/v2/.goreleaser.yaml new file mode 100644 index 000000000..b4ad1873a --- /dev/null +++ b/vendor/oras.land/oras-go/v2/.goreleaser.yaml @@ -0,0 +1,26 @@ +# Copyright The ORAS Authors. +# 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. + +version: 2 + +# oras-go is a library — no binary builds or archives needed. +builds: + - skip: true + +checksum: + disable: true + +release: + # Tags containing -alpha, -beta, or -rc are automatically marked pre-release. + prerelease: auto + draft: false diff --git a/vendor/oras.land/oras-go/v2/CODEOWNERS b/vendor/oras.land/oras-go/v2/CODEOWNERS index 45a68a31c..f3858e766 100644 --- a/vendor/oras.land/oras-go/v2/CODEOWNERS +++ b/vendor/oras.land/oras-go/v2/CODEOWNERS @@ -1,2 +1,2 @@ # Derived from OWNERS.md -* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia +* @sabre1041 @shizhMSFT @TerryHowe @Wwwsylvia diff --git a/vendor/oras.land/oras-go/v2/Makefile b/vendor/oras.land/oras-go/v2/Makefile index bc671e440..d0af0a73b 100644 --- a/vendor/oras.land/oras-go/v2/Makefile +++ b/vendor/oras.land/oras-go/v2/Makefile @@ -26,12 +26,10 @@ clean: .PHONY: check-encoding check-encoding: ! find . -not -path "./vendor/*" -name "*.go" -type f -exec file "{}" ";" | grep CRLF - ! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF .PHONY: fix-encoding fix-encoding: find . -not -path "./vendor/*" -name "*.go" -type f -exec sed -i -e "s/\r//g" {} + - find scripts -name "*.sh" -type f -exec sed -i -e "s/\r//g" {} + .PHONY: vendor vendor: diff --git a/vendor/oras.land/oras-go/v2/OWNERS.md b/vendor/oras.land/oras-go/v2/OWNERS.md index 402c4a97d..4d29e5eb1 100644 --- a/vendor/oras.land/oras-go/v2/OWNERS.md +++ b/vendor/oras.land/oras-go/v2/OWNERS.md @@ -1,11 +1,13 @@ # Owners Owners: - - Sajay Antony (@sajayantony) + - Andrew Block (@sabre1041) - Shiwei Zhang (@shizhMSFT) - - Steve Lasker (@stevelasker) - Sylvia Lei (@Wwwsylvia) + - Terry Howe (@TerryHowe) Emeritus: - Avi Deitcher (@deitch) - Josh Dolitsky (@jdolitsky) + - Sajay Antony (@sajayantony) + - Steve Lasker (@stevelasker) diff --git a/vendor/oras.land/oras-go/v2/README.md b/vendor/oras.land/oras-go/v2/README.md index 9224754fc..31bdb47cf 100644 --- a/vendor/oras.land/oras-go/v2/README.md +++ b/vendor/oras.land/oras-go/v2/README.md @@ -12,7 +12,7 @@ `oras-go` is a Go library for managing OCI artifacts, compliant with the [OCI Image Format Specification](https://github.com/opencontainers/image-spec) and the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). It provides unified APIs for pushing, pulling, and managing artifacts across OCI-compliant registries, local file systems, and in-memory stores. > [!Note] -> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.23` and `1.24`). +> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.24` and `1.25`). ## Getting Started diff --git a/vendor/oras.land/oras-go/v2/RELEASES.md b/vendor/oras.land/oras-go/v2/RELEASES.md new file mode 100644 index 000000000..c802c22de --- /dev/null +++ b/vendor/oras.land/oras-go/v2/RELEASES.md @@ -0,0 +1,108 @@ +# Releasing oras-go + +Releases are created via a GitOps workflow. Merging a `release/vX.Y.Z` branch +into `v2` automatically tags the commit and publishes the GitHub Release. + +## Steps + +### 1. Create a release branch + +The release branch needs at least one commit so GitHub will allow a PR to be +opened. Use an empty commit as a lightweight marker: + +```bash +git fetch upstream +git checkout -b release/v2.7.0 upstream/v2 +git commit --allow-empty -s -m "chore: prepare release v2.7.0" +git push origin release/v2.7.0 +``` + +The release does not need to contain the changes being released — those are +already on `v2`. The PR is a trigger: when it merges, the workflow tags the +PR's `merge_commit_sha` (the exact commit that landed on `v2`), which includes +all prior work on the branch. + +### 2. Open a pull request + +Open a PR from `release/v2.7.0` targeting the `v2` branch. Write the release +notes directly in the PR description using the format from prior releases: + +```markdown +## New Features +... + +## Bug Fixes +... + +## Documentation +... + +## Other Changes +... +``` + +The PR description becomes the GitHub Release body verbatim, so write it in +its final form. + +### 3. Get approvals + +Branch protection on `v2` requires approval from at least 3 of the 4 owners +listed in [OWNERS.md](OWNERS.md). Reviewers should verify: + +- The target commit is correct +- The release notes are accurate and complete +- All CI checks pass + +### 4. Merge + +Merge the PR. The [release workflow](.github/workflows/release.yml) +automatically: + +1. Extracts the version from the branch name (`release/v2.7.0` → `v2.7.0`) +2. Creates and pushes the git tag +3. Publishes the GitHub Release with the PR body as release notes + +## Pre-releases + +Tags containing `-alpha`, `-beta`, or `-rc` (e.g., `v2.7.0-rc.1`) are +automatically marked as pre-release on GitHub. Use the same branch naming +convention: `release/v2.7.0-rc.1`. + +## Testing the workflow locally + +Three levels of local validation are available without triggering a real release: + +**1. Validate the goreleaser config:** +```bash +goreleaser check +``` + +**2. Validate workflow structure and job matching (dry run):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -n +``` + +**3. Run the workflow end-to-end with a fake token (Colima + cached actions required):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -s GITHUB_TOKEN=fake \ + --pull=false \ + --action-offline-mode \ + --container-daemon-socket - +``` + +This runs all steps up to and including version extraction (`version=vX.Y.Z` will +appear in the output). The `git push` step then fails with a permission error — +that is expected and confirms no tag was pushed. The mock event payload is at +`.github/act/release-event.json`. + +## Updating the documentation site + +After a release, update [oras-www](https://github.com/oras-project/oras-www) +to reflect the new version. See the `CLAUDE.md` in that repository for the +exact steps. diff --git a/vendor/oras.land/oras-go/v2/content/file/file.go b/vendor/oras.land/oras-go/v2/content/file/file.go index 5caaedd0e..6cdba2e48 100644 --- a/vendor/oras.land/oras-go/v2/content/file/file.go +++ b/vendor/oras.land/oras-go/v2/content/file/file.go @@ -39,7 +39,7 @@ import ( // bufPool is a pool of byte buffers that can be reused for copying content // between files. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { // the buffer size should be larger than or equal to 128 KiB // for performance considerations. // we choose 1 MiB here so there will be less disk I/O. @@ -174,7 +174,7 @@ func (s *Store) Close() error { s.setClosed() var errs []string - s.tmpFiles.Range(func(name, _ interface{}) bool { + s.tmpFiles.Range(func(name, _ any) bool { if err := os.Remove(name.(string)); err != nil { errs = append(errs, err.Error()) } @@ -625,6 +625,13 @@ func (s *Store) resolveWritePath(name string) (string, error) { if strings.HasPrefix(rel, "../") || rel == ".." { return "", ErrPathTraversalDisallowed } + // The lexical check above prevents "../" escapes but does not resolve + // symlinks. A symlink component under workingDir (e.g. "out" -> "/outside") + // passes the lexical check yet directs writes outside workingDir. + // Re-check after resolving symlinks in the parent path to close that gap. + if err := checkSymlinkEscape(base, target); err != nil { + return "", err + } } if s.DisableOverwrite { if _, err := os.Stat(path); err == nil { @@ -686,3 +693,52 @@ func (s *Store) setClosed() { func ensureDir(path string) error { return os.MkdirAll(path, 0777) } + +// checkSymlinkEscape returns ErrPathTraversalDisallowed if resolving symlinks +// in target's ancestor directories causes it to escape base. target may not +// yet exist, so symlinks are resolved on its deepest existing ancestor. +func checkSymlinkEscape(base, target string) error { + realBase, err := filepath.EvalSymlinks(base) + if err != nil { + if os.IsNotExist(err) { + return nil // base doesn't exist yet; no symlinks to follow + } + return err + } + realTarget, err := realPathForWrite(target) + if err != nil { + return err + } + rel, err := filepath.Rel(realBase, realTarget) + if err != nil { + return ErrPathTraversalDisallowed + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(rel, "../") || rel == ".." { + return ErrPathTraversalDisallowed + } + return nil +} + +// realPathForWrite resolves symlinks in the deepest existing ancestor of path +// and returns the resulting absolute path. Non-existent path components are +// appended verbatim, matching the semantics of a file about to be created. +func realPathForWrite(path string) (string, error) { + dir := filepath.Dir(path) + suffix := filepath.Base(path) + for { + real, err := filepath.EvalSymlinks(dir) + if err == nil { + return filepath.Join(real, suffix), nil + } + if !os.IsNotExist(err) { + return "", err + } + parent := filepath.Dir(dir) + if parent == dir { + return path, nil // reached filesystem root + } + suffix = filepath.Join(filepath.Base(dir), suffix) + dir = parent + } +} diff --git a/vendor/oras.land/oras-go/v2/content/oci/storage.go b/vendor/oras.land/oras-go/v2/content/oci/storage.go index 072617cbf..be0cf037b 100644 --- a/vendor/oras.land/oras-go/v2/content/oci/storage.go +++ b/vendor/oras.land/oras-go/v2/content/oci/storage.go @@ -33,7 +33,7 @@ import ( // bufPool is a pool of byte buffers that can be reused for copying content // between files. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { // the buffer size should be larger than or equal to 128 KiB // for performance considerations. // we choose 1 MiB here so there will be less disk I/O. diff --git a/vendor/oras.land/oras-go/v2/content/reader.go b/vendor/oras.land/oras-go/v2/content/reader.go index 37bab5e1b..def9ebde8 100644 --- a/vendor/oras.land/oras-go/v2/content/reader.go +++ b/vendor/oras.land/oras-go/v2/content/reader.go @@ -24,6 +24,12 @@ 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 + var ( // ErrInvalidDescriptorSize is returned by ReadAll() when // the descriptor has an invalid size. @@ -119,7 +125,7 @@ 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 { + if desc.Size < 0 || desc.Size > maxDescriptorSize { return nil, ErrInvalidDescriptorSize } buf := make([]byte, desc.Size) diff --git a/vendor/oras.land/oras-go/v2/internal/cas/memory.go b/vendor/oras.land/oras-go/v2/internal/cas/memory.go index 7e358e136..2d97e2a68 100644 --- a/vendor/oras.land/oras-go/v2/internal/cas/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/cas/memory.go @@ -80,7 +80,7 @@ func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, err // necessarily correspond to any consistent snapshot of the storage contents. func (m *Memory) Map() map[descriptor.Descriptor][]byte { res := make(map[descriptor.Descriptor][]byte) - m.content.Range(func(key, value interface{}) bool { + m.content.Range(func(key, value any) bool { res[key.(descriptor.Descriptor)] = value.([]byte) return true }) diff --git a/vendor/oras.land/oras-go/v2/internal/graph/memory.go b/vendor/oras.land/oras-go/v2/internal/graph/memory.go index 016e5f96a..ffd065453 100644 --- a/vendor/oras.land/oras-go/v2/internal/graph/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/graph/memory.go @@ -25,7 +25,6 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/container/set" - "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/status" "oras.land/oras-go/v2/internal/syncutil" ) @@ -34,9 +33,9 @@ import ( type Memory struct { // nodes has the following properties and behaviors: // 1. a node exists in Memory.nodes if and only if it exists in the memory - // 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by + // 2. Memory.nodes saves the ocispec.Descriptor indexed by digest, which are used by // the other fields. - nodes map[descriptor.Descriptor]ocispec.Descriptor + nodes map[digest.Digest]ocispec.Descriptor // predecessors has the following properties and behaviors: // 1. a node exists in Memory.predecessors if it has at least one predecessor @@ -44,14 +43,14 @@ type Memory struct { // the memory. // 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors // in the memory. - predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + predecessors map[digest.Digest]set.Set[digest.Digest] // successors has the following properties and behaviors: // 1. a node exists in Memory.successors if and only if it exists in the memory. // 2. a node's entry in Memory.successors is always consistent with the actual // content of the node, regardless of whether or not each successor exists // in the memory. - successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + successors map[digest.Digest]set.Set[digest.Digest] lock sync.RWMutex } @@ -59,9 +58,9 @@ type Memory struct { // NewMemory creates a new memory PredecessorFinder. func NewMemory() *Memory { return &Memory{ - nodes: make(map[descriptor.Descriptor]ocispec.Descriptor), - predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), - successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + nodes: make(map[digest.Digest]ocispec.Descriptor), + predecessors: make(map[digest.Digest]set.Set[digest.Digest]), + successors: make(map[digest.Digest]set.Set[digest.Digest]), } } @@ -108,14 +107,13 @@ func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]oci m.lock.RLock() defer m.lock.RUnlock() - key := descriptor.FromOCI(node) - set, exists := m.predecessors[key] + set, exists := m.predecessors[node.Digest] if !exists { return nil, nil } var res []ocispec.Descriptor - for k := range set { - res = append(res, m.nodes[k]) + for digest := range set { + res = append(res, m.nodes[digest]) } return res, nil } @@ -126,25 +124,24 @@ func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { m.lock.Lock() defer m.lock.Unlock() - nodeKey := descriptor.FromOCI(node) var danglings []ocispec.Descriptor // remove the node from its successors' predecessor list - for successorKey := range m.successors[nodeKey] { - predecessorEntry := m.predecessors[successorKey] - predecessorEntry.Delete(nodeKey) + for successorDigest := range m.successors[node.Digest] { + predecessorEntry := m.predecessors[successorDigest] + predecessorEntry.Delete(node.Digest) // if none of the predecessors of the node still exists, we remove the // predecessors entry and return it as a dangling node. Otherwise, we do // not remove the entry. if len(predecessorEntry) == 0 { - delete(m.predecessors, successorKey) - if _, exists := m.nodes[successorKey]; exists { - danglings = append(danglings, m.nodes[successorKey]) + delete(m.predecessors, successorDigest) + if _, exists := m.nodes[successorDigest]; exists { + danglings = append(danglings, m.nodes[successorDigest]) } } } - delete(m.successors, nodeKey) - delete(m.nodes, nodeKey) + delete(m.successors, node.Digest) + delete(m.nodes, node.Digest) return danglings } @@ -154,8 +151,8 @@ func (m *Memory) DigestSet() set.Set[digest.Digest] { defer m.lock.RUnlock() s := set.New[digest.Digest]() - for desc := range m.nodes { - s.Add(desc.Digest) + for digest := range m.nodes { + s.Add(digest) } return s } @@ -170,22 +167,20 @@ func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispe defer m.lock.Unlock() // index the node - nodeKey := descriptor.FromOCI(node) - m.nodes[nodeKey] = node + m.nodes[node.Digest] = node // for each successor, put it into the node's successors list, and // put node into the succeesor's predecessors list - successorSet := set.New[descriptor.Descriptor]() - m.successors[nodeKey] = successorSet + successorSet := set.New[digest.Digest]() + m.successors[node.Digest] = successorSet for _, successor := range successors { - successorKey := descriptor.FromOCI(successor) - successorSet.Add(successorKey) - predecessorSet, exists := m.predecessors[successorKey] + successorSet.Add(successor.Digest) + predecessorSet, exists := m.predecessors[successor.Digest] if !exists { - predecessorSet = set.New[descriptor.Descriptor]() - m.predecessors[successorKey] = predecessorSet + predecessorSet = set.New[digest.Digest]() + m.predecessors[successor.Digest] = predecessorSet } - predecessorSet.Add(nodeKey) + predecessorSet.Add(node.Digest) } return successors, nil } @@ -195,7 +190,6 @@ func (m *Memory) Exists(node ocispec.Descriptor) bool { m.lock.RLock() defer m.lock.RUnlock() - nodeKey := descriptor.FromOCI(node) - _, exists := m.nodes[nodeKey] + _, exists := m.nodes[node.Digest] return exists } diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go index e44970530..d46ebd755 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go @@ -24,7 +24,7 @@ import ( // Once is an object that will perform exactly one action. // Unlike sync.Once, this Once allows the action to have return values. type Once struct { - result interface{} + result any err error status chan bool } @@ -46,7 +46,7 @@ func NewOnce() *Once { // Besides the return value of the function f, including the error, Do returns // true if the function f passed is called first and is not cancelled, deadline // exceeded, or panicking. Otherwise, returns false. -func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) { +func (o *Once) Do(ctx context.Context, f func() (any, error)) (bool, any, error) { defer func() { if r := recover(); r != nil { o.status <- true diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go index d11c092bf..80091ddf5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go @@ -109,7 +109,7 @@ func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Sche }, " ") statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) fetchOnce := statusValue.(*syncutil.Once) - fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { + fetchedFirst, result, err := fetchOnce.Do(ctx, func() (any, error) { return fetch(ctx) }) if fetchedFirst { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go index 5c5330e72..ce9297af7 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -136,7 +137,50 @@ func (c *Client) send(req *http.Request) (*http.Response, error) { for key, values := range c.Header { req.Header[key] = append(req.Header[key], values...) } - return c.client().Do(req) + // Drop the Authorization header when a redirect crosses an HTTP origin + // (scheme, host, or port). The standard library only strips sensitive + // headers when the hostname changes, so a redirect to a different port on + // the same host would otherwise forward credentials to an unintended + // endpoint. Any caller-provided CheckRedirect is preserved. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + client := c.client() + clientCopy := *client + checkRedirect := client.CheckRedirect + clientCopy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) > 0 && !sameHTTPOrigin(via[len(via)-1].URL, req.URL) { + req.Header.Del("Authorization") + } + if checkRedirect != nil { + return checkRedirect(req, via) + } + return nil + } + return clientCopy.Do(req) +} + +// sameHTTPOrigin reports whether a and b share the same HTTP origin, i.e. the +// same scheme and host. Default ports are normalized so that, for example, +// "example.com" and "example.com:443" compare equal over https. +func sameHTTPOrigin(a, b *url.URL) bool { + if !strings.EqualFold(a.Scheme, b.Scheme) { + return false + } + return canonicalHost(a) == canonicalHost(b) +} + +// canonicalHost returns the lower-cased host of u with the default port for its +// scheme applied when no explicit port is present. +func canonicalHost(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 } // credential resolves the credential for the given registry. @@ -156,6 +200,49 @@ func (c *Client) cache() Cache { return c.Cache } +// validateRealm rejects bearer token realm URLs that would have the client +// forward credentials to obviously unsafe destinations: +// +// - schemes other than http or https, +// - http realms when the registry was contacted over https (TLS downgrade), +// - hosts that are IP literals in loopback, link-local, private, or +// unspecified ranges (e.g. cloud instance metadata services such as +// 169.254.169.254). +// +// Cross-host realms with a public hostname are permitted, because the +// distribution spec allows a separate token endpoint (e.g. Docker Hub's +// auth.docker.io). When the registry itself is reached at the same hostname +// as the realm, the IP-literal check is skipped so loopback and in-cluster +// deployments continue to work. +func validateRealm(realm string, registryURL *url.URL) error { + if realm == "" { + return nil + } + realmURL, err := url.Parse(realm) + if err != nil { + return fmt.Errorf("failed to parse bearer realm %q: %w", realm, err) + } + switch realmURL.Scheme { + case "https": + // always allowed + case "http": + if registryURL != nil && registryURL.Scheme == "https" { + return fmt.Errorf("bearer realm %q uses http but registry was contacted over https", realm) + } + default: + return fmt.Errorf("bearer realm %q uses unsupported scheme %q", realm, realmURL.Scheme) + } + if ip := net.ParseIP(realmURL.Hostname()); ip != nil { + if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || + ip.IsPrivate() || ip.IsUnspecified() { + if registryURL == nil || realmURL.Hostname() != registryURL.Hostname() { + return fmt.Errorf("bearer realm host %q is a loopback, link-local, private, or unspecified address", realmURL.Hostname()) + } + } + } + return nil +} + // SetUserAgent sets the user agent for all out-going requests. func (c *Client) SetUserAgent(userAgent string) { if c.Header == nil { @@ -182,6 +269,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { var attemptedKey string cache := c.cache() host := originalReq.Host + if host == "" { + host = originalReq.URL.Host + } scheme, err := cache.GetScheme(ctx, host) if err == nil { switch scheme { @@ -207,6 +297,13 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { if resp.StatusCode != http.StatusUnauthorized { return resp, nil } + // If the challenge came from a different origin than originally requested + // (e.g. the request was redirected to another host or port), do not resolve + // or send the registry credentials to that origin. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + if resp.Request != nil && !sameHTTPOrigin(originalReq.URL, resp.Request.URL) { + return resp, nil + } // attempt again with credentials for recognized schemes challenge := resp.Header.Get("Www-Authenticate") @@ -257,6 +354,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { // attempt with credentials realm := params["realm"] + if err := validateRealm(realm, originalReq.URL); err != nil { + return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) + } service := params["service"] token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) { return c.fetchBearerToken(ctx, host, realm, service, scopes) diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go index bdd6e5c48..4e515a815 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go @@ -254,7 +254,7 @@ func CleanScopes(scopes []string) []string { actionSet = make(map[string]struct{}) namedActions[resourceName] = actionSet } - for _, action := range strings.Split(actions, ",") { + for action := range strings.SplitSeq(actions, ",") { if action != "" { actionSet[action] = struct{}{} } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/credentials/internal/config/config.go b/vendor/oras.land/oras-go/v2/registry/remote/credentials/internal/config/config.go index 3a898f22b..295b2d1d7 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/credentials/internal/config/config.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/credentials/internal/config/config.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -128,7 +129,13 @@ func Load(configPath string) (*Config, error) { // decode config content if the config file exists if err := json.NewDecoder(configFile).Decode(&cfg.content); err != nil { - return nil, fmt.Errorf("failed to decode config file at %s: %w: %v", configPath, ErrInvalidConfigFormat, err) + if errors.Is(err, io.EOF) { + // empty or whitespace only file + cfg.content = make(map[string]json.RawMessage) + cfg.authsCache = make(map[string]json.RawMessage) + return cfg, nil + } + return nil, fmt.Errorf("failed to decode config file %s: %w: %v", configPath, ErrInvalidConfigFormat, err) } if credsStoreBytes, ok := cfg.content[configFieldCredentialsStore]; ok { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/credentials/registry.go b/vendor/oras.land/oras-go/v2/registry/remote/credentials/registry.go index 39735b77c..d27a9a9ac 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/credentials/registry.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/credentials/registry.go @@ -81,10 +81,12 @@ func Credential(store Store) auth.CredentialFunc { // ServerAddressFromRegistry maps a registry to a server address, which is used as // a key for credentials store. The Docker CLI expects that the credentials of -// the registry 'docker.io' will be added under the key "https://index.docker.io/v1/". +// the registry 'registry-1.docker.io' or the alias 'docker.io' will be added +// under the key "https://index.docker.io/v1/". // See: https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48 func ServerAddressFromRegistry(registry string) string { - if registry == "docker.io" { + if registry == "docker.io" || + registry == "registry-1.docker.io" { return "https://index.docker.io/v1/" } return registry diff --git a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go index 0e10297cb..845bf6c53 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go @@ -16,6 +16,7 @@ limitations under the License. package remote import ( + "slices" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -41,12 +42,7 @@ func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool { if len(manifestMediaTypes) == 0 { manifestMediaTypes = defaultManifestMediaTypes } - for _, mediaType := range manifestMediaTypes { - if desc.MediaType == mediaType { - return true - } - } - return false + return slices.Contains(manifestMediaTypes, desc.MediaType) } // manifestAcceptHeader generates the set in the `Accept` header for resolving diff --git a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go index 720430d39..f2ad0b2dd 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go @@ -118,8 +118,8 @@ func isReferrersFilterApplied(applied, requested string) bool { if applied == "" || requested == "" { return false } - filters := strings.Split(applied, ",") - for _, f := range filters { + filters := strings.SplitSeq(applied, ",") + for f := range filters { if f == requested { return true } 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 fed993dff..bc649ec28 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/repository.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/repository.go @@ -24,6 +24,7 @@ import ( "io" "mime" "net/http" + "net/url" "slices" "strconv" "strings" @@ -872,6 +873,25 @@ func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, conte return s.completePushAfterInitialPost(ctx, req, resp, expected, content) } +// sameUploadHost reports whether location and reqURL refer to the same host, +// normalizing implicit default ports (80 for http, 443 for https) so that +// e.g. "example.com" and "example.com:443" compare equal over HTTPS. +func sameUploadHost(location, reqURL *url.URL) bool { + if location.Hostname() != reqURL.Hostname() { + return false + } + canonicalPort := func(u *url.URL) string { + if p := u.Port(); p != "" { + return p + } + if u.Scheme == "https" { + return "443" + } + return "80" + } + return canonicalPort(location) == canonicalPort(reqURL) +} + // completePushAfterInitialPost implements step 2 of the push protocol. This can be invoked either by // Push or by Mount when the receiving repository does not implement the // mount endpoint. @@ -894,6 +914,15 @@ func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http. if reqPort == "443" && locationHostname == reqHostname && locationPort == "" { location.Host = locationHostname + ":" + reqPort } + // Validate the Location stays on the same host to prevent credentials from + // being forwarded to an attacker-controlled endpoint. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-jxpm-75mh-9fp7 + if !sameUploadHost(location, req.URL) { + return fmt.Errorf("blob upload Location %q is on a different host than the registry %q", location.Host, req.URL.Host) + } + if req.URL.Scheme == "https" && location.Scheme != "https" { + return fmt.Errorf("blob upload Location %q downgrades scheme from https", location.Host) + } url := location.String() req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content) if err != nil {