Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package oras for openSUSE:Factory checked in at 2026-10-27 16:49:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/oras (Old) and /work/SRC/openSUSE:Factory/.oras.new.2252680 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "oras" Tue Oct 27 16:49:10 2026 rev:13 rq:1392951 version:1.3.6 Changes: -------- --- /work/SRC/openSUSE:Factory/oras/oras.changes 2026-10-20 12:38:34.738164466 +0200 +++ /work/SRC/openSUSE:Factory/.oras.new.2252680/oras.changes 2026-10-27 16:49:39.051218790 +0100 @@ -1,0 +2,41 @@ +Tue Oct 27 06:43:01 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.3.6: + * Dependencies + - Update oras.land/oras-go/v2 from v2.6.4 to v2.7.0 (#2284, + fixes #2275) + - Behavior change: v2.6.4 rejected bearer token realms on any + private address, which broke split-host registries such as + self-hosted GitLab (oras-project/oras-go#1631). The default + denylist now blocks only loopback, link-local, unspecified, + and cloud metadata addresses. RFC 1918, IPv6 unique-local, + and CGNAT realms are allowed again. + - Authentication now works after a same-host HTTP-to-HTTPS + redirect. + - Copy and verification fixes: FilterArtifactType matching + for referrers with an empty config and for image indexes, + PreCopy returning SkipNode after a declined cross-repo + mount, and a clearer error when a source fails after + sending all its declared bytes. + * Bug fixes + - oras tag now checks that it gets a source reference and at + least one tag (#2265, fixes #2264) + - --from-resolve and --to-resolve rules no longer overwrite + each other when combined with --resolve (#2282, fixes #2277) + - Fix a panic when a mount is attempted against a tracked + target that does not support mounting, such as an OCI layout + (#2283, fixes #2278) + * Release and packaging + - Release archives and the checksums file now come with GitHub + artifact attestations (SLSA build provenance). You can verify + them with gh attestation verify in addition to GPG signatures + (#2271, fixes #2269) + - Stable snap releases now publish to the candidate channel as + well as stable. The classic snap no longer bundles its own + ca-certificates and uses the host certificate store instead + (#2274) + * Other + - More unit tests for display JSON metadata handlers and + contentutil (#2266, #2267) + +------------------------------------------------------------------- Old: ---- oras-1.3.5.obscpio New: ---- oras-1.3.6.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ oras.spec ++++++ --- /var/tmp/diff_new_pack.uTPr8O/_old 2026-10-27 16:49:40.904296451 +0100 +++ /var/tmp/diff_new_pack.uTPr8O/_new 2026-10-27 16:49:40.907296577 +0100 @@ -17,7 +17,7 @@ Name: oras -Version: 1.3.5 +Version: 1.3.6 Release: 0 Summary: OCI registry client - manage content like artifacts, images, packages License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.uTPr8O/_old 2026-10-27 16:49:41.004300642 +0100 +++ /var/tmp/diff_new_pack.uTPr8O/_new 2026-10-27 16:49:41.012300977 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/oras-project/oras.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v1.3.5</param> + <param name="revision">refs/tags/v1.3.6</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.uTPr8O/_old 2026-10-27 16:49:41.070303408 +0100 +++ /var/tmp/diff_new_pack.uTPr8O/_new 2026-10-27 16:49:41.079303785 +0100 @@ -3,6 +3,6 @@ <param name="url">https://github.com/oras-project/oras</param> <param name="changesrevision">fe425992fdfdf300a1cfb380bc4271b3e1a3d3db</param></service><service name="tar_scm"> <param name="url">https://github.com/oras-project/oras.git</param> - <param name="changesrevision">d6e7b583b9afc7fa3798de77181c53c94418d727</param></service></servicedata> + <param name="changesrevision">9b1fc815f6b7fba743f08b9b5c5846639cc11d3c</param></service></servicedata> (No newline at EOF) ++++++ oras-1.3.5.obscpio -> oras-1.3.6.obscpio ++++++ ++++ 1637 lines of diff (skipped) ++++++ oras.obsinfo ++++++ --- /var/tmp/diff_new_pack.uTPr8O/_old 2026-10-27 16:49:41.874337104 +0100 +++ /var/tmp/diff_new_pack.uTPr8O/_new 2026-10-27 16:49:41.883337481 +0100 @@ -1,5 +1,5 @@ name: oras -version: 1.3.5 -mtime: 1792368263 -commit: d6e7b583b9afc7fa3798de77181c53c94418d727 +version: 1.3.6 +mtime: 1792866220 +commit: 9b1fc815f6b7fba743f08b9b5c5846639cc11d3c ++++++ vendor.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2026-10-19 02:04:23.000000000 +0200 +++ new/vendor/modules.txt 2026-10-24 20:23:40.000000000 +0200 @@ -74,7 +74,7 @@ # golang.org/x/term v0.47.0 ## explicit; go 1.26.0 golang.org/x/term -# oras.land/oras-go/v2 v2.6.4 +# oras.land/oras-go/v2 v2.7.0 ## explicit; go 1.26.0 oras.land/oras-go/v2 oras.land/oras-go/v2/content diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/oras.land/oras-go/v2/content/reader.go new/vendor/oras.land/oras-go/v2/content/reader.go --- old/vendor/oras.land/oras-go/v2/content/reader.go 2026-10-19 02:04:23.000000000 +0200 +++ new/vendor/oras.land/oras-go/v2/content/reader.go 2026-10-24 20:23:40.000000000 +0200 @@ -163,8 +163,11 @@ func ensureEOF(r io.Reader) error { var peek [1]byte _, err := io.ReadFull(r, peek[:]) - if err != io.EOF { + if err == nil { return ErrTrailingData } + if err != io.EOF { + return fmt.Errorf("read failed: %w", err) + } return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/oras.land/oras-go/v2/copy.go new/vendor/oras.land/oras-go/v2/copy.go --- old/vendor/oras.land/oras-go/v2/copy.go 2026-10-19 02:04:23.000000000 +0200 +++ new/vendor/oras.land/oras-go/v2/copy.go 2026-10-24 20:23:40.000000000 +0200 @@ -401,6 +401,10 @@ // Mount or copy if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil && !errors.Is(err, skipSource) { + if errors.Is(err, SkipNode) { + // PreCopy made the blob available in the target + return nil + } return newCopyError("Mount", CopyErrorOriginDestination, err) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/oras.land/oras-go/v2/extendedcopy.go new/vendor/oras.land/oras-go/v2/extendedcopy.go --- old/vendor/oras.land/oras-go/v2/extendedcopy.go 2026-10-19 02:04:23.000000000 +0200 +++ new/vendor/oras.land/oras-go/v2/extendedcopy.go 2026-10-24 20:23:40.000000000 +0200 @@ -361,7 +361,7 @@ // if the artifact type is not present in the descriptors, // fetch it from the manifest content. switch p.MediaType { - case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest: + case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: artifactType, err := fetchArtifactType(ctx, src, p) if err != nil { return nil, err @@ -397,7 +397,16 @@ if err := json.NewDecoder(rc).Decode(&manifest); err != nil { return "", err } + if manifest.ArtifactType != "" { + return manifest.ArtifactType, nil + } return manifest.Config.MediaType, nil + case ocispec.MediaTypeImageIndex: + var index ocispec.Index + if err := json.NewDecoder(rc).Decode(&index); err != nil { + return "", err + } + return index.ArtifactType, nil default: return "", nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go new/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go --- old/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go 2026-10-19 02:04:23.000000000 +0200 +++ new/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go 2026-10-24 20:23:40.000000000 +0200 @@ -27,6 +27,7 @@ "net/http" "net/netip" "net/url" + "slices" "strings" "oras.land/oras-go/v2/registry/remote/internal/errutil" @@ -128,6 +129,19 @@ // - https://distribution.github.io/distribution/spec/auth/jwt/ // - https://distribution.github.io/distribution/spec/auth/oauth/ ForceAttemptOAuth2 bool + + // BlockedRealmPrefixes is the denylist of address prefixes a bearer realm + // host must not be, or resolve to. A realm is rejected if any of its + // addresses falls in a listed prefix. + // + // nil uses DefaultBlockedRealmPrefixes (loopback, link-local, + // unspecified, and cloud metadata). An empty non-nil slice blocks + // nothing. A non-empty slice is used exactly, without merging defaults. + // The scheme check and the https-to-http downgrade check always apply. + // RFC 1918, IPv6 unique-local, and carrier-grade NAT addresses are + // allowed by default. Strict mode is + // append(DefaultBlockedRealmPrefixes(), PrivateRealmPrefixes()...). + BlockedRealmPrefixes []netip.Prefix } // client returns an HTTP client used to access the remote registry. @@ -207,6 +221,39 @@ return canonicalHost(a) == canonicalHost(b) } +// isHTTPSUpgrade reports whether a redirect upgrades the same host from +// standard HTTP to standard HTTPS. +func isHTTPSUpgrade(from, to *url.URL) bool { + return from.Scheme == "http" && to.Scheme == "https" && + strings.EqualFold(from.Hostname(), to.Hostname()) && + (from.Port() == "" || from.Port() == "80") && + (to.Port() == "" || to.Port() == "443") +} + +// safeHTTPSUpgrade checks every redirect hop before accepting an upgraded challenge. +func safeHTTPSUpgrade(original, last *http.Request) bool { + if !isHTTPSUpgrade(original.URL, last.URL) { + return false + } + if original.Host != "" && !strings.EqualFold(original.Host, original.URL.Host) { + return false + } + for req := last; ; { + if req.URL.User != nil { + return false + } + if req.Response == nil { + return sameHTTPOrigin(original.URL, req.URL) + } + previous := req.Response.Request + if previous == nil || + (!sameHTTPOrigin(previous.URL, req.URL) && !isHTTPSUpgrade(previous.URL, req.URL)) { + return false + } + req = previous + } +} + // 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 { @@ -243,17 +290,95 @@ // that tests can inject a fake resolver. var lookupIPAddr = net.DefaultResolver.LookupIPAddr -// cgnatPrefix is the shared address space for carrier-grade NAT (RFC 6598). -// It is not covered by net.IP.IsPrivate, yet it hosts internal services such as -// the Alibaba Cloud instance metadata service at 100.100.100.200. -var cgnatPrefix = netip.MustParsePrefix("100.64.0.0/10") - -// isInternalAddr reports whether addr is a loopback, link-local, private, -// unspecified, or carrier-grade NAT address. -func isInternalAddr(addr netip.Addr) bool { - addr = addr.Unmap() - return addr.IsLoopback() || addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() || - addr.IsPrivate() || addr.IsUnspecified() || cgnatPrefix.Contains(addr) +var ( + nat64Prefix = netip.MustParsePrefix("64:ff9b::/96") + sixTo4Prefix = netip.MustParsePrefix("2002::/16") + + defaultBlockedRealmPrefixes = []netip.Prefix{ + netip.MustParsePrefix("127.0.0.0/8"), + netip.MustParsePrefix("::1/128"), + netip.MustParsePrefix("169.254.0.0/16"), + netip.MustParsePrefix("fe80::/10"), + netip.MustParsePrefix("224.0.0.0/24"), + netip.MustParsePrefix("ff02::/16"), + netip.MustParsePrefix("0.0.0.0/8"), + netip.MustParsePrefix("::/128"), + netip.MustParsePrefix("100.100.100.200/32"), + netip.MustParsePrefix("fd00:ec2::254/128"), + netip.MustParsePrefix("fd00:ec2::23/128"), + netip.MustParsePrefix("fd20:ce::254/128"), + netip.MustParsePrefix("168.63.129.16/32"), + } + + privateRealmPrefixes = []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("172.16.0.0/12"), + netip.MustParsePrefix("192.168.0.0/16"), + netip.MustParsePrefix("fc00::/7"), + netip.MustParsePrefix("100.64.0.0/10"), + } +) + +// DefaultBlockedRealmPrefixes returns a fresh copy of the default realm +// address denylist: loopback, link-local (including IMDS), link-local +// multicast, unspecified, and cloud metadata addresses outside link-local. +// RFC 1918, IPv6 unique-local, and carrier-grade NAT are not included. +func DefaultBlockedRealmPrefixes() []netip.Prefix { + return slices.Clone(defaultBlockedRealmPrefixes) +} + +// PrivateRealmPrefixes returns a fresh copy of RFC 1918, IPv6 unique-local +// (fc00::/7), and carrier-grade NAT (100.64.0.0/10) prefixes. Append them to +// DefaultBlockedRealmPrefixes for a strict denylist. +func PrivateRealmPrefixes() []netip.Prefix { + return slices.Clone(privateRealmPrefixes) +} + +// unwrapRealmAddr returns the IPv4 address embedded in an IPv4-mapped, +// NAT64, or 6to4 IPv6 address. Other addresses are returned unmapped. +// The IPv6 zone is stripped so Prefix.Contains can match scoped literals. +func unwrapRealmAddr(addr netip.Addr) netip.Addr { + addr = addr.Unmap().WithZone("") + if !addr.Is6() { + return addr + } + b := addr.As16() + if nat64Prefix.Contains(addr) { + return netip.AddrFrom4([4]byte{b[12], b[13], b[14], b[15]}) + } + if sixTo4Prefix.Contains(addr) { + return netip.AddrFrom4([4]byte{b[2], b[3], b[4], b[5]}) + } + return addr +} + +// blockedPrefix reports whether addr, after unwrapping, falls in any prefix. +func blockedPrefix(addr netip.Addr, prefixes []netip.Prefix) (netip.Prefix, bool) { + check := unwrapRealmAddr(addr) + for _, prefix := range prefixes { + if prefix.Contains(check) || prefix.Contains(addr) { + return prefix, true + } + } + return netip.Prefix{}, false +} + +// resolveBlockedPrefixes returns the denylist to apply. nil means defaults; +// a non-nil slice (including empty) is used as given. +func resolveBlockedPrefixes(prefixes []netip.Prefix) []netip.Prefix { + if prefixes == nil { + return defaultBlockedRealmPrefixes + } + return prefixes +} + +const apiPrefix = "/v2/" + +var endpointSeparators = []string{ + "/manifests/", + "/blobs/", + "/tags/list", + "/referrers/", } // validateRealm rejects bearer token realm URLs that would have the client @@ -261,10 +386,8 @@ // // - schemes other than http or https, // - http realms when the registry was contacted over https (TLS downgrade), -// - hosts that are, or resolve to, loopback, link-local, private, -// unspecified, or carrier-grade NAT (100.64.0.0/10) addresses (e.g. cloud -// instance metadata services such as 169.254.169.254 or -// 100.100.100.200). +// - hosts that are, or resolve to, an address in prefixes (nil uses +// DefaultBlockedRealmPrefixes; an empty non-nil slice blocks nothing). // // Cross-host realms with a public hostname are permitted, because the // distribution spec allows a separate token endpoint (e.g. Docker Hub's @@ -272,12 +395,15 @@ // as the realm, the address check is skipped so loopback and in-cluster // deployments continue to work. // +// IPv4-mapped, NAT64, and 6to4 addresses are unwrapped before matching. +// The scheme and downgrade checks always apply. +// // A realm host that is not an IP literal is resolved with ctx, and the realm -// is rejected if any of its A or AAAA records is an internal address. If the +// is rejected if any of its A or AAAA records is in a blocked prefix. If the // lookup fails, the realm is allowed: behind an HTTP proxy the client may be // unable to resolve names that the proxy can, and a bogus name fails at dial // time anyway. The check does not defend against DNS rebinding, where the -// name resolves to a public address here and to an internal address when the +// name resolves to a public address here and to a blocked prefix when the // request is dialed. // // validateRealm is also applied to every redirect hop of a token request. @@ -285,7 +411,7 @@ // References: // - https://github.com/oras-project/oras-go/security/advisories/GHSA-xf85-363p-868w // - https://github.com/oras-project/oras-go/security/advisories/GHSA-mm27-wq9c-mxxr -func validateRealm(ctx context.Context, realm string, registryURL *url.URL) error { +func validateRealm(ctx context.Context, realm string, registryURL *url.URL, prefixes []netip.Prefix) error { if realm == "" { return nil } @@ -307,9 +433,13 @@ if registryURL != nil && strings.EqualFold(host, registryURL.Hostname()) { return nil } + prefixes = resolveBlockedPrefixes(prefixes) + if len(prefixes) == 0 { + return nil + } if addr, err := netip.ParseAddr(host); err == nil { - if isInternalAddr(addr) { - return fmt.Errorf("bearer realm host %q is a loopback, link-local, private, unspecified, or carrier-grade NAT address", host) + if prefix, ok := blockedPrefix(addr, prefixes); ok { + return fmt.Errorf("bearer realm host %q is %s, in blocked prefix %s", host, unwrapRealmAddr(addr), prefix) } return nil } @@ -320,8 +450,11 @@ } for _, ipAddr := range addrs { addr, ok := netip.AddrFromSlice(ipAddr.IP) - if ok && isInternalAddr(addr) { - return fmt.Errorf("bearer realm host %q resolves to %s, a loopback, link-local, private, unspecified, or carrier-grade NAT address", host, addr.Unmap()) + if !ok { + continue + } + if prefix, ok := blockedPrefix(addr, prefixes); ok { + return fmt.Errorf("bearer realm host %q resolves to %s, in blocked prefix %s", host, unwrapRealmAddr(addr), prefix) } } return nil @@ -330,14 +463,15 @@ // realmRedirectGuard returns a shallow copy of client whose CheckRedirect // applies validateRealm to every redirect hop of a token request, measured // against registryURL, before delegating to the CheckRedirect of client. This -// keeps a validated realm from redirecting the token request to an internal -// address or from https to http. +// keeps a validated realm from redirecting the token request into a blocked +// prefix or from https to http. prefixes is the same denylist used for the +// initial realm so redirect hops are evaluated consistently. // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-mm27-wq9c-mxxr -func realmRedirectGuard(client *http.Client, registryURL *url.URL) *http.Client { +func realmRedirectGuard(client *http.Client, registryURL *url.URL, prefixes []netip.Prefix) *http.Client { clientCopy := *client checkRedirect := client.CheckRedirect clientCopy.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if err := validateRealm(req.Context(), req.URL.String(), registryURL); err != nil { + if err := validateRealm(req.Context(), req.URL.String(), registryURL, prefixes); err != nil { return fmt.Errorf("redirect of token request rejected: %w", err) } if checkRedirect != nil { @@ -361,7 +495,7 @@ // - https://github.com/oras-project/oras-go/security/advisories/GHSA-xc97-9f2f-9hrx func (c *Client) sendTokenRequest(req *http.Request, registryURL *url.URL) (*http.Response, error) { tokenClient := *c - tokenClient.Client = realmRedirectGuard(c.client(), registryURL) + tokenClient.Client = realmRedirectGuard(c.client(), registryURL, c.BlockedRealmPrefixes) return tokenClient.sendRedirectSafe(req, true) } @@ -419,12 +553,23 @@ 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 + // Resolve credentials only for the original origin and repository, or a + // same-host HTTPS upgrade. + authReq := originalReq + if resp.Request != nil { + responseURL := resp.Request.URL + if !sameHTTPOrigin(originalReq.URL, responseURL) { + if !safeHTTPSUpgrade(originalReq, resp.Request) { + return resp, nil + } + authReq = resp.Request.Clone(ctx) + authReq.Header = originalReq.Header.Clone() + if authReq.Method == originalReq.Method { + authReq.GetBody = originalReq.GetBody + } + // Cache keys don't distinguish HTTP from HTTPS. + cache = noCache{} + } } // attempt again with credentials for recognized schemes @@ -441,7 +586,7 @@ return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) } - req = originalReq.Clone(ctx) + req = authReq.Clone(ctx) req.Header.Set("Authorization", "Basic "+token) case SchemeBearer: resp.Body.Close() @@ -457,7 +602,7 @@ // attempt the cache again if there is a scope change if key != attemptedKey { if token, err := cache.GetToken(ctx, host, SchemeBearer, key); err == nil { - req = originalReq.Clone(ctx) + req = authReq.Clone(ctx) req.Header.Set("Authorization", "Bearer "+token) if err := rewindRequestBody(req); err != nil { return nil, err @@ -476,18 +621,18 @@ // attempt with credentials realm := params["realm"] - if err := validateRealm(ctx, realm, originalReq.URL); err != nil { + if err := validateRealm(ctx, realm, authReq.URL, c.BlockedRealmPrefixes); 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, originalReq.URL, host, realm, service, scopes) + return c.fetchBearerToken(ctx, authReq.URL, host, realm, service, scopes) }) if err != nil { return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) } - req = originalReq.Clone(ctx) + req = authReq.Clone(ctx) req.Header.Set("Authorization", "Bearer "+token) default: return resp, nil
