CVE-2026-32885
Description
DDEV is an open-source tool for running local web development environments for PHP and Node.js. Versions prior to 1.25.2 have unsanitized extraction in both Untar() and Unzip() functions in pkg/archive/archive.go. Downloads and extracts archives from remote sources without path validation. Version 1.25.2 patches the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
DDEV before 1.25.2 has a ZipSlip path traversal in Untar and Unzip, enabling extraction outside intended directories.
Vulnerability
Overview DDEV versions prior to 1.25.2 contain unsanitized extraction in both the Untar() and Unzip() functions in pkg/archive/archive.go [2]. The functions download and extract archives from remote sources without validating that file paths remain within the intended destination directory, allowing a ZipSlip-style path traversal attack.
Attack
Vector The affected functions are used in ddev import-db, ddev import-files, and all CMS-specific file import handlers [3]. A crafted archive containing entries with ../ sequences or symlink targets pointing outside the destination directory can write files to arbitrary locations on the filesystem. The attack is realistic when a developer receives a database dump or file archive from an untrusted or compromised source. No authentication is required beyond the ability to invoke the import commands.
Impact
An attacker can achieve arbitrary file write by including path traversal elements in archive entries. This could lead to overwriting critical system files, injecting malicious code (e.g., a web shell), or otherwise compromising the host system. The vulnerability is similar to classic ZipSlip attacks.
Mitigation
The issue is fixed in DDEV version 1.25.2 [1]. The fix adds path containment checks in both Untar() and Unzip() to reject any entry that escapes the destination directory [3]. Users are advised to upgrade immediately. No workarounds are provided for older versions; archives that previously would have been extracted with traversal will now return an error.
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/ddev/ddevGo | < 1.25.2 | 1.25.2 |
Affected products
3Patches
105cbe299770afix: prevent path traversal (ZipSlip) in Untar and Unzip (#8213)
2 files changed · +112 −0
pkg/archive/archive.go+24 −0 modified@@ -234,6 +234,11 @@ func Untar(source string, dest string, extractionDir string) error { fullPath := filepath.Join(dest, file.Name) + // Prevent path traversal (ZipSlip): ensure fullPath stays within dest + if !strings.HasPrefix(filepath.Clean(fullPath)+string(os.PathSeparator), filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("archive entry %q escapes destination directory", file.Name) + } + // Handle directories, regular files, and symlinks switch file.Typeflag { case tar.TypeDir: @@ -284,6 +289,20 @@ func Untar(source string, dest string, extractionDir string) error { return fmt.Errorf("failed to create the directory %s, err: %v", fullPathDir, err) } + // Validate symlink target doesn't escape dest. + // Absolute targets are rebased against dest (treating dest as root), + // so container paths like /var/www/html/... are accepted. + // Relative targets are resolved against the symlink's parent directory. + var resolvedTarget string + if filepath.IsAbs(file.Linkname) { + resolvedTarget = filepath.Join(dest, file.Linkname) + } else { + resolvedTarget = filepath.Join(fullPathDir, file.Linkname) + } + if !strings.HasPrefix(filepath.Clean(resolvedTarget)+string(os.PathSeparator), filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("symlink target %q in archive entry %q escapes destination directory", file.Linkname, file.Name) + } + // Remove any existing file/symlink at this path _ = os.Remove(fullPath) @@ -341,6 +360,11 @@ func Unzip(source string, dest string, extractionDir string) error { fullPath := filepath.Join(dest, file.Name) + // Prevent path traversal (ZipSlip): ensure fullPath stays within dest + if !strings.HasPrefix(filepath.Clean(fullPath)+string(os.PathSeparator), filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("archive entry %q escapes destination directory", file.Name) + } + if strings.HasSuffix(file.Name, "/") { err = os.MkdirAll(fullPath, 0777) if err != nil {
pkg/archive/archive_test.go+88 −0 modified@@ -2,6 +2,7 @@ package archive_test import ( "archive/tar" + "archive/zip" "compress/gzip" "io" "io/fs" @@ -174,6 +175,93 @@ func TestDownloadAndExtractTarball(t *testing.T) { require.NoDirExists(t, dir) } +// TestUntarPathTraversal verifies that path traversal attempts in tar archives are rejected +func TestUntarPathTraversal(t *testing.T) { + destDir := testcommon.CreateTmpDir(t.Name()) + t.Cleanup(func() { _ = os.RemoveAll(destDir) }) + + buildTar := func(entryName string, linkname string, typeflag byte) string { + f, err := os.CreateTemp("", t.Name()+"_*.tar.gz") + require.NoError(t, err) + t.Cleanup(func() { _ = os.Remove(f.Name()) }) + + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + + hdr := &tar.Header{ + Name: entryName, + Typeflag: typeflag, + Linkname: linkname, + Mode: 0644, + Size: 0, + } + if typeflag == tar.TypeReg { + hdr.Size = 5 + } + require.NoError(t, tw.WriteHeader(hdr)) + if typeflag == tar.TypeReg { + _, err = tw.Write([]byte("hello")) + require.NoError(t, err) + } + require.NoError(t, tw.Close()) + require.NoError(t, gw.Close()) + require.NoError(t, f.Close()) + return f.Name() + } + + t.Run("traversal_in_file_path", func(t *testing.T) { + tarball := buildTar("../../traversal_file.txt", "", tar.TypeReg) + err := archive.Untar(tarball, destDir, "") + require.Error(t, err) + require.Contains(t, err.Error(), "escapes destination directory") + }) + + t.Run("traversal_in_symlink_target", func(t *testing.T) { + tarball := buildTar("link.txt", "../../outside.txt", tar.TypeSymlink) + err := archive.Untar(tarball, destDir, "") + require.Error(t, err) + require.Contains(t, err.Error(), "escapes destination directory") + }) + + t.Run("absolute_symlink_target_with_traversal", func(t *testing.T) { + // Absolute path with .. traversal that escapes dest even when rebased + tarball := buildTar("link.txt", "/../../../etc/passwd", tar.TypeSymlink) + err := archive.Untar(tarball, destDir, "") + require.Error(t, err) + require.Contains(t, err.Error(), "escapes destination directory") + }) + + t.Run("absolute_symlink_container_path_allowed", func(t *testing.T) { + // Absolute container paths like /var/www/html/... should be allowed + tarball := buildTar("link.txt", "/var/www/html/lib/web/underscore.js", tar.TypeSymlink) + err := archive.Untar(tarball, destDir, "") + require.NoError(t, err) + }) +} + +// TestUnzipPathTraversal verifies that path traversal attempts in zip archives are rejected +func TestUnzipPathTraversal(t *testing.T) { + destDir := testcommon.CreateTmpDir(t.Name()) + t.Cleanup(func() { _ = os.RemoveAll(destDir) }) + + // Build a zip with a traversal entry + zipFile, err := os.CreateTemp("", t.Name()+"_*.zip") + require.NoError(t, err) + t.Cleanup(func() { _ = os.Remove(zipFile.Name()) }) + + zw := zip.NewWriter(zipFile) + w, err := zw.Create("../../traversal_file.txt") + require.NoError(t, err) + _, err = w.Write([]byte("pwned")) + require.NoError(t, err) + require.NoError(t, zw.Close()) + require.NoError(t, zipFile.Close()) + + err = archive.Unzip(zipFile.Name(), destDir, "") + require.Error(t, err) + require.Contains(t, err.Error(), "escapes destination directory") +} + // TestUntarSymlinks tests that symlinks are properly extracted from tarballs func TestUntarSymlinks(t *testing.T) { assert := asrt.New(t)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/ddev/ddev/security/advisories/GHSA-x2xq-qhjf-5mvgnvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-x2xq-qhjf-5mvgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32885ghsaADVISORY
- github.com/ddev/ddev/commit/05cbe299770a590b89bfc8dddab33e61b4302e43ghsaWEB
- github.com/ddev/ddev/pull/8213ghsaWEB
- github.com/ddev/ddev/releases/tag/v1.25.2nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.