Reported from GHSA-4qxq-p5hm-3q3p
Summary
The fix for CVE-2026-48752 (GHSA-vxp5-584q-c479, template-traversal to host read/write, addressed in 7.2.0) was applied to the LXC container driver only. The sibling QEMU/VM driver's template path (driver_qemu.go → templateApplyNow) never received the same guard, so its template read and write remain unbounded.
An authenticated user who can create/launch a VM from a custom image, or restore a VM backup, controls the image/backup metadata.yaml (the templates map and each tpl.Template value) and can traverse out of the templates directory to:
- read arbitrary host files as root, which are then disclosed into the guest, and
- create root-owned files anywhere on the host (with a
.out suffix).
Details
The LXC driver was hardened for CVE-2026-48752 (internal/server/instance/drivers/driver_lxc.go):
os.OpenRoot(d.RootfsPath()) // ~:7493 confines writes
if filepath.Base(tpl.Template) != tpl.Template // ~:7515 rejects any "/", blocks all "../"
os.Lstat(d.TemplatesPath()) ... ModeSymlink // ~:7519 / ~:7533 rejects symlinked dir/source
The QEMU/VM driver received none of these. driver_qemu.go → templateApplyNow (func @ :3712) has, before its file operations, only a slices.Contains(tpl.When, trigger) check. There is no path validation:
// READ ~:3779 -- raw tpl.Template, no containment
tplString, err := os.ReadFile(filepath.Join(d.TemplatesPath(), tpl.Template))
// WRITE ~:3765 -- raw tpl.Template, path is host-side under incusd (root)
w, err = os.Create(filepath.Join(path, fmt.Sprintf("%s.out", tpl.Template)))
- READ vector:
tpl.Template = "../../../../etc/shadow" reads an arbitrary host file, which is rendered into the VM config drive (config/files/*.out, exported to the guest over 9p/virtiofs) → host-file disclosure to the attacker's guest. Cleanest, exact-path vector.
- WRITE vector:
tpl.Template = "../../../../etc/cron.d/x" creates /etc/cron.d/x.out, a root-owned file anywhere on the host with pongo2-rendered content.
A grep for os.OpenRoot / OpenTemplates over the templates directory in the incus QEMU path returns zero hits. Reachability: templateApplyNow is invoked from generateConfigShare on VM start, so the static call path is strong.
Reference remediation. Apply incus's own LXC-driver guards (the filepath.Base reject + os.OpenRoot confinement) to driver_qemu.go → templateApplyNow, covering both the read and the write. For the read specifically, upstream LXD is a second reference (it confines the QEMU read via d.OpenTemplates(), an os.Root). Note that LXD does not confine the QEMU write either, so the write reference is incus's own LXC guard, not LXD.
Scope / honesty. Source-confirmed via a white-box review of the public repo at fresh HEAD (ccdf811) plus an LXD cross-reference; the guard absence was grep-verified. A live-VM end-to-end PoC is the documented next step. The read is a clean arbitrary host-file disclosure. The write is a genuine root-owned arbitrary-file-create primitive, but the created path is forced to carry a .out suffix, which constrains a direct write-to-RCE, so this is framed as High, not a 9.9 Critical, and no RCE is claimed without a .out-compatible execution path.
PoC
On a self-hosted incus 7.2.0 or later, craft a VM image whose metadata.yaml contains:
templates:
"/x":
template: "../../../../../../tmp/incus-poc"
when:
- start
Launch the VM, then confirm on the host:
ls -l /tmp/incus-poc.out # created by incusd (root) = out-of-templates write
Separately, a tpl.Template of ../../etc/hostname is read and disclosed into the guest config drive (config/files/*.out).
Impact
Path traversal (CWE-22) + link following (CWE-59) in the VM/QEMU template path. An authenticated user able to create/launch a VM from a custom image, or restore a VM backup, can:
- read arbitrary host files as root, disclosed into their guest (e.g.
/etc/shadow), and
- create root-owned files with attacker-controlled content anywhere on the host (
.out-suffixed).
Undisclosed. The authoritative public dissection (Snyk Labs: Newlines, Symlinks, and Arbitrary Writes: Dissecting High-Severity Vulnerabilities in Incus) covers only driver_lxc.go and explicitly notes the VM drivers were not separately audited; no incus issue, PR, or CVE names driver_qemu.go → templateApplyNow.
Reporter: Kingsley Olukanni (GitHub: babakizo420).
Reported from GHSA-4qxq-p5hm-3q3p
Summary
The fix for CVE-2026-48752 (
GHSA-vxp5-584q-c479, template-traversal to host read/write, addressed in 7.2.0) was applied to the LXC container driver only. The sibling QEMU/VM driver's template path (driver_qemu.go→templateApplyNow) never received the same guard, so its template read and write remain unbounded.An authenticated user who can create/launch a VM from a custom image, or restore a VM backup, controls the image/backup
metadata.yaml(thetemplatesmap and eachtpl.Templatevalue) and can traverse out of the templates directory to:.outsuffix).Details
The LXC driver was hardened for CVE-2026-48752 (
internal/server/instance/drivers/driver_lxc.go):The QEMU/VM driver received none of these.
driver_qemu.go→templateApplyNow(func @:3712) has, before its file operations, only aslices.Contains(tpl.When, trigger)check. There is no path validation:tpl.Template = "../../../../etc/shadow"reads an arbitrary host file, which is rendered into the VM config drive (config/files/*.out, exported to the guest over 9p/virtiofs) → host-file disclosure to the attacker's guest. Cleanest, exact-path vector.tpl.Template = "../../../../etc/cron.d/x"creates/etc/cron.d/x.out, a root-owned file anywhere on the host with pongo2-rendered content.A grep for
os.OpenRoot/OpenTemplatesover the templates directory in the incus QEMU path returns zero hits. Reachability:templateApplyNowis invoked fromgenerateConfigShareon VM start, so the static call path is strong.Reference remediation. Apply incus's own LXC-driver guards (the
filepath.Basereject +os.OpenRootconfinement) todriver_qemu.go→templateApplyNow, covering both the read and the write. For the read specifically, upstream LXD is a second reference (it confines the QEMU read viad.OpenTemplates(), anos.Root). Note that LXD does not confine the QEMU write either, so the write reference is incus's own LXC guard, not LXD.Scope / honesty. Source-confirmed via a white-box review of the public repo at fresh HEAD (
ccdf811) plus an LXD cross-reference; the guard absence was grep-verified. A live-VM end-to-end PoC is the documented next step. The read is a clean arbitrary host-file disclosure. The write is a genuine root-owned arbitrary-file-create primitive, but the created path is forced to carry a.outsuffix, which constrains a direct write-to-RCE, so this is framed as High, not a 9.9 Critical, and no RCE is claimed without a.out-compatible execution path.PoC
On a self-hosted incus 7.2.0 or later, craft a VM image whose
metadata.yamlcontains:Launch the VM, then confirm on the host:
ls -l /tmp/incus-poc.out # created by incusd (root) = out-of-templates writeSeparately, a
tpl.Templateof../../etc/hostnameis read and disclosed into the guest config drive (config/files/*.out).Impact
Path traversal (CWE-22) + link following (CWE-59) in the VM/QEMU template path. An authenticated user able to create/launch a VM from a custom image, or restore a VM backup, can:
/etc/shadow), and.out-suffixed).Undisclosed. The authoritative public dissection (Snyk Labs: Newlines, Symlinks, and Arbitrary Writes: Dissecting High-Severity Vulnerabilities in Incus) covers only
driver_lxc.goand explicitly notes the VM drivers were not separately audited; no incus issue, PR, or CVE namesdriver_qemu.go→templateApplyNow.Reporter: Kingsley Olukanni (GitHub: babakizo420).