GHSA-9HQ4-HM3J-JMPH
Vulnerability from github – Published: 2026-11-09 22:37 – Updated: 2026-11-09 22:37Summary
The 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.
The defect (the symlink check is a no-op for absolute targets)
CVE-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:
if filepath.IsAbs(file.Linkname) {
resolvedTarget = filepath.Join(dest, file.Linkname) // line 298: absolute target REBASED into dest
} else {
resolvedTarget = filepath.Join(fullPathDir, file.Linkname)
}
if !strings.HasPrefix(filepath.Clean(resolvedTarget)+sep, filepath.Clean(dest)+sep) { // line 302
return ...escapes destination... // line 303
}
_ = os.Remove(fullPath) // line 307
err = os.Symlink(file.Linkname, fullPath) // line 310: writes the RAW Linkname
For 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.
Write-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 (the exact remote-add-on vector the advisory names), and via ddev import-db / import-files. GitHub codeload tarballs preserve symlinks.
Verification
EXECUTED (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).
Suggested fix
Reject 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.
Tooling
AI 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).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/ddev/ddev"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.25.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-108262"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-09T22:37:58Z",
"nvd_published_at": "2026-11-06T19:17:06Z",
"severity": "HIGH"
},
"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 \u003crepo|url\u003e (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\u0027s 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\u0027s 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).",
"id": "GHSA-9hq4-hm3j-jmph",
"modified": "2026-11-09T22:37:59Z",
"published": "2026-11-09T22:37:58Z",
"references": [
{
"type": "WEB",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "DDEV incomplete tar symlink traversal fix allows host file overwrite - Incomplete fix of CVE-2026-32885"
}
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.
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
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
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.
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.