Nov 10, 2026·10 min read·1 visit
An incomplete fix for CVE-2026-32885 in DDEV versions 1.25.2 and 1.25.3 allows malicious tar archives containing absolute symlink targets to bypass extraction directory containment, resulting in arbitrary file overwrite on the host filesystem.
CVE-2026-108262 (GHSA-9hq4-hm3j-jmph) is a path traversal and arbitrary file overwrite vulnerability in the tar archive extraction routine of DDEV versions 1.25.2 and 1.25.3. Resulting from an incomplete fix for CVE-2026-32885, the Untar function in pkg/archive/archive.go validates absolute symbolic link targets by rebasing them against the extraction destination directory, but subsequently creates the symbolic link on disk using the raw, unvalidated absolute path. When subsequent entries in the archive write through the planted symbolic link, the host operating system resolves the link directly to the host filesystem, allowing an attacker to overwrite arbitrary files with the privileges of the user executing DDEV.
DDEV is an open-source tool designed to provision and manage local PHP and Node.js development environments using Docker containers. As part of its core workflow, DDEV frequently ingests and extracts compressed tar archives (.tar, .tar.gz, .tar.bz2, .tar.xz) on the host machine. These operations occur during database imports (ddev import-db), static asset imports (ddev import-files), and third-party add-on installations (ddev get). The archive extraction logic is centralized within the Untar function located in pkg/archive/archive.go.
Because archive extraction runs directly on the host operating system rather than inside an isolated container, pkg/archive/archive.go exposes a direct host filesystem attack surface when processing untrusted archives. To maintain security, the extraction routine must strictly confine all extracted files, directories, and symbolic links within the designated target directory (dest). Failure to enforce boundary checks during archive extraction exposes the host system to path traversal attacks.
CVE-2026-108262 (also tracked as GHSA-9hq4-hm3j-jmph) is classified under both CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and CWE-59 (Improper Link Resolution Before File Access). The vulnerability affects DDEV versions 1.25.2 and 1.25.3, and was introduced directly by commit 662ea61e3c453e16a25b4630e78417e3087e36c6 as an incomplete remediation for an earlier path traversal vulnerability, CVE-2026-32885.
By crafting a tar archive containing a symbolic link with an absolute target path followed by a regular file entry that traverses that symbolic link, an attacker can write or overwrite arbitrary files anywhere on the host filesystem where the DDEV user has write permissions. Notably, the Unzip functionality within the same package is unaffected by this flaw.
The root cause of CVE-2026-108262 is a state discrepancy between the path value validated during the security check and the path value passed to the operating system's symlink creation syscall. When remediating CVE-2026-32885 in DDEV v1.25.2, the developers implemented validation logic inside Untar() to prevent symbolic links from pointing outside the extraction root dest. However, the implementation attempted to preserve compatibility with container-originating archives that might contain absolute symlink targets (such as /var/www/html).
When Untar() encounters a tar header of type tar.TypeSymlink, it inspects the link target stored in file.Linkname. In vulnerable versions, the code checks if file.Linkname is an absolute path using filepath.IsAbs(file.Linkname). If filepath.IsAbs returns true, the function constructs a synthetic validation path by joining the destination directory and the absolute link target: resolvedTarget = filepath.Join(dest, file.Linkname).
In Go's path/filepath library, filepath.Join concatenates path elements and cleans the result, stripping leading slashes from subsequent arguments. Consequently, if dest is /tmp/extract and file.Linkname is /home/victim/.ssh, filepath.Join evaluates resolvedTarget to /tmp/extract/home/victim/.ssh. When DDEV subsequently verifies whether resolvedTarget begins with the prefix /tmp/extract/, the validation check unconditionally succeeds for any absolute path that does not contain excessive .. segments.
Following this validation step, Untar() creates the symbolic link on disk by invoking os.Symlink(file.Linkname, fullPath). Rather than passing the rebased resolvedTarget variable that was just validated, the code passes the raw, unmodified file.Linkname. This plants a live symbolic link inside the extraction directory pointing directly to the absolute host path /home/victim/.ssh.
Once the absolute symbolic link is planted on the filesystem, the second stage of the vulnerability—symlink write-through traversal—is possible. When the tar reader processes a subsequent regular file entry (tar.TypeReg) located at link/authorized_keys, the lexical path validation checks the string <dest>/link/authorized_keys. Because this string lexically resides under <dest>/, the lexical check passes. When os.OpenFile or os.Create is subsequently invoked on <dest>/link/authorized_keys, the operating system kernel dereferences the link directory component, writing the attacker's payload directly to the host path /home/victim/.ssh/authorized_keys.
The vulnerable code resides in pkg/archive/archive.go within the Untar function. In DDEV 1.25.2 and 1.25.3, the tar.TypeSymlink switch case handled path resolution and symlink creation as follows:
// Vulnerable logic in pkg/archive/archive.go (v1.25.2 - v1.25.3)
var resolvedTarget string
if filepath.IsAbs(file.Linkname) {
// BUG 1: filepath.Join strips the leading slash from file.Linkname,
// rebasing the path under 'dest' strictly in memory.
resolvedTarget = filepath.Join(dest, file.Linkname)
} else {
resolvedTarget = filepath.Join(fullPathDir, file.Linkname)
}
// The prefix check validates 'resolvedTarget' (<dest>/<file.Linkname>),
// which always passes for simple absolute paths like "/etc" or "/Users/admin".
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)
// BUG 2: os.Symlink uses the raw, unvalidated 'file.Linkname' instead of 'resolvedTarget'.
err = os.Symlink(file.Linkname, fullPath)
if err != nil {
return fmt.Errorf("failed to create symlink %v -> %v, err: %v", fullPath, file.Linkname, err)
}In commit 7034963403c5374fecceb02070d27b335329cede (merged via PR #8625 in commit 5da91aeb9ebab0b0e66171c450b72099308d332c), the DDEV maintainers eliminated the rebasing mechanism entirely. They determined that archives processed on the host have no legitimate reason to establish absolute symbolic links.
// Patched logic in pkg/archive/archive.go (v1.25.4)
func isArchiveAbsolutePath(p string) bool {
// Explicitly check POSIX '/', Windows '\', and host-native filepath.IsAbs
return strings.HasPrefix(p, "/") || strings.HasPrefix(p, `\`) || filepath.IsAbs(p)
}
// Inside Untar() -> case tar.TypeSymlink:
if isArchiveAbsolutePath(file.Linkname) {
return fmt.Errorf("symlink target %q in archive entry %q is absolute, which is not allowed", file.Linkname, file.Name)
}
// Only relative targets are permitted and are resolved against fullPathDir
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)
}
_ = os.Remove(fullPath)
err = os.Symlink(file.Linkname, fullPath)
if err != nil {
return fmt.Errorf("failed to create symlink %v -> %v, err: %v", fullPath, file.Linkname, err)
}A critical aspect of the patch in commit 7034963403c5374fecceb02070d27b335329cede is the introduction of the helper function isArchiveAbsolutePath(p string). Relying solely on Go's standard filepath.IsAbs(p) is insufficient for cross-platform archive extraction because filepath.IsAbs is host-OS dependent. On Windows hosts, filepath.IsAbs("/Users/victim") or filepath.IsAbs("\\Users\\victim") returns false because a drive letter or UNC share is absent, yet passing such a path to os.Symlink on Windows still creates a symlink rooted at the current drive (e.g., C:\Users\victim). By explicitly rejecting leading forward slashes (/), leading backslashes (\), and any path matching filepath.IsAbs(p), the patch prevents cross-platform bypasses.
However, it is worth examining whether relative symlinks can still be chained or abused. Because isArchiveAbsolutePath blocks all absolute targets, file.Linkname is guaranteed to be relative. When filepath.Join(fullPathDir, file.Linkname) is evaluated, filepath.Clean collapses any ../ sequences relative to fullPathDir, and the strings.HasPrefix check ensures the resolved target remains strictly inside dest. Provided that fullPathDir itself has not already traversed outside dest via an earlier symlink, the containment holds.
Exploiting CVE-2026-108262 requires an attacker to supply a crafted tar archive (.tar, .tar.gz, .tar.bz2, or .tar.xz) to a user running DDEV version 1.25.2 or 1.25.3. No elevated privileges on the target machine are required, though user interaction is necessary to trigger the archive extraction operation. Common delivery vectors include publishing a malicious third-party DDEV add-on retrieved via ddev get <attacker-repo>, or supplying a trojanized project database/asset dump imported via ddev import-db or ddev import-files.
Constructing the exploit archive requires two sequential tar entries:
Typeflag: tar.TypeSymlink, an arbitrary relative name within the archive (e.g., Name: "link"), and Linkname set to the target absolute directory on the host filesystem (e.g., /home/username/.ssh or /Users/username).Typeflag: tar.TypeReg whose Name traverses the symlink created in the first entry (e.g., Name: "link/authorized_keys" or Name: "link/.zshrc"), followed by the attacker's file content.The official patch commit includes a complete proof-of-concept regression test in pkg/archive/archive_test.go that demonstrates the exact exploitation mechanics:
// Proof-of-Concept from pkg/archive/archive_test.go
t.Run("absolute_symlink_write_through_escape", func(t *testing.T) {
victimDir := t.TempDir()
victimFile := filepath.Join(victimDir, "pwned.txt")
require.NoError(t, os.WriteFile(victimFile, []byte("original"), 0644))
f, err := os.CreateTemp("", "absolute_symlink_write_through_escape_*.tar")
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(f.Name()) })
tw := tar.NewWriter(f)
// Step 1: Plant symlink 'link' pointing to absolute host path 'victimDir'
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: "link",
Typeflag: tar.TypeSymlink,
Linkname: victimDir,
Mode: 0777,
}))
// Step 2: Write regular file through 'link/pwned.txt'
content := []byte("OWNED-BY-ARCHIVE")
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: "link/pwned.txt",
Typeflag: tar.TypeReg,
Size: int64(len(content)),
Mode: 0644,
}))
_, err = tw.Write(content)
require.NoError(t, err)
require.NoError(t, tw.Close())
require.NoError(t, f.Close())
writeThroughDest := testcommon.CreateTmpDir("absolute_symlink_write_through_escape")
t.Cleanup(func() { _ = os.RemoveAll(writeThroughDest) })
// On DDEV 1.25.2 and 1.25.3, this overwrites victimFile on the host
err = archive.Untar(f.Name(), writeThroughDest, "")
})> [!NOTE]
> In practical attack scenarios where the attacker does not know the exact username of the target developer, the archive can include multiple symlink/file pairs targeting common paths (e.g., /root/.bashrc, /home/ubuntu/.bashrc, /var/spool/cron/crontabs/...), or target project-level paths if the workspace directory structure is predictable.
The direct technical impact of CVE-2026-108262 is arbitrary file creation and overwrite on the host filesystem with the privileges of the local user running the ddev CLI. Because DDEV runs as a standard user application on the developer's workstation rather than inside a restricted sandbox during initial archive extraction, the scope of writable locations encompasses the entire user home directory, project repositories, SSH configurations, and local credential stores.
An attacker who successfully exploits this vulnerability can escalate from arbitrary file write to remote code execution (RCE) and persistence on the developer's host machine. Specifically, overwriting shell initialization files (~/.bashrc, ~/.zshrc, ~/.profile), SSH configuration files (~/.ssh/authorized_keys, ~/.ssh/config), or local Git hooks (.git/hooks/pre-commit, .git/hooks/post-checkout) guarantees code execution the next time the user opens a terminal, connects via SSH, or runs a Git command.
The official CVSS v3.1 score is 6.5 (Medium) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N. The score reflects high integrity impact (I:H) due to unrestricted filesystem modification, low attack complexity (AC:L), and required user interaction (UI:R) to initiate the import or add-on installation command. Confidentiality and availability are scored as None (C:N/A:N) in the base vector because the vulnerability primitive itself is strictly a file write mechanism, though secondary post-exploitation effects routinely compromise confidentiality.
From a threat intelligence perspective, the EPSS score for CVE-2026-108262 is 0.00306 (23.60th percentile), indicating a low probability of widespread automated exploitation in the wild. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and no public weaponized exploits currently exist on ExploitDB or PacketStorm.
To remediate CVE-2026-108262, users and administrators must upgrade DDEV to version 1.25.4 or later. Version 1.25.4 incorporates commit 7034963403c5374fecceb02070d27b335329cede, which strictly rejects any tar symlink entry containing an absolute target path.
Developers can verify their currently installed DDEV version by running the following command in their terminal:
ddev --versionIf the output indicates version v1.25.2 or v1.25.3, upgrade immediately using the appropriate package manager for the host operating system (such as Homebrew on macOS/Linux, apt/rpm repositories on Linux, or Winget/Chocolatey on Windows).
For environments where an immediate upgrade to v1.25.4 is not possible, users must avoid running ddev get, ddev import-db, or ddev import-files against untrusted or externally sourced tar archives. Before importing any archive on a vulnerable version, operators can manually inspect the archive's table of contents to verify that no symbolic links point to absolute paths:
# Inspect tar archive for symlink entries (marked with leading 'l' in permissions)
tar -tvf archive.tar.gz | grep -E '^l'If any entry in the output displays a link target beginning with /, \, or a Windows drive letter (e.g., link -> /etc/passwd or link -> C:\Users), the archive must be rejected and deleted immediately.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
ddev DDEV | >= 1.25.2, < 1.25.4 | 1.25.4 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Path Traversal / Arbitrary File Overwrite (CWE-22, CWE-59) |
| Affected Component | pkg/archive/archive.go (Untar function) |
| Affected Versions | DDEV >= 1.25.2, < 1.25.4 (v1.25.2, v1.25.3) |
| CVSS v3.1 Score | 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N) |
| EPSS Score | 0.31% (23.60th Percentile) |
| Exploit Status | Proof-of-Concept Available (Unit Test in Patch) |
| CISA KEV Status | Not Listed |
| Patched Version | DDEV 1.25.4 |
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVE-2026-119298 (GHSA-x89h-7h96-v88f) is a file overwrite confirmation bypass vulnerability in schollz/croc prior to version 11.5.2. The receive-side file validation logic in src/croc/croc.go improperly exempted any incoming file whose basename began with the prefix 'croc-stdin-' from interactive overwrite prompts and automatic collision renaming, failing to verify if the transfer session was actually an internal text transfer. An unauthenticated remote peer participating in a transfer session can exploit this flaw to silently overwrite, truncate, or replace matching pre-existing files in the receiver's working directory.
Claude Code prior to version 2.1.251 contains a Time-of-Check to Time-of-Use (TOCTOU) race condition in its internal file manipulation tools (Edit, Write, Read). Because path validation was performed using string-based pathname and symbolic link resolution without binding to the underlying directory inodes or open file descriptors, a process running concurrently inside the macOS Bash sandbox can atomically replace an in-workspace directory with a symbolic link after validation passes. This causes the unsandboxed main process to follow the symlink and overwrite user-writable files outside the permitted workspace boundary.
CVE-2026-88982 (GHSA-2xwh-426q-42c5) is an authorization bypass and data integrity vulnerability affecting Kubeflow Pipelines versions prior to 2.18.0. The flaw resides in the legacy V1 and raw-Argo execution caching mutating admission webhook and the associated cache-server storage backend. In multi-tenant deployments where namespaces share a centralized cache service, execution cache keys were derived solely from container specification fields without incorporating the workload's Kubernetes namespace. Additionally, the cache watcher trusted mutable pod annotations when persisting execution outputs. Consequently, an authenticated user with workload submission privileges in one tenant namespace can poison cached step outputs consumed by other tenants or read cached outputs generated by workloads in separate namespaces.
CVE-2026-88983 is an authenticated Server-Side Request Forgery (SSRF) vulnerability affecting the Kubeflow Pipelines API server in versions prior to 2.17.0. When an authenticated user creates a pipeline or pipeline version using a remote specification URI, the backend server retrieves the document via Go's default HTTP client without validating the target scheme, hostname, port, or IP address. Because the API server reflects remote connection errors and HTTP status codes in its response, an attacker with pipeline creation permissions can exploit the server as a status-code and port-scanning oracle against Kubernetes cluster-internal services, private networks, and cloud metadata endpoints.
CVE-2026-88985 is a SQL injection vulnerability (CWE-89) in the Kubeflow Pipelines (KFP) API server prior to version 2.17.0. The flaw stems from the untrusted deserialization of base64-encoded JSON pagination tokens (page_token) and the direct concatenation of metric names during run list sorting. Authenticated users with permissions to invoke REST or gRPC list endpoints can inject arbitrary SQL syntax into structural query identifiers and JOIN predicates, enabling unauthorized cross-tenant read access to all database records accessible by the API server.
A reflected Cross-Site Scripting (XSS) vulnerability exists in the Kubeflow Pipelines frontend server prior to version 2.15.0. Specifically, the Express route handlers for '/pipeline/k8s/pod' and '/pipeline/k8s/pod/events' reflect unvalidated 'podname' and 'podnamespace' query parameters, along with backend Kubernetes API error messages, directly into HTML-capable HTTP error responses. An unauthenticated attacker can exploit this flaw by convincing an authenticated user to visit a crafted URL, resulting in arbitrary JavaScript execution within the context of the Kubeflow Pipelines frontend origin.