updated vendor
This commit is contained in:
+26
@@ -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
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
# Derived from OWNERS.md
|
||||
* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia
|
||||
* @sabre1041 @shizhMSFT @TerryHowe @Wwwsylvia
|
||||
|
||||
+4
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+108
@@ -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.
|
||||
+7
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
})
|
||||
|
||||
+29
-35
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+101
-1
@@ -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)
|
||||
|
||||
@@ -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{}{}
|
||||
}
|
||||
|
||||
+8
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user