VYPR
Medium severity6.5NVD Advisory· Published Apr 22, 2026· Updated May 11, 2026

CVE-2026-32885

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.

PackageAffected versionsPatched versions
github.com/ddev/ddevGo
< 1.25.21.25.2

Affected products

3
  • Ddev/Ddevreferences3 versions
    (expand)+ 2 more
    • (no CPE)
    • cpe:2.3:a:ddev:ddev:*:*:*:*:*:*:*:*range: <1.25.2
    • (no CPE)range: <1.25.2

Patches

1
05cbe299770a

fix: prevent path traversal (ZipSlip) in Untar and Unzip (#8213)

https://github.com/ddev/ddevRandy FayMar 23, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.