Jul 25, 2026·8 min read·4 visits
A premature normalization flaw in etcd's gRPC Watch handler allows users with read permission for a single key to bypass RBAC and watch all lexicographically greater keys.
An authorization bypass vulnerability in the gRPC Watch API of etcd allows low-privileged users to read keys outside of their authorized range. By utilizing an open-ended range request sentinel, the input is prematurely normalized before RBAC validation, misclassifying a range watch as a single-key point query.
etcd serves as the primary distributed key-value store for Kubernetes clusters, containing highly sensitive state and configuration data. The gRPC Watch API provides a mechanism for clients to stream real-time updates when keys are modified. This system relies on robust Role-Based Access Control (RBAC) to ensure clients only receive updates for keys they are authorized to access.
The vulnerability tracked as GHSA-XG4H-6GFC-H4M8 represents an incorrect authorization mechanism (CWE-863) within etcd's watch stream handler. An authenticated client with minimal privileges—restricted to reading a single specific key—can bypass RBAC checks. This bypass allows the client to watch and receive streamed updates for an arbitrary range of keys lexicographically greater than or equal to the authorized key.
Because etcd typically contains critical cluster metadata, secrets, and configurations, unauthorized access to the keyspace exposes the entire cluster infrastructure to compromise. The attack surface is exposed via the gRPC network port (typically 2379) to any user with valid credentials. This report details the technical mechanics of the premature parameter normalization that causes this authorization bypass.
The core defect lies in the sequence of operations inside the recvLoop method of the serverWatchStream struct in server/etcdserver/api/v3rpc/watch.go. In the gRPC Watch API, clients request open-ended range queries (e.g., watching all keys starting from a specific key, represented as [Key, +inf)) by setting the RangeEnd parameter to a null byte \x00 sentinel. The Go client (clientv3) provides this abstract capability through the clientv3.WithFromKey() option.
In vulnerable versions of etcd, the gRPC handler performed normalization on the RangeEnd buffer before validating user authorizations. Specifically, the watch stream handler modified an incoming range request by rewriting the \x00 sentinel (a byte slice of length 1 containing zero) to an empty byte slice ([]byte{}). This normalization was intended to match internal MVCC engine representation requirements where an empty slice denotes an open-ended upper bound.
However, the RBAC authorization layer, implemented in server/auth/range_perm_cache.go, evaluates permissions after this normalization block. The function isRangeOpPermitted differentiates between a point query (a single key) and a range query by inspecting the length of RangeEnd. When RangeEnd is normalized to []byte{}, its length becomes 0, leading the authorization logic to process the request as a point query for the single key Key.
Consequently, if an attacker has legitimate read authorization for the singular key "foo", the RBAC engine verifies that point permission and permits the request. When the backend MVCC storage layer subsequently executes the watch, it correctly interprets the empty RangeEnd slice as an open-ended range query, establishing a stream for all keys starting from "foo" to infinity.
To understand the specific vulnerability mechanics, we must examine the implementation of the watch request receiver loop. The original, vulnerable implementation of serverWatchStream.recvLoop() performed the sanitization and rewriting of RangeEnd before validating the user permissions against the RPC rules. Below is a code trace of the vulnerable sequence:
// Vulnerable Order in server/etcdserver/api/v3rpc/watch.go
if len(creq.RangeEnd) == 0 {
creq.RangeEnd = nil
}
if len(creq.RangeEnd) == 1 && creq.RangeEnd[0] == 0 {
// Premature normalization converts sentinel \x00 to empty slice []byte{}
creq.RangeEnd = []byte{}
}
// Downstream authorization check occurs after this transformation
// The RBAC engine evaluates len(creq.RangeEnd) which is now 0The patch modifies this flow by deferring the normalization until after the authorization check and context initialization have completed. The RBAC engine now receives the original, raw request parameters where RangeEnd contains the single null byte []byte{0}. Because the length of this slice is 1 and not 0, the RBAC engine correctly identifies the operation as a range query and demands read permissions for the entire target range.
// Patched Order in server/etcdserver/api/v3rpc/watch.go
// Step 1: Authorization and validation occur here using the unmodified creq.RangeEnd
if err := sws.authStore.IsRangePermitted(ctx, creq.Key, creq.RangeEnd); err != nil {
// The check correctly identifies the wide range and denies unauthorized access
return err
}
// Step 2: Normalization is safely performed downstream for MVCC engine compatibility
if len(creq.RangeEnd) == 0 {
creq.RangeEnd = nil
}
if len(creq.RangeEnd) == 1 && creq.RangeEnd[0] == 0 {
creq.RangeEnd = []byte{}
}This remediation completely resolves the semantic mismatch by ensuring that security policy evaluation is performed on the raw, canonical client-supplied parameters rather than on mutated storage-level representations. The fix is robust because it addresses the architectural separation of concerns between authentication/authorization and database backend storage formats.
An attacker seeking to exploit this vulnerability must possess valid credentials for an account within etcd that has been granted read permission to at least one key. The step-by-step attack path relies on sending a crafted gRPC request over a standard HTTP/2 connection. The attacker initiates a gRPC Watch stream and submits a WatchCreateRequest payload containing the specific authorized key and the open-ended range sentinel.
When the client sends the payload {"key": "foo", "range_end": "\x00"}, the vulnerable server processes the packet. Inside recvLoop(), the field creq.RangeEnd is rewritten from []byte{0} to []byte{}. Next, the authorization check evaluates len(RangeEnd). Finding the length to be zero, it determines the operation is a point query targeting only "foo", which matches the attacker's restricted privileges. The server returns a successful watch creation acknowledgment.
Because the internal engine subsequently registers a watch stream for ["foo", +inf), any modification to keys starting with "foo" or lexicographically higher (such as "foo_secrets", "foobar", or even keys like "g", "h", and beyond) generates events. These modification events, including both key names and values, are streamed back to the unauthorized client. This allows the attacker to systematically harvest sensitive data across the entire following namespace.
The security implications of this authorization bypass are severe, especially in containerized orchestration environments like Kubernetes. Kubernetes utilizes etcd to store the absolute state of the cluster, including ServiceAccount tokens, database credentials, application secrets, and configuration maps. If an attacker gains restricted access to a single namespaces' configuration key, they can leverage this vulnerability to dump secrets and tokens of adjacent namespaces.
An administrative boundaries breach is highly likely when etcd RBAC is used to multi-tenant a cluster. For example, a tenant limited to their own prefix (e.g., "tenant-a/") can request a watch from "tenant-a/" with RangeEnd = "\x00". The bypass validates their permission to "tenant-a/" but registers a watch for ["tenant-a/", +inf), exposing "tenant-b/", "tenant-c/", and system configurations.
The qualitative CVSS v3.1 score is evaluated at 6.5 (Medium/High severity) with vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N. The attack complexity is low, requiring no special system state or race conditions. Although it requires authentication, any low-privileged account is sufficient to initiate the bypass, resulting in complete loss of confidentiality for all keys following the compromised index.
Detecting active exploitation of GHSA-XG4H-6GFC-H4M8 requires comprehensive monitoring of etcd's gRPC request streams. Organizations should enable etcd audit logging and inspect incoming Watch requests. Security Operations Centers (SOC) should configure detections targeting watch requests where the client provides a Key parameter and a RangeEnd parameter equal to []byte{0} (on the wire, \x00), particularly when requested by non-administrative accounts.
Remediation requires upgrading the etcd cluster to a patched release. The fix has been backported to all active release branches:
For environments where immediate patching is unfeasible, administrators should minimize the usage of fine-grained, prefix-adjacent single-key read permissions. Implementing network segmentations to restrict gRPC client port access (typically TCP 2379) to authorized workloads and nodes also limits the exposure of the vulnerable interface. To verify compliance, security teams can run the integration-level watch permission test implemented in etcd's test suite to assert that unauthorized range queries are correctly met with a permission-denied error.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
etcd etcd-io | >= 3.5.0, < 3.5.33 | v3.5.33 |
etcd etcd-io | >= 3.6.0, < 3.6.14 | v3.6.14 |
etcd etcd-io | >= 3.7.0, < 3.7.1 | v3.7.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-863 (Incorrect Authorization) |
| Attack Vector | Network (gRPC API) |
| CVSS Score | 6.5 (Medium) |
| Exploit Status | PoC available via etcd integration tests |
| KEV Status | Not listed |
| Impact | Unauthorized read access to adjacent or global key namespaces |
| Affected Component | gRPC Watch API (recvLoop in watch.go) |
The software performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly obtain the correct results, leading to a bypass.
A prototype pollution vulnerability (CWE-1321) in the Quasar framework's utility function 'extend' allows unauthenticated remote attackers to modify the global Object.prototype. This vulnerability can lead to application-level logic bypasses, denial of service, or potentially arbitrary code execution depending on downstream implementation.
A critical connection-level Denial of Service (DoS) vulnerability exists in the Yamux stream multiplexer implementation of py-libp2p (versions <= 0.6.0). The flaw allows unauthenticated or authenticated peers to permanently stall a Yamux multiplexed connection by transmitting a single malformed 12-byte header claiming an oversized payload while withholding the payload bytes.
An unauthenticated remote resource exhaustion vulnerability in Amazon aws-smithy-http-server enables denial-of-service (DoS) attacks. Affected versions do not enforce connection limits or header timeouts, allowing standard Slowloris techniques to block server operations.
An argument injection vulnerability in the AWS Bedrock AgentCore Python SDK allows authenticated users to execute arbitrary commands inside the Code Interpreter sandbox container via crafted Python package specifiers containing shell metacharacters.
A NULL pointer dereference vulnerability was discovered in the getkin/kin-openapi Go library. When parsing incoming request parameters that are validated against a content map with an empty media type, the openapi3filter request validation engine attempts to resolve an uninitialized schema pointer. This results in an unhandled Go runtime panic and process termination, yielding an unauthenticated, remote Denial of Service vector.
A Server-Side Request Forgery (SSRF) vulnerability in FrontMCP allows unauthenticated remote attackers to query internal network services by exploiting the un-guarded background OpenAPI specification polling mechanism.