GHSA-XG4H-6GFC-H4M8: Root-cause Fix for etcd Watch RBAC Bypass
Summary
The patch addresses an authorization bypass in etcd's Watch API where an open-ended range sentinel was normalized before RBAC evaluation, causing a ">= key" watch to be authorized as an exact-key watch. Based on the supplied test changes and advisory context, the fix moves permission checking ahead of RangeEnd normalization, which directly corrects the semantic mismatch that enabled the bypass.
Analysis
Vulnerability
GHSA-XG4H-6GFC-H4M8 describes an authorization bypass in etcd's gRPC Watch handler. The issue is a semantic mismatch between wire-format range encoding and the authorization layer's interpretation of that range. Per the supplied test commentary in the referenced commits, a client using clientv3.WithFromKey() sends an open-ended watch as RangeEnd == 0x00. In server/etcdserver/api/v3rpc/watch.go, that sentinel was rewritten to []byte{} before the permission check. The authorization logic in server/auth/range_perm_cache.go then evaluated len(rangeEnd) == 0, treating the request like an exact-key query rather than an open-ended [key, +inf) range.
The result was that a principal with READ permission on a single key could create a watch that observed all lexicographically greater keys. This is a classic time-of-check/representation bug: the object was normalized for downstream watch semantics before RBAC consumed it, and the normalized representation collapsed two distinct meanings relevant to authorization.
if len(creq.RangeEnd) == 0 {
// force nil since watchstream.Watch distinguishes
// between nil and []byte{} for single key / >=
creq.RangeEnd = nil
}
if len(creq.RangeEnd) == 1 && creq.RangeEnd[0] == 0 {
// support >= key queries
creq.RangeEnd = []byte{}
}The vulnerable behavior and root cause are documented in the e2e test updates shipped with the fixes in commit 6643f80602461a6095c9b294b6512fd9719bef41, commit afeaa624da19085b47fb5ccc7c22a8c421bc2eae, and commit e863b001bbf3367003a543aa3099db9892134cd7. An additional summary is available at cvereports.com.
Patch
The supplied code digest does not show a visible line-level change in the normalization snippet itself, but the updated test text explicitly states the intended fix: permission checking now runs against the unmodified RangeEnd, before the []byte{}/nil rewrite needed by watchStream.Watch. That is the correct repair strategy because it preserves the original request semantics for RBAC while still allowing internal watch machinery to receive its preferred normalized representation.
The strongest evidence for the patch behavior is the renamed and rewritten e2e test. The old test demonstrated event leakage to a low-permission user. The new test expects the watch to be canceled and the channel closed when that same user attempts a WithFromKey() watch beyond its exact-key grant.
// Fixed by running the permission check against the
// unmodified RangeEnd, before the []byte{}/nil rewrite for watchStream.Watch.
...
require.True(t, wresp.Canceled)
...
if ok {
t.Fatalf("expected watch channel to be closed after denial, got response: %+v", wresp)
}This is a source-grounded root-cause fix because it restores the correct ordering: authorize first on the original wire semantics, then normalize for execution. It does not merely filter leaked events after watch creation, nor does it special-case one client helper without addressing the underlying representation collapse.
Review
Pros
- Fixes the actual semantic bug: authorization now evaluates the original open-ended range request instead of a normalized representation that aliases to exact-key semantics.
- Preserves existing watch subsystem behavior by keeping the
nilvs[]byte{}normalization forwatchStream.Watch, but moves it after the security boundary. - Adds an end-to-end regression test that validates the security property from the caller perspective: unauthorized watch creation is denied, canceled, and closed.
- The test narrative clearly documents the exploit path and the corrected behavior, which improves maintainability and reduces the chance of reintroducing the bug in future refactors.
- The same fix pattern appears to have been backported across multiple branches, as indicated by the three referenced commits.
Cons
- The provided patch digest omits the exact changed lines in
watch.go, so reviewers must infer the implementation from the updated test and advisory context rather than directly inspect the reordered authorization call in the snippet. - The bug class suggests broader risk anywhere request normalization occurs before RBAC or admission checks. The supplied materials only demonstrate coverage for the
WithFromKey()watch case. - The authorization layer's reliance on
len(rangeEnd) == 0to distinguish semantics remains subtle. Even with the ordering fixed, this representation-sensitive contract is easy to misuse elsewhere.
Verdict
Root-cause.
Based on the advisory and the updated e2e tests in 6643f80602461a6095c9b294b6512fd9719bef41, afeaa624da19085b47fb5ccc7c22a8c421bc2eae, and e863b001bbf3367003a543aa3099db9892134cd7, the patch corrects the ordering flaw that caused RBAC to authorize the wrong operation. The vulnerable condition was not merely insufficient validation; it was premature normalization across a trust boundary. Moving the permission check ahead of normalization directly addresses that root cause. Follow-up hardening would still be prudent: audit other APIs for pre-auth normalization and consider making range semantics explicit in types rather than inferred from empty-slice length.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- No Sutpo.go
Best match for this etcd issue because the advisory describes a broken authorization / RBAC bypass in Go. This lab focuses on horizontal privilege escalation and improper authorization checks, which maps closely to ensuring watch or range-style access is constrained to the caller’s permitted scope.
- Bad token.go
Useful follow-on Go lab for access-control enforcement mistakes tied to token or identity context. It reinforces the defensive pattern missing in the advisory scenario: validating that authenticated identity is authorized for the exact resource set being requested, not a broader effective range.
- Codehub.go
Recommended as a broader authorization-hardening exercise. Although framed as mass assignment, its mapped weaknesses include improper authorization and IDOR-style access control failures. It helps practice defense-in-depth around server-side trust boundaries and request normalization—relevant to preventing scope expansion bugs like open-ended range misuse.