Summary
POST /1.0/storage-pools/<pool>/volumes with an application/octet-stream body (custom-volume ISO or backup import) uses an attacker-controlled name as an on-disk path segment without validation. A project user with the can_create_storage_volumes entitlement can supply X-Incus-name: ../../../../<target> (ISO) or an index.yaml name: ../../../../<target> (backup), causing the root Incus daemon to create directories, remove directory trees (os.RemoveAll), and (backup vector) unpack attacker content outside the storage pool. The validate.IsAPIName guard exists only on the JSON create branch and is never reached on the import branches.
Root Cause
In storagePoolVolumesPost (cmd/incusd/storage_volumes.go:828-833), octet-stream requests dispatch to createStoragePoolVolumeFromISO(..., r.Header.Get("X-Incus-name")) (:830) or createStoragePoolVolumeFromBackup(...) (:833) BEFORE the validate.IsAPIName(req.Name) at :845 (which is only on the JSON branch). createStoragePoolVolumeFromISO (:2697) checks only volName == "" (:2701). The name flows to pool.CreateCustomVolumeFromISO(projectName, volName, ...) -> project.StorageVolume(projectName, volName) -> GetVolume(...).MountPath() -> dir.CreateVolume EnsureMountPath os.Mkdir + reverter os.RemoveAll(volPath). VolumeDBCreate/validateVolume validate config keys only, not the name (verified - no ValidName/IsAPIName/filepath check in either).
Impact
Root-owned arbitrary directory creation and directory-tree removal (os.RemoveAll) at an attacker-chosen path (integrity/DoS). The backup import vector additionally unpacks tarball rootfs bytes, giving arbitrary-content file write as root -> host RCE.
Proof of Concept
ISO: curl -X POST --data-binary @x.iso -H 'Content-Type: application/octet-stream' -H 'X-Incus-type: iso' -H 'X-Incus-name: ../../../../etc/cron.d/pwn' https://incus/1.0/storage-pools/default/volumes (as a user with can_create_storage_volumes).
Attack Chain
- Entry:
POST /1.0/storage-pools/<pool>/volumes, Content-Type: application/octet-stream, X-Incus-type: iso, X-Incus-name: ../../../../etc/cron.d/x -> createStoragePoolVolumeFromISO (storage_volumes.go:830). Guard: allowPermission(project, can_create_storage_volumes).
- Check:
if volName == "" (:2701) - rejects only empty. validate.IsAPIName(req.Name) (:845) is on the JSON branch, not reached. Bypass proof: volName passes with any value including the traversal string.
- Sink:
pool.CreateCustomVolumeFromISO(projectName, volName, ...) (backend.go) -> project.StorageVolume(proj, volName) -> GetVolume(...).MountPath() -> EnsureMountPath os.Mkdir + reverter os.RemoveAll(volPath), as root. VolumeDBCreate->validateVolume validates config keys only (verified).
- Impact: root arbitrary dir create + directory-tree removal at attacker path; backup vector adds arbitrary-content write (RCE).
Bypass Evidence
validate.IsAPIName present but on the wrong branch (:845, JSON only); import branches (:830/:833) bypass it; downstream validation is config-only. filepath.Join collapse verified (same as the instance backup-import path). Forced .iso suffix constrains the final filename but still permits arbitrary-directory creation/removal via ...
Affected Versions
<= 7.2.0 (vulnerable code verified present on latest release tag v7.2.0 and on HEAD 60d85b3; no fix commit on cmd/incusd/storage_volumes.go between v7.2.0 and HEAD).
Suggested Fix
Apply validate.IsAPIName(volName, false) to the X-Incus-name header and to bInfo.Name/each volume-snapshot name at handler entry, before any storage I/O.
Credit
Vulnerability independently discovered and responsibly disclosed by Zhixi "Jace" Sun (@manus-use).
Summary
POST /1.0/storage-pools/<pool>/volumeswith anapplication/octet-streambody (custom-volume ISO or backup import) uses an attacker-controlled name as an on-disk path segment without validation. A project user with thecan_create_storage_volumesentitlement can supplyX-Incus-name: ../../../../<target>(ISO) or anindex.yaml name: ../../../../<target>(backup), causing the root Incus daemon to create directories, remove directory trees (os.RemoveAll), and (backup vector) unpack attacker content outside the storage pool. Thevalidate.IsAPINameguard exists only on the JSON create branch and is never reached on the import branches.Root Cause
In
storagePoolVolumesPost(cmd/incusd/storage_volumes.go:828-833), octet-stream requests dispatch tocreateStoragePoolVolumeFromISO(..., r.Header.Get("X-Incus-name"))(:830) orcreateStoragePoolVolumeFromBackup(...)(:833) BEFORE thevalidate.IsAPIName(req.Name)at:845(which is only on the JSON branch).createStoragePoolVolumeFromISO(:2697) checks onlyvolName == ""(:2701). The name flows topool.CreateCustomVolumeFromISO(projectName, volName, ...)->project.StorageVolume(projectName, volName)->GetVolume(...).MountPath()->dir.CreateVolumeEnsureMountPathos.Mkdir+ reverteros.RemoveAll(volPath).VolumeDBCreate/validateVolumevalidate config keys only, not the name (verified - noValidName/IsAPIName/filepathcheck in either).Impact
Root-owned arbitrary directory creation and directory-tree removal (
os.RemoveAll) at an attacker-chosen path (integrity/DoS). The backup import vector additionally unpacks tarball rootfs bytes, giving arbitrary-content file write as root -> host RCE.Proof of Concept
ISO:
curl -X POST --data-binary @x.iso -H 'Content-Type: application/octet-stream' -H 'X-Incus-type: iso' -H 'X-Incus-name: ../../../../etc/cron.d/pwn' https://incus/1.0/storage-pools/default/volumes(as a user withcan_create_storage_volumes).Attack Chain
POST /1.0/storage-pools/<pool>/volumes,Content-Type: application/octet-stream,X-Incus-type: iso,X-Incus-name: ../../../../etc/cron.d/x->createStoragePoolVolumeFromISO(storage_volumes.go:830). Guard:allowPermission(project, can_create_storage_volumes).if volName == ""(:2701) - rejects only empty.validate.IsAPIName(req.Name)(:845) is on the JSON branch, not reached. Bypass proof:volNamepasses with any value including the traversal string.pool.CreateCustomVolumeFromISO(projectName, volName, ...)(backend.go) ->project.StorageVolume(proj, volName)->GetVolume(...).MountPath()->EnsureMountPathos.Mkdir+ reverteros.RemoveAll(volPath), as root.VolumeDBCreate->validateVolumevalidates config keys only (verified).Bypass Evidence
validate.IsAPINamepresent but on the wrong branch (:845, JSON only); import branches (:830/:833) bypass it; downstream validation is config-only.filepath.Joincollapse verified (same as the instance backup-import path). Forced.isosuffix constrains the final filename but still permits arbitrary-directory creation/removal via...Affected Versions
<= 7.2.0(vulnerable code verified present on latest release tag v7.2.0 and on HEAD 60d85b3; no fix commit oncmd/incusd/storage_volumes.gobetween v7.2.0 and HEAD).Suggested Fix
Apply
validate.IsAPIName(volName, false)to theX-Incus-nameheader and tobInfo.Name/each volume-snapshot name at handler entry, before any storage I/O.Credit
Vulnerability independently discovered and responsibly disclosed by Zhixi "Jace" Sun (@manus-use).