A secrets vault with namespace chamber walls and a lease key silently crossing between tenant vaults, dark neon security aesthetic, 16:9

Same Shape, Different Primitive: CVE-2026-55774 in OpenBao

OpenBao had just fixed a silent namespace switch on token accessors. The identical shape lived one primitive over, on lease IDs, untouched — plus the HTTP 500 I misread as an outage. CVE-2026-55774.

CVE-2026-55774 · GHSA-c36x-h252-g9x2 · OpenBao · CWE-863 · Low

The lead: a fix on the sibling

OpenBao is a secrets manager (a HashiCorp Vault fork), and it's multi-tenant via namespaces. In CVE-2026-40264 the maintainers fixed a subtle authorization bug: lookupByAccessor would silently switch the request's namespace context to whatever was encoded in a token accessor string, with no check that the caller was actually in that namespace. The fix (commit f58111d2c) turned the silent switch into an explicit namespace-mismatch error.

That fix is the lead. When a project fixes a "silent context switch" on one identifier, the question writes itself: do any other identifiers carry a namespace and get resolved the same way? I grepped for the shape.

The bug

Lease IDs do. Every OpenBao lease ID embeds its namespace, and ExpirationManager.loadEntry splits it out and switches context to it — the same silent switch, no caller check:

// vault/expiration.go:2001 loadEntry
// namespace.SplitIDFromString(leaseID) -> silently swaps ctx namespace

And loadEntry sits under the canonical, ACL-reachable endpoints:

  • sys/leases/revoke/:lease_id → handleRevoke → Revoke → revokeCommon → loadEntry
  • sys/leases/renew/:lease_id → handleRenew → Renew → loadEntry

Neither handler, nor revokeCommon, checks that the caller's namespace matches the lease's. So anyone who knows a lease ID — in any namespace — can revoke or renew it, straight through their own namespace's ACL. In a secrets manager, revoking someone else's lease means killing the credential behind it.

The parent advisory here (CVE-2026-45808) had removed some legacy duplicate routes to the revoke logic. It fixed the routes and left the shared primitive they all funnel into untouched. Route, not class.

The proof

This one I proved by call-chain, not a live cluster: I traced both handlers down to loadEntry and confirmed no namespace check exists at any hop. Crucially, I also checked the read path (FetchLeaseInfo → loadEntryInternal) and found it does avoid the silent switch — which is what isolates revoke/renew as the vulnerable asymmetry rather than a blanket claim. A negative control matters as much as the positive: "this related path is safe" is what makes "this path is not" credible.

The pain: the 500 I misread

Here's the part I'm least proud of. My first attempt to file this through GitHub's advisory API returned HTTP 500. I checked whether another submission (an unrelated repo) was also 500ing that day — it was — and concluded GitHub was having a platform incident. So I fell back to emailing OpenBao's security list (openbao-security@lists.openssf.org), where maintainer Jonas Köhnen accepted the finding and offered credit.

The "outage" was not an outage. Days later I root-caused it: my payload had an empty-string field the API rejected with a 500 instead of a clean 4xx. My own malformed JSON. The same endpoint worked fine the moment the payload was well-formed. I'd seen one correlated failure and pattern-matched it into "everyone is down" — the debugging equivalent of blaming the compiler.

The lesson stuck: verify your own payload before you believe the world is broken. A 500 correlating with someone else's 500 is not proof of an outage; it's two data points that might share a cause you're holding in your own hand. (There was a coda, too — triage later flagged the advisory as missing a vulnerable_version_range, so I had to PATCH the metadata. Filing is a protocol, not a single POST.)

The lesson

When a fix lands on a "silent context switch," treat every identifier that carries the same context as a suspect until you've read its resolution path. The bug class here isn't "token accessors" or "lease IDs" — it's trusting an identifier's embedded namespace without checking the caller's. Fix it once, at the primitive, or you'll fix it forever, one identifier at a time.

Disclosed to the OpenBao security list and via GitHub Security Advisory; published as GHSA-c36x-h252-g9x2 / CVE-2026-55774. Fix slated for OpenBao v2.5.5. Thanks to Jonas Köhnen for the gracious triage.

No comments yet