{"id":"CVE-2026-108262","aliases":["GHSA-9hq4-hm3j-jmph"],"url":"https://o3.security/vulnerability/CVE-2026-108262","summary":"DDEV incomplete tar symlink traversal fix allows host file overwrite - Incomplete fix of CVE-2026-32885","details":"## Summary\n\nThe CVE-2026-32885 ZipSlip fix added a symlink-target escape check to Untar, but the check rebases an ABSOLUTE symlink target into dest before validating (filepath.Join(dest, \"/etc\") stays lexically inside dest and passes), while the symlink is then created with the RAW absolute target via os.Symlink(file.Linkname, ...), so a malicious archive plants a host-absolute symlink and a following regular-file entry writes through it, restoring arbitrary host file create/overwrite (reaching $HOME rc-files, hence RCE). Confirmed at HEAD.\n\n## The defect (the symlink check is a no-op for absolute targets)\n\nCVE-2026-32885 (fixed v1.25.2, commit 05cbe299) added a lexical containment check on the entry path and a NEW symlink-target validation. pkg/archive/archive.go, Untar, tar.TypeSymlink case:\n\n```go\nif filepath.IsAbs(file.Linkname) {\n    resolvedTarget = filepath.Join(dest, file.Linkname)   // line 298: absolute target REBASED into dest\n} else {\n    resolvedTarget = filepath.Join(fullPathDir, file.Linkname)\n}\nif !strings.HasPrefix(filepath.Clean(resolvedTarget)+sep, filepath.Clean(dest)+sep) {  // line 302\n    return ...escapes destination...                       // line 303\n}\n_ = os.Remove(fullPath)                                     // line 307\nerr = os.Symlink(file.Linkname, fullPath)                  // line 310: writes the RAW Linkname\n```\n\nFor an absolute Linkname like /Users/victim, line 298 computes filepath.Join(dest, \"/Users/victim\") = dest/Users/victim (lexically inside dest), so the escape check at line 302 PASSES. But line 310 creates the symlink with the raw file.Linkname, so the on-disk symlink dest/link points at the real host path /Users/victim. The value validated (rebased) is not the value written (raw): a check-one-thing / do-another divergence.\n\nWrite-through: the archive carries a second regular-file entry whose Name traverses the planted link (e.g. link/.zshrc). Its fullPath is lexically inside dest so the line-238 check passes (that check is purely lexical and does not resolve the symlink), then os.Create follows the planted symlink and overwrites /Users/victim/.zshrc on the host. Overwriting a shell rc-file, git hook, or cron entry yields code execution. Reachable via ddev add-on get <repo|url> (the exact remote-add-on vector the advisory names), and via ddev import-db / import-files. GitHub codeload tarballs preserve symlinks.\n\n## Verification\n\nEXECUTED (go test driving the real Untar): a file was written OUTSIDE dest and Untar returned nil (VULNERABLE); a pre-existing host file was truncated and overwritten with the attacker payload; the negative controls (a relative ../ symlink escape, and a plain ../ file entry) are both correctly rejected, isolating the bug to the absolute-symlink branch. NOT verified: the end-to-end add-on get over the network (the caller chain is source-confirmed); Windows (reliable on macOS/Linux, DDEV's primary hosts). Unzip is NOT affected (tar-only).\n\n## Suggested fix\n\nReject absolute symlink targets outright (a dev-environment archive has no need to plant host-absolute symlinks), or validate the RAW target that will actually be written rather than a rebased copy. Additionally, resolve symlinks when validating each regular-file entry's parent so a write cannot traverse a previously-planted link (or extract to a staging dir and reject any entry whose realpath escapes). The lexical line-238 check does not defend against a planted symlink.\n\n## Tooling\n\nAI assistance was used while investigating. The write-through is executed-verified with negative controls (an absolute-symlink entry plus a follow-up file entry wrote and overwrote files OUTSIDE dest while relative-escape and plain-ZipSlip controls were correctly rejected), and I confirmed at HEAD that Untar rebases an absolute symlink target into dest for the escape check (line 298) but creates the symlink with the raw absolute target (line 310).","published":"2026-11-06T18:29:49.225Z","modified":"2026-11-08T02:47:42.601185078Z","cvss":{"score":6.5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Go","name":"github.com/ddev/ddev","fixedVersion":"1.25.4"}],"fix":{"url":"https://github.com/ddev/ddev/pull/8625","label":"ddev/ddev#8625"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/108xxx/CVE-2026-108262.json"},{"type":"ADVISORY","url":"https://github.com/ddev/ddev/security/advisories/GHSA-9hq4-hm3j-jmph"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-108262"},{"type":"WEB","url":"https://github.com/ddev/ddev/pull/8625"},{"type":"WEB","url":"https://github.com/ddev/ddev/commit/7034963403c5374fecceb02070d27b335329cede"},{"type":"PACKAGE","url":"https://github.com/ddev/ddev"},{"type":"WEB","url":"https://github.com/ddev/ddev/releases/tag/v1.25.4"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-08T02:47:42.601185078Z"}}