Nov 6, 2026·11 min read·4 visits
Authenticated users in restricted LXD or Incus projects can bypass security restrictions by restoring instance snapshots or importing backups containing prohibited low-level hypervisor hooks (e.g., raw.lxc), leading to arbitrary command execution as root on the host OS.
A missing authorization and configuration validation vulnerability in Canonical LXD and LXC Incus allows authenticated users within restricted projects to bypass project security boundaries. By smuggling prohibited low-level configuration directives (such as raw.lxc or raw.qemu) inside instance snapshots or backup archives, an attacker can restore these configurations within a restricted project and execute arbitrary commands with root privileges on the underlying host operating system.
Canonical LXD and its upstream fork, LXC Incus, are system container and virtual machine managers built on top of LXC and QEMU. They expose a REST API over Unix sockets and HTTPS, enabling administrators and delegated users to provision and manage compute instances. To support multi-tenant environments, both platforms implement a feature called Projects, which isolates instances, images, profiles, and networks into discrete administrative namespaces.
Within a multi-tenant deployment, administrators apply project-level security restrictions to prevent unprivileged tenants from escaping their containers or compromising the host. When a project is configured with restricted=true, features that allow direct interaction with the host system are disabled by default. Specifically, the restricted.containers.lowlevel=block and restricted.virtual-machines.lowlevel=block settings prohibit users from specifying raw hypervisor parameters like raw.lxc, raw.idmap, and raw.qemu.
GHSA-47w9-6r3f-938g (also tracked as CVE-2026-48751, GHSA-48q5-w887-33wv, and GO-2026-5799) is a missing authorization vulnerability (CWE-862) in the instance state restoration workflows of LXD and Incus. When a user restores an instance to a previous snapshot or creates an instance from a backup archive, the daemon fails to validate the configuration stored inside the snapshot against the active project's restriction policies.
Consequently, an authenticated user with access to a restricted project can smuggle prohibited raw.lxc lifecycle hooks into the project via a snapshot. Upon restoring the snapshot and starting the instance, the LXD or Incus daemon passes these unvalidated directives to the underlying LXC runtime, resulting in arbitrary command execution with full root privileges on the host operating system.
The security model of LXD and Incus relies on intercepting and validating instance configuration changes before they are committed to the internal database and applied to the runtime environment. During standard API operations—such as creating an instance via POST /1.0/instances or modifying an active instance via PUT /1.0/instances/<name>—the daemon invokes validation routines located in the limits package, specifically limits.AllowInstanceCreation and limits.AllowInstanceUpdate.
These validation functions inspect the incoming configuration dictionary, attached devices, and profiles against the project's restriction rules. If a project enforces restricted.containers.lowlevel=block and the request payload contains a raw.lxc key, the validator rejects the request before any state modification occurs. This check is also enforced when migrating an active instance across project boundaries; the target project validates the active configuration of the incoming instance.
The vulnerability arises because snapshots maintain their own independent copy of the instance's configuration metadata (LocalConfig), devices (LocalDevices), and profiles at the point in time the snapshot was created. Two distinct state-restoration code paths failed to pass this snapshot-embedded metadata through the limits validation functions:
Snapshot Restoration (instanceSnapRestore in lxd/instance_put.go): When an instance is moved from an unrestricted project into a restricted project, only its current active configuration is checked against the destination project's restrictions. If the user unsets raw.lxc on the active instance prior to migration, the transfer succeeds even if an attached snapshot (snap0) still contains the prohibited raw.lxc key. When the user subsequently triggers a snapshot restore inside the restricted project, instanceSnapRestore extracts the configuration map from source.LocalConfig() and overwrites the parent instance's configuration without invoking limits.AllowInstanceUpdate.
Backup Ingestion (createFromBackup in lxd/instances_post.go): When importing an instance from a backup tarball (backup.yaml or index.yaml), the daemon parses the archive into a backupInfo structure containing both the primary instance configuration (bInfo.Config.Instance) and an array of snapshot configurations (bInfo.Config.Snapshots). While createFromBackup called limits.AllowInstanceCreation on the primary instance structure, it iterated over bInfo.Config.Snapshots and committed them to the database without performing any restriction checks. A user could import a crafted backup containing clean primary metadata and a malicious snapshot, then immediately restore the snapshot to apply the forbidden directives.
The remediation required introducing explicit authorization and restriction checks across both the snapshot restoration path and the backup ingestion path. In Canonical LXD, these fixes were implemented in lxd/instance_put.go and lxd/instances_post.go.
lxd/instance_put.go)In the vulnerable version of instanceSnapRestore, the function verified disk storage quotas and prepared the instance for restoration, then immediately generated a new volatile UUID and applied the snapshot's state. It treated the snapshot as an already-trusted internal object rather than an untrusted configuration update.
Commit a52e5d06a52fb7ad8862cb2a8659a8c47e446efe (and backport commits 6b93b2a0c4f6f3eaf91e1e813f6875d5eb66b59b and cfd9f66f6af89e1fa3b3acb12bfe72f51b7f1e19) resolves this by constructing a synthetic api.InstancePut request representing the state stored inside the snapshot (source) and validating it within a cluster database transaction via limits.AllowInstanceUpdate.
// File: lxd/instance_put.go
func instanceSnapRestore(s *state.State, projectName string, name string, snap string, stateful bool, diskOnly bool) error {
// ... [existing snapshot lookup and storage preparation] ...
// PATCH: Extract profile names associated with the snapshot
snapProfileNames := make([]string, 0, len(source.Profiles()))
for _, profile := range source.Profiles() {
snapProfileNames = append(snapProfileNames, profile.Name)
}
// PATCH: Construct an InstancePut payload mirroring the snapshot's stored config and devices
snapConfig := api.InstancePut{
Config: source.LocalConfig(),
Devices: source.LocalDevices().CloneNative(),
Profiles: snapProfileNames,
}
// PATCH: Validate the snapshot's configuration against the current project's limits and restrictions
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
return limits.AllowInstanceUpdate(ctx, s.GlobalConfig, tx, projectName, name, snapConfig, inst.LocalConfig())
})
if err != nil {
return err
}
// Generate a new `volatile.uuid.generation` to differentiate this instance restored from a snapshot from the original instance.
source.LocalConfig()["volatile.uuid.generation"] = uuid.New().String()By passing snapConfig and the active instance's inst.LocalConfig() into limits.AllowInstanceUpdate, LXD evaluates the state transition identically to a standard PUT or PATCH API request. If the snapshot contains keys that are blocked in projectName, the transaction returns an error and aborts the restoration before any changes reach the instance runtime.
lxd/instances_post.go)In createFromBackup, the daemon previously validated only the top-level req object representing the main instance. Commit 5aac658c888d52e3a2480a8f879799ac643fdc49 extends this logic to iterate through every snapshot definition inside bInfo.Config.Snapshots, wrapping each snapshot's metadata in an api.InstancesPost structure and passing it to limits.AllowInstanceCreation.
// File: lxd/instances_post.go
// Check restrictions/limits if defined on project.
if restrictions != nil {
err = limits.AllowInstanceCreation(s.GlobalConfig, *restrictions, req)
if err != nil {
return response.SmartError(err)
}
// PATCH: Iterate through all bundled snapshots in the backup archive
for i, snapshot := range bInfo.Config.Snapshots {
if snapshot == nil {
return response.SmartError(fmt.Errorf("Nil instance snapshot definition found at index %d", i))
}
// PATCH: Build a synthetic creation request for each snapshot
snapshotReq := api.InstancesPost{
InstancePut: api.InstancePut{
Architecture: snapshot.Architecture,
Config: snapshot.Config,
Devices: snapshot.Devices,
Ephemeral: snapshot.Ephemeral,
Profiles: snapshot.Profiles,
Stateful: snapshot.Stateful,
},
Name: bInfo.Name + "/" + snapshot.Name,
Source: api.InstanceSource{},
Type: api.InstanceType(bInfo.Config.Instance.Type),
}
// PATCH: Enforce project creation restrictions on the snapshot metadata
err = limits.AllowInstanceCreation(s.GlobalConfig, *restrictions, snapshotReq)
if err != nil {
return response.SmartError(err)
}
}
}Together, these two patches close both the direct ingestion vector (importing a backup with poisoned snapshots into a restricted project) and the time-of-check to time-of-use (TOCTOU) vector (moving an instance with pre-existing snapshots into a restricted project, or applying restrictions to a project after snapshots have already been created). Even if a snapshot containing raw.lxc already resides in a restricted project's database, the check in instanceSnapRestore prevents that snapshot from ever being restored onto an runnable instance.
Exploiting GHSA-47w9-6r3f-938g requires an authenticated user with permissions to manage instances within a restricted project on the target LXD or Incus server. To use the cross-project migration vector, the attacker also needs access to an unrestricted environment—such as a local LXD/Incus installation under their control or an unrestricted project on the same cluster—from which they can push an instance to the restricted remote project. Alternatively, if the attacker can upload instance backups (POST /1.0/instances with application/octet-stream), they can craft a malicious backup tarball locally without needing a second LXD daemon.
In the migration scenario, the attacker exploits LXC's lxc.hook.pre-start directive via the raw.lxc configuration key. In LXC, lxc.hook.pre-start specifies a command or script that the LXC runtime executes in the host's mount and process namespace before the container's namespaces are initialized or privileges are dropped. Because the LXD/Incus daemon runs as root on the host, any command specified in lxc.hook.pre-start executes with full host root privileges.
The official advisory provides a step-by-step reproduction demonstrating how to smuggle raw.lxc across project boundaries:
# Step 1: Assume the target server ('rem:') has a restricted project configured by an admin
incus project set rem:project restricted=true
incus project set rem:project restricted.containers.lowlevel=block
# Step 2: On an attacker-controlled local daemon (or unrestricted project), initialize a container
incus init images:debian/trixie rce-raw-lxc
# Step 3: Configure the raw.lxc pre-start hook to execute /bin/id and write to the host root filesystem
incus config set rce-raw-lxc raw.lxc='lxc.hook.pre-start = /bin/sh -c "/bin/id > /lxc-hook-prestart"'
# Step 4: Create a snapshot ('snap0'). The database record for 'snap0' now holds the raw.lxc key.
incus snapshot create rce-raw-lxc snap0
# Step 5: Remove the raw.lxc key from the active container so it passes target project migration checks
incus config unset rce-raw-lxc raw.lxc
# Step 6: Push the container and its attached snapshots into the target's restricted project
incus move rce-raw-lxc rem: --mode push
# Step 7: Restore 'snap0' on the remote restricted server. Due to the missing check, raw.lxc is reinstated.
incus snapshot restore rem:rce-raw-lxc snap0
# Step 8: Start the container on the remote server, triggering the pre-start hook on the host
incus start rem:rce-raw-lxcWhen incus start rem:rce-raw-lxc (or the equivalent lxc CLI command) is issued in Step 8, the remote daemon generates the container's runtime lxc.conf file, appends the unvalidated contents of raw.lxc, and spawns the container monitor. Before the container rootfs is pivoted, /bin/sh -c "/bin/id > /lxc-hook-prestart" executes directly on the target host, creating /lxc-hook-prestart owned by uid=0(root).
The vulnerability carries a CVSS v3.1 base score of 9.9 (Critical) with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H. The low privileges required (PR:L) reflects that an attacker only needs standard tenant credentials for a restricted project, while the changed scope (S:C) underscores that a vulnerability in the multi-tenant management control plane leads directly to a complete compromise of the underlying hypervisor host.
> [!WARNING]
> Because raw.lxc and raw.qemu allow direct manipulation of hypervisor execution arguments and lifecycle hooks, successful exploitation completely negates all container and virtual machine isolation mechanisms (namespaces, cgroups, seccomp, and AppArmor confinement).
Once arbitrary code execution as root on the host is achieved, the attacker gains unrestricted control over:
C:H): Full read access to the host filesystem, host memory, storage pools, cryptographic keys, cluster certificates, and the filesystems of all other tenant containers and virtual machines running on the node.I:H): The ability to modify host binaries, inject persistent rootkits, alter the LXD/Incus database directly, and tamper with workloads belonging to other isolated projects.A:H): The capability to terminate the LXD/Incus daemon, destroy storage pools, disrupt host networking, or shut down the physical server.In clustered LXD or Incus deployments, compromising a single cluster member as root typically allows the attacker to extract the cluster client certificate and database credentials (/var/snap/lxd/common/lxd/cluster.crt or /var/lib/incus/cluster.crt), enabling lateral movement and administrative takeover across the entire cluster.
Administrators operating multi-tenant LXD or Incus environments where untrusted or semi-trusted users are granted access to restricted projects must upgrade to a patched release immediately.
>= 4.12.0, < 5.0.7).>= 5.1.0, < 5.21.5).>= 6.0.0, < 6.9.0).< 7.2.0).Upgrading the daemon prevents existing malicious snapshots from being restored, and prevents new malicious backups from being imported. However, if a snapshot was already restored prior to patching, the forbidden configuration key will remain active on the instance. Administrators should audit all instances and snapshots across restricted projects for the presence of raw.* configuration directives (raw.lxc, raw.qemu, raw.idmap, raw.apparmor, raw.seccomp).
The following command queries the LXD REST API recursively and uses jq to filter for any instance snapshots containing raw.lxc or raw.qemu entries:
# Audit snapshots across all instances in the current project
lxc query "/1.0/instances?recursion=2" | jq '.[] | select(.snapshots != null) | {
name: .name,
project: .project,
active_raw_lxc: .config["raw.lxc"],
active_raw_qemu: .config["raw.qemu"],
snapshots: [
.snapshots[] | select(.config["raw.lxc"] != null or .config["raw.qemu"] != null) | {
name: .name,
raw_lxc: .config["raw.lxc"],
raw_qemu: .config["raw.qemu"]
}
]
} | select(.active_raw_lxc != null or .active_raw_qemu != null or (.snapshots | length > 0))'If immediate patching is not feasible, administrators should revoke API access for untrusted tenants or use RBAC / OpenFGA rules (where supported) to block POST /1.0/instances (backup import/migration) and PUT /1.0/instances/<name> (snapshot restoration) operations within restricted projects until the upgrade is completed.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
LXD (LTS 4.0 / 5.0 series) Canonical | >= 4.12.0, < 5.0.7 | 5.0.7 |
LXD (LTS 5.21 series) Canonical | >= 5.1.0, < 5.21.5 | 5.21.5 |
LXD (Feature series) Canonical | >= 6.0.0, < 6.9.0 | 6.9.0 |
Incus Linux Containers (LXC) | < 7.2.0 | 7.2.0 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | GHSA-47w9-6r3f-938g / CVE-2026-48751 |
| Weakness Class | CWE-862: Missing Authorization |
| Attack Vector | Network (REST API) |
| CVSS v3.1 Score | 9.9 Critical (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| EPSS Score | 0.68% (52.67th Percentile) |
| Security Impact | Container Escape / Host Root Command Execution |
| Exploit Status | Public Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
An operating system command injection vulnerability (CWE-78) exists in GeoNode versions prior to 5.1.0. The vulnerability occurs in the vector upload processing pipeline when handling user-supplied geospatial files. By supplying a crafted layer name containing shell metacharacters, an authenticated attacker with basic data upload privileges can execute arbitrary commands on the underlying server or container hosting the GeoNode application and Celery worker processes.
GeoNode versions prior to 5.0.3 contain an unrestricted file upload vulnerability (CWE-434) within the document and dataset ingestion architecture. Authenticated low-privileged users can bypass upload validation via direct REST API interactions, file extension masquerading, metadata mutation via PATCH requests, or malicious archive submissions. This allows attackers to store and serve dangerous file types, alter on-disk file references, or trigger path traversal and resource exhaustion during archive extraction.
CVE-2026-55649 is a stored Cross-Site Scripting (XSS) vulnerability in GeoNode versions prior to 5.0.3. The flaw exists because document abstract metadata fields are ingested via the metadata management pipeline and stored in the backend database without HTML sanitization or input neutralization. An authenticated attacker with privileges to create or edit document metadata can inject arbitrary HTML and JavaScript payloads into these fields. When another user, such as an administrator, views the affected document page or catalog listing, the unencoded payload is rendered into the Document Object Model (DOM) and executes within the victim's browser session.
CVE-2026-55622 (GHSA-qx75-2p3r-pwm5) is an improper access control vulnerability (CWE-284) in Canonical LXD and LXC Incus (tracked as GHSA-c9f5-j9c3-mhrg). In multi-tenant deployments utilizing project-based isolation, the REST API handler for instance creation via POST /1.0/instances verifies that the authenticated user has authorization in the destination project specified by the query parameter, but fails to verify whether the user holds read or view permissions for the source instance when executing a copy operation across projects. Consequently, an authenticated client restricted to a single project can clone instances or snapshots from unauthorized projects into their own project, resulting in complete disclosure of the target instance filesystem and embedded credentials.
An authorization bypass vulnerability in Canonical LXD (tracked as GHSA-7mr3-28h5-m5vx and CVE-2026-55621, and as GHSA-64f3-v33m-w89f in Linux Containers Incus) allows an authenticated user with restricted project access to copy custom storage volumes and volume snapshots from unauthorized projects. The flaw resides in the storagePoolVolumesPost REST API handler, which verifies permissions on the destination project but fails to check whether the caller holds view entitlements on the source project or volume specified in the request body.
A symbolic link traversal vulnerability (CWE-59) in Canonical LXD and Linux Containers Incus allows an authenticated user with image import permissions to read and write arbitrary files on the host filesystem. During image archive unpacking, the daemon fails to reject top-level symbolic links for the instance `templates` directory. Subsequent requests to the instance template API endpoints follow this symbolic link outside the instance storage directory with root privileges, enabling complete host compromise.