GHSA-crmg-9m86-636r lxd's non-recursive certificate listing bypasses per-object authorization and leaks all fingerprints
SummaryThe `GET /1.0/certificates` endpoint (non-recursive mode) returns URLs containing fingerprints for all certificates in the trust store, bypassing the per-object `can_view` authorization check that is correctly applied in the recursive path. Any authenticated identity — including restricted, non-admin users — can enumerate all certificate fingerprints, exposing the full set of trusted identities in the LXD deployment.Affected Component`lxd/certificates.go` — `certificatesGet` (lines 185–192) — Non-recursive code path returns unfiltered certificate list.CWE**CWE-862**: Missing AuthorizationDescriptionCore vulnerability: missing permission filter in non-recursive listing pathThe `certificatesGet` handler obtains a permission checker at line 143 and correctly applies it when building the recursive response (lines 163-176). However, the non-recursive code path at lines 185-192 creates a fresh loop over the unfiltered `baseCerts` slice, completely bypassing the authorization check:Inconsistency with other list endpoints confirms the bugFive other list endpoints in the same codebase correctly filter results in both recursive and non-recursive paths:| Endpoint | File | Filters non-recursive? |
|----------|------|----------------------|
| Instances | `lxd/instances_get.go` — `instancesGet` | Yes — filters before either path |
| Images | `lxd/images.go` — `doImagesGet` | Yes — checks `hasPermission` for both paths |
| Networks | `lxd/networks.go` — `networksGet` | Yes — filters outside recursion check |
| Profiles | `lxd/profiles.go` — `profilesGet` | Yes — separate filter in non-recursive path |
| **Certificates** | **`lxd/certificates.go` — `certificatesGet`** | **No — unfiltered** |The certificates endpoint is the sole outlier, confirming this is an oversight rather than a design choice.Access handler provides no defenseThe endpoint uses `allowAuthenticated` as its `AccessHandler` (`certificates.go:45`), which only checks `requestor.IsTrusted()`:The comment explicitly states that `allowAuthenticated` should be "used in conjunction with further access control within the handler" — which the non-recursive path fails to do.Execution chainRestricted authenticated user sends `GET /1.0/certificates` (no `recursion` parameter)`allowAuthenticated` access handler passes because user is trusted (`daemon.go:263`)`certificatesGet` creates permission checker for `EntitlementCanView` on `TypeCertificate` (line 143)Loop at lines 163-176 filters `baseCerts` by permission — but only populates `certResponses` for recursive modeSince `!recursion`, control reaches lines 185-192New loop iterates ALL `baseCerts` (unfiltered) and builds URL list with fingerprintsFull list of certificate fingerprints returned to restricted userProof of ConceptImpact**Identity enumeration**: A restricted user can discover the fingerprints of all trusted certificates, revealing the complete set of identities in the LXD trust store.**Reconnaissance for targeted attacks**: Fingerprints identify specific certificates used for inter-cluster communication, admin access, and other privileged operations.**RBAC bypass**: In deployments using fine-grained RBAC (OpenFGA or built-in TLS authorization), the non-recursive path completely bypasses the intended per-object visibility controls.**Information asymmetry**: Restricted users gain knowledge of the full trust topology, which the administrator explicitly intended to hide via per-certificate `can_view` entitlements.Recommended RemediationOption 1: Apply the permission filter to the non-recursive path (preferred)Replace the unfiltered loop with one that checks `userHasPermission`, matching the pattern used in the recursive path and in all other list endpoints:Option 2: Build both response types in a single filtered loopRestructure the function to build both the URL list and the recursive response in the same permission-checked loop, eliminating the possibility of divergent filtering:Option 2 is structurally safer as it prevents the two paths from diverging in the future.CreditThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).
CVSS v4.0 score: 5.3