VYPR
High severity7.1GHSA Advisory· Published May 28, 2026· Updated May 28, 2026

OpenBao's cross-namespace lease revocation via legacy sys/revoke path bypasses ACL

CVE-2026-45808

Description

# Impact

OpenBao's namespaces provide multi-tenant separation. A tenant who intentionally leaks lease identifiers can have their lease and underlying credential revoked or renewed by a user in another tenant via the legacy, undocumented sys/revoke and sys/renew endpoints.

# Patch

This will be addressed in v2.5.4.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OpenBao namespaces can be bypassed via legacy sys/revoke and sys/renew endpoints, allowing cross-namespace lease manipulation.

Vulnerability

The legacy, undocumented sys/revoke and sys/renew endpoints in OpenBao do not enforce namespace boundaries, allowing a user in one namespace to revoke or renew leases created in another namespace if they know the lease identifier [1][2]. This affects all versions prior to v2.5.4.

Exploitation

An attacker with any authenticated user account in any namespace can exploit this by obtaining a lease identifier from another namespace (e.g., through intentional leakage or prior compromise) and then sending a request to the legacy endpoints sys/revoke or sys/renew with that lease ID [2]. No special privileges beyond a valid token are required.

Impact

Successful exploitation allows an attacker to revoke or renew credentials belonging to other tenants, breaking multi-tenant isolation. This can lead to denial of service (revocation) or unauthorized extension of lease validity [1][2].

Mitigation

The fix was released in OpenBao v2.5.4, which removes the legacy endpoints [3][4]. Users should upgrade to v2.5.4 or later. As a workaround, if upgrading is not immediately possible, network policies can restrict access to the sys/revoke and sys/renew paths, but the only complete fix is the upgrade.

AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Openbao/OpenbaoGHSA2 versions
    <= 2.5.3+ 1 more
    • (no CPE)range: <= 2.5.3
    • (no CPE)range: <2.5.4

Patches

1
c0495646b41c

Remove legacy cross-namespace lease endpoints (#3152)

https://github.com/openbao/openbaoAlexander ScheelMay 20, 2026via ghsa
14 files changed · +35 62
  • api/plugin_helpers.go+1 1 modified
    @@ -256,7 +256,7 @@ func IsSudoPath(path string) bool {
     	}
     
     	// Some sudo paths have templated fields in them.
    -	// (e.g. /sys/revoke-prefix/{prefix})
    +	// (e.g. /sys/leases/revoke-prefix/{prefix})
     	// The values in the sudoPaths map are actually regular expressions,
     	// so we can check if our path matches against them.
     	for _, sudoPathRegexp := range sudoPaths {
    
  • changelog/3152.txt+3 0 added
    @@ -0,0 +1,3 @@
    +```release-note:security
    +core: Remove legacy lease endpoints (`sys/revoke`, `sys/renew`, `sys/revoke-prefix`, and `sys/revoke-force`) due to cross-namespace lease modification. GHSA-v8v8-cm84-m686 / CVE-2026-45808.
    +```
    
  • helper/testhelpers/logical/testing.go+1 1 modified
    @@ -328,7 +328,7 @@ func Test(tt TestT, c TestCase) {
     			// Revoke this secret later
     			revoke = append(revoke, &logical.Request{
     				Operation: logical.UpdateOperation,
    -				Path:      "sys/revoke/" + resp.Secret.LeaseID,
    +				Path:      "sys/leases/revoke/" + resp.Secret.LeaseID,
     			})
     		}
     
    
  • http/sys_lease_test.go+2 10 modified
    @@ -36,14 +36,6 @@ func TestSysRenew(t *testing.T) {
     		LeaseID string                 `json:"lease_id"`
     		Data    map[string]interface{} `json:"data"`
     	}
    -	resp = testHttpPut(t, token, addr+"/v1/sys/renew/"+result.LeaseID, nil)
    -	testResponseStatus(t, resp, 200)
    -	if err := jsonutil.DecodeJSONFromReader(resp.Body, &renewResult); err != nil {
    -		t.Fatal(err)
    -	}
    -	if result.LeaseID != renewResult.LeaseID {
    -		t.Fatal("lease id changed in renew request")
    -	}
     
     	resp = testHttpPut(t, token, addr+"/v1/sys/leases/renew/"+result.LeaseID, nil)
     	testResponseStatus(t, resp, 200)
    @@ -61,7 +53,7 @@ func TestSysRevoke(t *testing.T) {
     	defer ln.Close()
     	TestServerAuth(t, addr, token)
     
    -	resp := testHttpPut(t, token, addr+"/v1/sys/revoke/secret/foo/1234", nil)
    +	resp := testHttpPut(t, token, addr+"/v1/sys/leases/revoke/secret/foo/1234", nil)
     	testResponseStatus(t, resp, 204)
     }
     
    @@ -71,6 +63,6 @@ func TestSysRevokePrefix(t *testing.T) {
     	defer ln.Close()
     	TestServerAuth(t, addr, token)
     
    -	resp := testHttpPut(t, token, addr+"/v1/sys/revoke-prefix/secret/foo/1234", nil)
    +	resp := testHttpPut(t, token, addr+"/v1/sys/leases/revoke-prefix/secret/foo/1234", nil)
     	testResponseStatus(t, resp, 204)
     }
    
  • ui/tests/unit/serializers/policy-test.js+3 3 modified
    @@ -30,15 +30,15 @@ module('Unit | Serializer | policy', function (hooks) {
       const POLICY_SHOW_RESPONSE = {
         name: 'default',
         rules:
    -      '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
    +      '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/leases/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
         request_id: '890eabf8-d418-07af-f978-928d328a7e64',
         lease_id: '',
         renewable: false,
         lease_duration: 0,
         data: {
           name: 'default',
           rules:
    -        '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
    +        '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/leases/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
         },
         wrap_info: null,
         warnings: null,
    @@ -48,7 +48,7 @@ module('Unit | Serializer | policy', function (hooks) {
       const EMBER_DATA_EXPECTS_FOR_POLICY_SHOW = {
         name: 'default',
         rules:
    -      '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
    +      '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n    capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n    capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n    capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/leases/renew" {\n    capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n    capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n    capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n    capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n    capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n    capabilities = ["update"]\n}\n',
       };
     
       test('it transforms a list request payload', function (assert) {
    
  • vault/core_test.go+1 14 modified
    @@ -2718,19 +2718,6 @@ func TestCore_RenewSameLease(t *testing.T) {
     	original := resp.Secret.LeaseID
     
     	// Renew the lease
    -	req = logical.TestRequest(t, logical.UpdateOperation, "sys/renew/"+resp.Secret.LeaseID)
    -	req.ClientToken = root
    -	resp, err = c.HandleRequest(namespace.RootContext(t.Context()), req)
    -	if err != nil {
    -		t.Fatalf("err: %v", err)
    -	}
    -
    -	// Verify the lease did not change
    -	if resp.Secret.LeaseID != original {
    -		t.Fatalf("lease id changed: %s %s", original, resp.Secret.LeaseID)
    -	}
    -
    -	// Renew the lease (alternate path)
     	req = logical.TestRequest(t, logical.UpdateOperation, "sys/leases/renew/"+resp.Secret.LeaseID)
     	req.ClientToken = root
     	resp, err = c.HandleRequest(namespace.RootContext(t.Context()), req)
    @@ -2775,7 +2762,7 @@ func TestCore_RenewToken_SingleRegister(t *testing.T) {
     	}
     
     	// Revoke using the renew prefix
    -	req = logical.TestRequest(t, logical.UpdateOperation, "sys/revoke-prefix/auth/token/renew/")
    +	req = logical.TestRequest(t, logical.UpdateOperation, "sys/leases/revoke-prefix/auth/token/renew/")
     	req.ClientToken = root
     	_, err = c.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != nil {
    
  • vault/expiration_test.go+4 4 modified
    @@ -2680,14 +2680,14 @@ func TestExpiration_RevokeForce(t *testing.T) {
     	}
     
     	req.Operation = logical.UpdateOperation
    -	req.Path = "sys/revoke-prefix/badrenew/creds"
    +	req.Path = "sys/leases/revoke-prefix/badrenew/creds"
     
     	_, err = core.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err == nil {
     		t.Fatal("expected error")
     	}
     
    -	req.Path = "sys/revoke-force/badrenew/creds"
    +	req.Path = "sys/leases/revoke-force/badrenew/creds"
     	_, err = core.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != nil {
     		t.Fatalf("got error: %s", err)
    @@ -2745,14 +2745,14 @@ func TestExpiration_RevokeForceSingle(t *testing.T) {
     		t.Fatalf("expected id %q, got %q", leaseID, resp.Data["id"].(string))
     	}
     
    -	req.Path = "sys/revoke-prefix/" + leaseID
    +	req.Path = "sys/leases/revoke-prefix/" + leaseID
     
     	_, err = core.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err == nil {
     		t.Fatal("expected error")
     	}
     
    -	req.Path = "sys/revoke-force/" + leaseID
    +	req.Path = "sys/leases/revoke-force/" + leaseID
     	_, err = core.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != nil {
     		t.Fatalf("got error: %s", err)
    
  • vault/logical_system_integ_test.go+0 5 modified
    @@ -96,11 +96,6 @@ func TestSystemBackend_InternalUIResultantACL(t *testing.T) {
     					"update",
     				},
     			},
    -			"sys/renew": map[string]interface{}{
    -				"capabilities": []interface{}{
    -					"update",
    -				},
    -			},
     			"sys/tools/hash": map[string]interface{}{
     				"capabilities": []interface{}{
     					"update",
    
  • vault/logical_system_paths.go+4 4 modified
    @@ -2672,7 +2672,7 @@ func (b *SystemBackend) leasePaths() []*framework.Path {
     		},
     
     		{
    -			Pattern: "(leases/)?renew" + framework.OptionalParamRegex("url_lease_id"),
    +			Pattern: "leases/renew" + framework.OptionalParamRegex("url_lease_id"),
     
     			DisplayAttrs: &framework.DisplayAttributes{
     				OperationPrefix: "leases",
    @@ -2712,7 +2712,7 @@ func (b *SystemBackend) leasePaths() []*framework.Path {
     		},
     
     		{
    -			Pattern: "(leases/)?revoke" + framework.OptionalParamRegex("url_lease_id"),
    +			Pattern: "leases/revoke" + framework.OptionalParamRegex("url_lease_id"),
     
     			DisplayAttrs: &framework.DisplayAttributes{
     				OperationPrefix: "leases",
    @@ -2753,7 +2753,7 @@ func (b *SystemBackend) leasePaths() []*framework.Path {
     		},
     
     		{
    -			Pattern: "(leases/)?revoke-force/(?P<prefix>.+)",
    +			Pattern: "leases/revoke-force/(?P<prefix>.+)",
     
     			DisplayAttrs: &framework.DisplayAttributes{
     				OperationPrefix: "leases",
    @@ -2786,7 +2786,7 @@ func (b *SystemBackend) leasePaths() []*framework.Path {
     		},
     
     		{
    -			Pattern: "(leases/)?revoke-prefix/(?P<prefix>.+)",
    +			Pattern: "leases/revoke-prefix/(?P<prefix>.+)",
     
     			DisplayAttrs: &framework.DisplayAttributes{
     				OperationPrefix: "leases",
    
  • vault/logical_system_test.go+11 11 modified
    @@ -1554,7 +1554,7 @@ func TestSystemBackend_renew(t *testing.T) {
     	}
     
     	// Test orig path
    -	req2 = logical.TestRequest(t, logical.UpdateOperation, "renew")
    +	req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/renew")
     	req2.Data["lease_id"] = resp.Secret.LeaseID
     	resp2, err = b.HandleRequest(namespace.RootContext(t.Context()), req2)
     	if err != nil {
    @@ -1600,7 +1600,7 @@ func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) {
     	b := testSystemBackend(t)
     
     	// Attempt renew
    -	req := logical.TestRequest(t, logical.UpdateOperation, "renew/foobarbaz")
    +	req := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/foobarbaz")
     	resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != logical.ErrInvalidRequest {
     		t.Fatalf("err: %v", err)
    @@ -1610,7 +1610,7 @@ func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) {
     	}
     
     	// Attempt renew with other method
    -	req = logical.TestRequest(t, logical.UpdateOperation, "renew")
    +	req = logical.TestRequest(t, logical.UpdateOperation, "leases/renew")
     	req.Data["lease_id"] = "foobarbaz"
     	resp, err = b.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != logical.ErrInvalidRequest {
    @@ -1653,7 +1653,7 @@ func TestSystemBackend_revoke(t *testing.T) {
     	}
     
     	// Attempt revoke
    -	req2 := logical.TestRequest(t, logical.UpdateOperation, "revoke/"+resp.Secret.LeaseID)
    +	req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke/"+resp.Secret.LeaseID)
     	resp2, err := b.HandleRequest(namespace.RootContext(t.Context()), req2)
     	if err != nil {
     		t.Fatalf("err: %v %#v", err, resp2)
    @@ -1663,7 +1663,7 @@ func TestSystemBackend_revoke(t *testing.T) {
     	}
     
     	// Attempt renew
    -	req3 := logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID)
    +	req3 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID)
     	resp3, err := b.HandleRequest(namespace.RootContext(t.Context()), req3)
     	if err != logical.ErrInvalidRequest {
     		t.Fatalf("err: %v", err)
    @@ -1688,7 +1688,7 @@ func TestSystemBackend_revoke(t *testing.T) {
     	}
     
     	// Test the other route path
    -	req2 = logical.TestRequest(t, logical.UpdateOperation, "revoke")
    +	req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke")
     	req2.Data["lease_id"] = resp.Secret.LeaseID
     	resp2, err = b.HandleRequest(namespace.RootContext(t.Context()), req2)
     	if err != nil {
    @@ -1763,7 +1763,7 @@ func TestSystemBackend_revoke_invalidID_origUrl(t *testing.T) {
     	b := testSystemBackend(t)
     
     	// Attempt revoke
    -	req := logical.TestRequest(t, logical.UpdateOperation, "revoke/foobarbaz")
    +	req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke/foobarbaz")
     	resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != nil {
     		t.Fatalf("err: %v", err)
    @@ -1773,7 +1773,7 @@ func TestSystemBackend_revoke_invalidID_origUrl(t *testing.T) {
     	}
     
     	// Attempt revoke with other method
    -	req = logical.TestRequest(t, logical.UpdateOperation, "revoke")
    +	req = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke")
     	req.Data["lease_id"] = "foobarbaz"
     	resp, err = b.HandleRequest(namespace.RootContext(t.Context()), req)
     	if err != nil {
    @@ -1873,7 +1873,7 @@ func TestSystemBackend_revokePrefix_origUrl(t *testing.T) {
     	}
     
     	// Attempt revoke
    -	req2 := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/secret/")
    +	req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/secret/")
     	resp2, err := b.HandleRequest(namespace.RootContext(t.Context()), req2)
     	if err != nil {
     		t.Fatalf("err: %v %#v", err, resp2)
    @@ -1883,7 +1883,7 @@ func TestSystemBackend_revokePrefix_origUrl(t *testing.T) {
     	}
     
     	// Attempt renew
    -	req3 := logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID)
    +	req3 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID)
     	resp3, err := b.HandleRequest(namespace.RootContext(t.Context()), req3)
     	if err != logical.ErrInvalidRequest {
     		t.Fatalf("err: %v", err)
    @@ -2008,7 +2008,7 @@ func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) {
     		t.Fatalf("err: %v", err)
     	}
     
    -	req := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/auth/github/")
    +	req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/auth/github/")
     	resp, err := b.HandleRequest(ctx, req)
     	if err != nil {
     		t.Fatalf("err: %v %v", err, resp)
    
  • vault/policy/policy_store.go+0 3 modified
    @@ -90,9 +90,6 @@ path "sys/internal/ui/resultant-acl" {
     
     # Allow a token to renew a lease via lease_id in the request body; old path for
     # old clients, new path for newer
    -path "sys/renew" {
    -    capabilities = ["update"]
    -}
     path "sys/leases/renew" {
         capabilities = ["update"]
     }
    
  • vault/request_handling.go+1 2 modified
    @@ -1290,8 +1290,7 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp
     
     	// If there is a secret, we must register it with the expiration manager.
     	// We exclude renewal of a lease, since it does not need to be re-registered
    -	if resp != nil && resp.Secret != nil && !strings.HasPrefix(req.Path, "sys/renew") &&
    -		!strings.HasPrefix(req.Path, "sys/leases/renew") {
    +	if resp != nil && resp.Secret != nil && !strings.HasPrefix(req.Path, "sys/leases/renew") {
     		// KV mounts should return the TTL but not register
     		// for a lease as this provides a massive slowdown
     		registerLease := true
    
  • vault/token_store.go+3 3 modified
    @@ -3192,14 +3192,14 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque
     }
     
     // handleRevokeSelf handles the auth/token/revoke-self path for revocation of tokens
    -// in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke
    +// in a way that revokes all child tokens. Normally, using sys/leases/revoke/leaseID will revoke
     // the token and all children anyways, but that is only available when there is a lease.
     func (ts *TokenStore) handleRevokeSelf(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
     	return ts.revokeCommon(ctx, req, data, req.ClientToken)
     }
     
     // handleRevokeTree handles the auth/token/revoke/id path for revocation of tokens
    -// in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke
    +// in a way that revokes all child tokens. Normally, using sys/leases/revoke/leaseID will revoke
     // the token and all children anyways, but that is only available when there is a lease.
     func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
     	id := data.Get("token").(string)
    @@ -3250,7 +3250,7 @@ func (ts *TokenStore) revokeCommon(ctx context.Context, req *logical.Request, da
     }
     
     // handleRevokeOrphan handles the auth/token/revoke-orphan/id path for revocation of tokens
    -// in a way that leaves child tokens orphaned. Normally, using sys/revoke/leaseID will revoke
    +// in a way that leaves child tokens orphaned. Normally, using sys/leases/revoke/leaseID will revoke
     // the token and all children.
     func (ts *TokenStore) handleRevokeOrphan(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
     	// Parse the id
    
  • website/content/docs/internals/token.mdx+1 1 modified
    @@ -27,7 +27,7 @@ Each token maintains the source path, or the login path, that was used
     to create the token. This is used to allow source based revocation. For example,
     if we believe our GitHub organization was compromised, we may want to revoke
     all tokens generated via `auth/github/login`. This would be done by using the
    -`sys/revoke-prefix/` API with the `auth/github/` prefix. Revoking the
    +`sys/leases/revoke-prefix/` API with the `auth/github/` prefix. Revoking the
     prefix will revoke all client tokens generated at that path, as well as all
     dynamic secrets generated by those tokens. This provides a powerful "break glass"
     procedure during a potential compromise.
    

Vulnerability mechanics

Root cause

"Legacy lease endpoints (`sys/revoke`, `sys/renew`, `sys/revoke-prefix`, `sys/revoke-force`) did not enforce namespace scoping, allowing a user in one tenant to renew or revoke leases belonging to another tenant."

Attack vector

OpenBao's namespaces provide multi-tenant separation, but the legacy, undocumented `sys/revoke` and `sys/renew` endpoints (and their `-prefix`/`-force` variants) operated without namespace scoping [ref_id=1]. A tenant who intentionally leaks a lease identifier enables an attacker in another tenant to call these legacy endpoints to renew or revoke that lease and its underlying credential. The attacker only needs the lease ID string and network access to the OpenBao API; no additional cross-tenant authorization is required because the legacy paths bypass namespace isolation.

Affected code

The vulnerable route patterns are defined in `vault/logical_system_paths.go` where the regex `(leases/)?renew`, `(leases/)?revoke`, `(leases/)?revoke-force`, and `(leases/)?revoke-prefix` matched both the legacy bare paths (e.g., `sys/renew`) and the namespaced paths (e.g., `sys/leases/renew`) [patch_id=2964630]. The default policy in `vault/policy/policy_store.go` also granted `update` capability on `sys/renew`.

What the fix does

The patch removes the legacy route patterns by changing the path regex from `(leases/)?renew`, `(leases/)?revoke`, `(leases/)?revoke-force`, and `(leases/)?revoke-prefix` to require the `leases/` prefix (e.g., `leases/renew`, `leases/revoke`) in `vault/logical_system_paths.go` [patch_id=2964630]. This eliminates the undocumented bare `sys/renew`, `sys/revoke`, `sys/revoke-prefix`, and `sys/revoke-force` endpoints that lacked namespace enforcement. The commit message notes these endpoints were "largely undocumented except for tests" and that removing them "reduce[s] noise in OpenAPI" [ref_id=1]. The fix also updates the default policy in `vault/policy/policy_store.go` to reference `sys/leases/renew` instead of `sys/renew`.

Preconditions

  • networkThe attacker must have network access to the OpenBao API.
  • inputThe attacker must obtain a valid lease ID from another tenant (e.g., via intentional leak or information disclosure).
  • authThe attacker must have a valid token or authentication that can reach the legacy sys/revoke or sys/renew endpoints (the default policy allowed update on sys/renew).

Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.