Repository navigation
Renovate: Update module oras.land/oras-go/v2 to v2.6.2 [SECURITY] - #41
renovate[bot] wants to merge 1 commit into
Conversation
23ae74e to
a73c82f
Compare
44e470c to
f075586
Compare
cbaaaf1 to
3a5e065
Compare
3a5e065 to
77fc3cb
Compare
There was a problem hiding this comment.
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/v2fromv2.6.1tov2.6.2(security patch release). - Update
golang.org/x/syncfromv0.21.0tov0.22.0as part of dependency resolution. - Promote
github.com/gardener/gardener-extension-provider-openstack v1.54.0from indirect to direct ingo.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.
77fc3cb to
2c3802e
Compare
Merging this branch will decrease overall coverage
Coverage by fileChanged files (no unit tests)
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
|
2c3802e to
a86d465
Compare
There was a problem hiding this comment.
🟢 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
a86d465 to
d590bfd
Compare
There was a problem hiding this comment.
🟢 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
d590bfd to
3e1c4f9
Compare
3e1c4f9 to
f2dfce7
Compare
53cf951 to
871a577
Compare
871a577 to
fccbf99
Compare
|
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:
For more information about GitHub Code Scanning, check out the documentation. |
fccbf99 to
73aaecb
Compare
73aaecb to
25272b3
Compare
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
v2.6.1→v2.6.2oras-gotar extraction: Hardlink entry with relative Linkname escapes extract dir via process CWD resolutionCVE-2026-50163 / GHSA-fxhp-mv3v-67qp
More information
Details
Root cause
The tar-extraction helper
ensureLinkPathatcontent/file/utils.go:262-275validates that a hardlink's target resolves inside the extract base, but then returns the original unresolvedtargetstring back to the caller:The caller for
TypeLinkhardlinks then does:os.Link(oldname, newname)wraps thelink(2)system call. From thelink(2)man page:So when
target(i.e.,header.Linkname) is a relative path,os.Linkresolves it against the process's current working directory, not againstfilepath.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:payload.tar.gz/README.txt.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 tellsoras-goto auto-extract).When a victim runs
oras pull(or any Go code usingcontent.File), the extraction:payload.tar.gz/evil_cwd_link— passes.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.target = "victim.secret"(NOTpath).os.Link("victim.secret", "<extract_base>/payload.tar.gz/evil_cwd_link").link(2)resolves relativeoldname="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
orasCLI v1.3.0 (SHA-256040e140304b7dbdd9b40dacd798e2303cea44ad84eeb210750afdf15f1dcf8b4, 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:
Observed output:
A library-level regression test is also provided (
poc_test.go) that drops intocontent/file/utils_test.goand runs viago 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
orasCLI or any Go program usingoras-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:
oras pullruns 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.oras-goto fetch artifacts; their CWD is typically/or/root— very sensitive.oras-goto 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):tarDirectoryin 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.TypeSymlink):os.Symlinkstores the target string verbatim and does not CWD-resolve at creation time. The currentensureLinkPathreturn-of-targetis 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 (permay_linkat()in the kernel). Verified on Ubuntu 24.04: as non-root,ln /etc/passwd /tmp/xreturnsEPERM, and the same via the oras PoC path returnslink 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 invokesoras pullas a regular user. The attack surface depends on the invocation context:oras pullrun by a regular user.env,.git/config,.aws/credentials,~/.ssh/config, project-local secrets, CI workspace files.oras pullrun as root (systemd withoutUser=, container entrypoint default root, Kubernetes operator)/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
ensureLinkPathto expose both the verbatim target (for symlinks) and the resolved absolute path (for hardlinks); have theTypeLinkcase use the resolved path.Regression test to add:
Extend
Test_extractTarDirectory_HardLinkwith a third sub-test that:t.TempDir()(or an explicitlyos.Chdir-entered directory) with a known name, e.g.sentinel.txt.TypeLinkentry withLinkname: "sentinel.txt"(relative).extractTarDirectoryreturned an error, OR the resulting hardlink's inode does NOT match the sentinel's inode.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:NReferences
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.Storein oras-go v2 unpacks OCI layer tarballs when a descriptor carriesio.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-symlinkLstatwalk. 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 defaultAllowPathTraversalOnWrite=falseconfiguration — a canonical tar-slip → RCE primitive.Details
Affected versions:
<= v2.6.1Entry point:
content/file/file.goline 486,(*Store).pushDir— reached from(*Store).Pushfor any descriptor whose annotations includeio.deis.oras.content.unpack: "true"(i.e.file.AnnotationUnpack) and anorg.opencontainers.image.title.oras.Copyfrom a remote registry into afile.New(dir)store invokes this per layer.Root cause 1 — lexical link validation.
content/file/utils.golines 264–275,ensureLinkPath:filepath.Joincleans..components textually and does not dereference symlinks in intermediate components. It therefore cannot detect that a component oftargetis 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.golines 247–257, insideresolveRelToBase:For an entry named
<title>/escape,path == "escape"andfilepath.Dir("escape") == ".", so the loop body never executes — the entry itself is neverLstat-checked.Root cause 3 — write follows symlinks.
content/file/utils.goline 279,writeFile:No
O_NOFOLLOW, so ifpathis a symlink the write goes to its target.Data flow / exploit construction. Let
baseAbs = <workingDir>/<title>andN = depth(baseAbs)(number of path components from/). The attacker's tar.gz contains, in order:Nnested directories<title>/d0/d1/…/d{N-1}.<title>/d0/…/d{N-1}/up→"../../…"(Nlevels). Both lexically and on disk this resolves tobaseAbs, soensureLinkPathaccepts it andresolveRelToBasesees only real directories in its ancestry.<title>/escape→"d0/…/d{N-1}/up/../../…/<absTarget>"(N..components afterup). Lexically,filepath.Join(baseAbs, "d0/…/up/../…/<absTarget>")cancels theN..againstupplusd{N-2}…d0, yieldingbaseAbs/d0/<absTarget>— inside the root, soensureLinkPathaccepts it.resolveRelToBasethen walksd0/<absTarget-parents>, none of which are symlinks (they don't exist), so the link is created. At the kernel, resolvingbaseAbs/d0/…/upfirst followsupback tobaseAbs, and the remainingN..components climb frombaseAbsto/, then<absTarget>is appended — the symlink points at the attacker-chosen absolute path.<title>/escape(same name).resolveRelToBase("escape")yieldsdir == "."(root cause 2), so noLstatis performed.extractTarDirectory(line 181) callswriteFilewhich opensbaseAbs/escapewithO_TRUNCand noO_NOFOLLOW(root cause 3), writing the attacker's payload through the symlink to<absTarget>.Why v2.6.1's
checkSymlinkEscapedoes not help. The fix for GHSA-8xwf-rjm4-xvhv added a symlink-resolving containment check, but it is called only fromresolveWritePath(content/file/file.goline 632) on thepushFilepath.pushDir→extractTarGzip→extractTarDirectorynever calls it;content/file/utils.gois byte-identical between v2.6.0 and v2.6.1.Suggested remediation. Any of: (a)
Lstatthe final path component before opening for write and reject symlinks; (b) open withO_NOFOLLOW(orO_EXCLfor new files); (c) resolve link targets withfilepath.EvalSymlinkson the deepest existing ancestor (ascheckSymlinkEscapealready does) instead of lexicalfilepath.Join; (d) extract into a fresh empty directory and useopenat2(RESOLVE_BENEATH)/os.Root(Go 1.24+) for all filesystem operations.PoC
Expected output (paths vary):
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 pullto 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:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HReferences
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()inregistry/remote/utils.gofollows theLinkresponse header from a registry without validating the URL's host or scheme. When a malicious registry returns aLinkheader 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()atregistry/remote/utils.go:54callsresp.Request.URL.Parse(link)which, for absolute URLs, returns the absolute URL unchanged. The result is passed directly to the next pagination loop iteration wherehttp.NewRequestWithContext()creates a GET request to the attacker-specified URL. No host comparison, scheme validation, or IP filtering exists betweenparseLinkoutput and the HTTP request.Affected Code Paths
registry/remote/repository.go:Tags()callsparseLink()for next page URLregistry/remote/repository.go:Referrers()callsparseLink()for next page URLregistry/remote/registry.go:Repositories()callsparseLink()for next page URLImpact
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:
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
repo.Tags()/repo.Referrers()/registry.Repositories()against attacker-controlled registryparseLink()extracts the absolute URL with zero validationPoC
Expand PoC
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:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
oras-gotar extraction: Hardlink entry with relative Linkname escapes extract dir via process CWD resolutionCVE-2026-50163 / GHSA-fxhp-mv3v-67qp / GO-2026-5880
More information
Details
Root cause
The tar-extraction helper
ensureLinkPathatcontent/file/utils.go:262-275validates that a hardlink's target resolves inside the extract base, but then returns the original unresolvedtargetstring back to the caller:The caller for
TypeLinkhardlinks then does:os.Link(oldname, newname)wraps thelink(2)system call. From thelink(2)man page:So when
target(i.e.,header.Linkname) is a relative path,os.Linkresolves it against the process's current working directory, not againstfilepath.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:payload.tar.gz/README.txt.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 tellsoras-goto auto-extract).When a victim runs
oras pull(or any Go code usingcontent.File), the extraction:payload.tar.gz/evil_cwd_link— passes.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.target = "victim.secret"(NOTpath).os.Link("victim.secret", "<extract_base>/payload.tar.gz/evil_cwd_link").link(2)resolves relativeoldname="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
orasCLI v1.3.0 (SHA-256040e140304b7dbdd9b40dacd798e2303cea44ad84eeb210750afdf15f1dcf8b4, 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:
Observed output:
A library-level regression test is also provided (
poc_test.go) that drops intocontent/file/utils_test.goand runs viago 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
orasCLI or any Go program usingoras-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:
oras pullruns 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.oras-goto fetch artifacts; their CWD is typically/or/root— very sensitive.oras-goto 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):tarDirectoryin 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.TypeSymlink):os.Symlinkstores the target string verbatim and does not CWD-resolve at creation time. The currentensureLinkPathreturn-of-targetis 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 (permay_linkat()in the kernel). Verified on Ubuntu 24.04: as non-root,ln /etc/passwd /tmp/xreturnsEPERM, and the same via the oras PoC path returnslink 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 invokesoras pullas a regular user. The attack surface depends on the invocation context:oras pullrun by a regular user.env,.git/config,.aws/credentials,~/.ssh/config, project-local secrets, CI workspace files.oras pullrun as root (systemd withoutUser=, container entrypoint default root, Kubernetes operator)/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
ensureLinkPathto expose both the verbatim target (for symlinks) and the resolved absolute path (for hardlinks); have theTypeLinkcase use the resolved path.Regression test to add:
Extend
Test_extractTarDirectory_HardLinkwith a third sub-test that:t.TempDir()(or an explicitlyos.Chdir-entered directory) with a known name, e.g.sentinel.txt.TypeLinkentry withLinkname: "sentinel.txt"(relative).extractTarDirectoryreturned an error, OR the resulting hardlink's inode does NOT match the sentinel's inode.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:NReferences
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()inregistry/remote/utils.gofollows theLinkresponse header from a registry without validating the URL's host or scheme. When a malicious registry returns aLinkheader 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()atregistry/remote/utils.go:54callsresp.Request.URL.Parse(link)which, for absolute URLs, returns the absolute URL unchanged. The result is passed directly to the next pagination loop iteration wherehttp.NewRequestWithContext()creates a GET request to the attacker-specified URL. No host comparison, scheme validation, or IP filtering exists betweenparseLinkoutput and the HTTP request.Affected Code Paths
registry/remote/repository.go:Tags()callsparseLink()for next page URLregistry/remote/repository.go:Referrers()callsparseLink()for next page URLregistry/remote/registry.go:Repositories()callsparseLink()for next page URLImpact
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:
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
repo.Tags()/repo.Referrers()/registry.Repositories()against attacker-controlled registryparseLink()extracts the absolute URL with zero validationPoC
Expand PoC
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:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:NReferences
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.Storein oras-go v2 unpacks OCI layer tarballs when a descriptor carriesio.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-symlinkLstatwalk. 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 defaultAllowPathTraversalOnWrite=falseconfiguration — a canonical tar-slip → RCE primitive.Details
Affected versions:
<= v2.6.1Entry point:
content/file/file.goline 486,(*Store).pushDir— reached from(*Store).Pushfor any descriptor whose annotations includeio.deis.oras.content.unpack: "true"(i.e.file.AnnotationUnpack) and anorg.opencontainers.image.title.oras.Copyfrom a remote registry into afile.New(dir)store invokes this per layer.Root cause 1 — lexical link validation.
content/file/utils.golines 264–275,ensureLinkPath:filepath.Joincleans..components textually and does not dereference symlinks in intermediate components. It therefore cannot detect that a component oftargetis 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.golines 247–257, insideresolveRelToBase:For an entry named
<title>/escape,path == "escape"andfilepath.Dir("escape") == ".", so the loop body never executes — the entry itself is neverLstat-checked.Root cause 3 — write follows symlinks.
content/file/utils.goline 279,writeFile:No
O_NOFOLLOW, so ifpathis a symlink the write goes to its target.Data flow / exploit construction. Let
baseAbs = <workingDir>/<title>andN = depth(baseAbs)(number of path components from/). The attacker's tar.gz contains, in order:Nnested directories<title>/d0/d1/…/d{N-1}.<title>/d0/…/d{N-1}/up→"../../…"(Nlevels). Both lexically and on disk this resolves tobaseAbs, soensureLinkPathaccepts it andresolveRelToBasesees only real directories in its ancestry.<title>/escape→"d0/…/d{N-1}/up/../../…/<absTarget>"(N..components afterup). Lexically,filepath.Join(baseAbs, "d0/…/up/../…/<absTarget>")cancels theN..againstupplusd{N-2}…d0, yieldingbaseAbs/d0/<absTarget>— inside the root, soensureLinkPathaccepts it.resolveRelToBasethen walksd0/<absTarget-parents>, none of which are symlinks (they don't exist), so the link is created. At the kernel, resolvingbaseAbs/d0/…/upfirst followsupback tobaseAbs, and the remainingN..components climb frombaseAbsto/, then<absTarget>is appended — the symlink points at the attacker-chosen absolute path.<title>/escape(same name).resolveRelToBase("escape")yieldsdir == "."(root cause 2), so noLstatis performed.extractTarDirectory(line 181) callswriteFilewhich opensbaseAbs/escapewithO_TRUNCand noO_NOFOLLOW(root cause 3), writing the attacker's payload through the symlink to<absTarget>.Why v2.6.1's
checkSymlinkEscapedoes not help. The fix for GHSA-8xwf-rjm4-xvhv added a symlink-resolving containment check, but it is called only fromresolveWritePath(content/file/file.goline 632) on thepushFilepath.pushDir→extractTarGzip→extractTarDirectorynever calls it;content/file/utils.gois byte-identical between v2.6.0 and v2.6.1.Suggested remediation. Any of: (a)
Lstatthe final path component before opening for write and reject symlinks; (b) open withO_NOFOLLOW(orO_EXCLfor new files); (c) resolve link targets withfilepath.EvalSymlinkson the deepest existing ancestor (ascheckSymlinkEscapealready does) instead of lexicalfilepath.Join; (d) extract into a fresh empty directory and useopenat2(RESOLVE_BENEATH)/os.Root(Go 1.24+) for all filesystem operations.PoC