{"id":"GHSA-339v-266x-79xr","summary":"nebula-mesh: Host revocation is not durable - blocked/offboarded hosts can regain a valid certificate","details":"## Summary\n\nTwo related authorization gaps let a host that should no longer be trusted obtain a fresh, valid Nebula certificate, because nebula-mgmt does not re-evaluate revocation/authorization state at certificate *issuance* time — only at poll time.\n\n## 1. Blocklist not enforced at sign / re-enroll time\n\n`internal/api/enroll.go:128` calls `caMgr.Sign(...)` without consulting the blocklist. The blocklist is only checked in the poll path (`internal/api/updates.go:57`, `fingerprintInBlocklist`). The blocklist is keyed by certificate *fingerprint* (`internal/store/sqlite.go`), so a re-enrollment produces a new fingerprint that is not in the blocklist.\n\n`mintEnrollmentTokenForHost` (`internal/api/hosts.go:491`) authorizes the caller via `canAccessHost` but does **not** check the host status. There is no guard preventing a `blocked` host from transitioning back to `enrolled` (`internal/store/sqlite.go`, `enrollHostInTx` updates status unconditionally).\n\n**Impact:** A host that an operator has blocked can be silently un-blocked by issuing a new enrollment token and re-enrolling — it receives a fresh certificate (new fingerprint) that passes all subsequent poll-time blocklist checks. Revocation is therefore not durable. Requires an operator action (minting a re-enroll token), so this is an integrity/operational-revocation failure rather than an unauthenticated bypass.\n\n## 2. Renewal does not re-validate operator / CA status\n\nAuto-renewal at poll time (`internal/api/updates.go:285-319`, `signHostCert`) reads `host.Name`, `host.Groups`, `host.NebulaIPs` from the DB and re-signs without checking whether the owning operator is still active or the CA still valid. `DisableOperator` (`internal/store/sqlite_operators.go`) revokes sessions and API keys but does not retire the operator's CAs, and `pki/signer.go` checks only CA cert time-expiry, not operator/CA status.\n\n**Impact:** A host enrolled under an operator who is later disabled continues to renew its certificate indefinitely. Offboarding an operator does not cut off the hosts they provisioned.\n\n## Affected versions\n\nLatest tagged release (v0.3.6) and `main`.\n\n## Remediation\n\n1. Call a blocklist/status guard inside `handleEnroll` and `signHostCert` **before** `caMgr.Sign(...)`; refuse issuance for a host whose status is `blocked` or whose previous fingerprint is on the blocklist. Require an explicit unblock before re-enroll.\n2. At renewal, re-resolve the owning operator/CA status and reject renewal if the operator is disabled or the CA retired (force re-enrollment instead).\n\n## Discovery\n\nFound during an internal source + offensive security audit (tracking issue #178). Adversarially cross-verified against the code paths above.","aliases":["CVE-2026-53602","GO-2026-5950"],"modified":"2026-07-21T19:53:48.627191602Z","published":"2026-07-09T20:52:27Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-07-09T20:52:27Z","nvd_published_at":null,"cwe_ids":["CWE-613","CWE-862"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/forgekeep/nebula-mesh/security/advisories/GHSA-339v-266x-79xr"},{"type":"WEB","url":"https://github.com/forgekeep/nebula-mesh/issues/178"},{"type":"PACKAGE","url":"https://github.com/forgekeep/nebula-mesh"}],"affected":[{"package":{"name":"github.com/forgekeep/nebula-mesh","ecosystem":"Go","purl":"pkg:golang/github.com/forgekeep/nebula-mesh"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-339v-266x-79xr/GHSA-339v-266x-79xr.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:U"}]}