Restrict internal secret adoption to outbound verified links

The cluster bus protocol is unauthenticated by design and not meant to
be exposed to untrusted networks. However, the internal secret update
logic blindly accepted INTERNALSECRET extensions from any connection,
including inbound ones initiated by external parties. An attacker with
access to the bus port could send a crafted PING spoofing a known node
name with an all-zeros secret, causing the server to adopt it. The
attacker could then authenticate on the client port via
AUTH "internal connection" <zeros>, bypassing requirepass and ACLs.

Only accept internal secret updates from outbound links to fully
handshaken nodes, i.e. connections we initiated to verified cluster
peers.
This commit is contained in:
Slavomir Kaslev 2026-05-05 13:43:54 +03:00
parent 7cf63635f0
commit 15cbe73d84
2 changed files with 92 additions and 1 deletions

View file

@ -2715,7 +2715,9 @@ void clusterProcessPingExtensions(clusterMsg *hdr, clusterLink *link) {
ext_shardid = shardid_ext->shard_id;
} else if (type == CLUSTERMSG_EXT_TYPE_INTERNALSECRET) {
clusterMsgPingExtInternalSecret *internal_secret_ext = (clusterMsgPingExtInternalSecret *) &(ext->ext[0].internal_secret);
if (memcmp(server.cluster->internal_secret, internal_secret_ext->internal_secret, CLUSTER_INTERNALSECRETLEN) > 0 ) {
if (!link->inbound && link->node && !nodeInHandshake(link->node) &&
memcmp(server.cluster->internal_secret, internal_secret_ext->internal_secret, CLUSTER_INTERNALSECRETLEN) > 0)
{
memcpy(server.cluster->internal_secret, internal_secret_ext->internal_secret, CLUSTER_INTERNALSECRETLEN);
}
} else {

View file

@ -15,6 +15,65 @@ proc wait_for_secret_sync {maxtries delay num_nodes} {
}
}
# Build a raw cluster bus PING packet with an INTERNALSECRET extension.
# sender_name: 40-char hex node ID to spoof as the sender.
# secret: 40-byte binary string to inject as the internal secret.
# client_port: the client port to announce.
proc build_cluster_bus_ping_with_secret {sender_name secret client_port} {
set CLUSTER_NAMELEN 40
set CLUSTER_SLOTS 16384
set NET_IP_STR_LEN 46
set CLUSTERMSG_TYPE_PING 0
set CLUSTERMSG_EXT_TYPE_INTERNALSECRET 4
set CLUSTERMSG_FLAG0_EXT_DATA 4
set CLUSTER_INTERNALSECRETLEN 40
# Extension: length(4) + type(2) + unused(2) + secret(40) = 48 bytes
set ext_len 48
set ext [binary format ISS $ext_len $CLUSTERMSG_EXT_TYPE_INTERNALSECRET 0]
append ext $secret
set base_header_size 2256
set totlen [expr {$base_header_size + $ext_len}]
set cport [expr {$client_port + 10000}]
# Pad sender to CLUSTER_NAMELEN
set sender_padded [binary format a${CLUSTER_NAMELEN} $sender_name]
# Build header fields up to the data section
set hdr {}
append hdr "RCmb"
append hdr [binary format I $totlen]
append hdr [binary format S 1] ;# ver
append hdr [binary format S $client_port] ;# port
append hdr [binary format S $CLUSTERMSG_TYPE_PING] ;# type
append hdr [binary format S 0] ;# count (0 gossip entries)
append hdr [binary format W 0] ;# currentEpoch
append hdr [binary format W 0] ;# configEpoch
append hdr [binary format W 0] ;# offset
append hdr $sender_padded ;# sender
append hdr [string repeat "\x00" [expr {$CLUSTER_SLOTS / 8}]] ;# myslots
append hdr [string repeat "\x00" $CLUSTER_NAMELEN] ;# slaveof
set myip [binary format a${NET_IP_STR_LEN} "127.0.0.1"]
append hdr $myip ;# myip
append hdr [binary format S 1] ;# extensions count
append hdr [string repeat "\x00" 30] ;# notused1
append hdr [binary format S 0] ;# pport
append hdr [binary format S $cport] ;# cport
append hdr [binary format S 1] ;# flags (CLUSTER_NODE_MASTER)
append hdr [binary format c 0] ;# state
append hdr [binary format ccc $CLUSTERMSG_FLAG0_EXT_DATA 0 0] ;# mflags
# Pad to base_header_size
set cur_len [string length $hdr]
if {$cur_len < $base_header_size} {
append hdr [string repeat "\x00" [expr {$base_header_size - $cur_len}]]
}
append hdr $ext
return $hdr
}
start_cluster 3 3 {tags {external:skip cluster}} {
test "Test internal secret sync" {
wait_for_secret_sync 50 100 6
@ -69,3 +128,33 @@ start_cluster 3 3 {tags {external:skip cluster}} {
}
}
}
start_cluster 1 0 {tags {external:skip cluster}} {
test "Inbound cluster bus connection cannot inject a forged internal secret" {
set host [srv 0 host]
set port [srv 0 port]
set cport [expr {$port + 10000}]
set node_id [R 0 CLUSTER MYID]
set secret_before [R 0 debug internal_secret]
# Open a raw TCP socket to the cluster bus and send a forged PING
# spoofing the server's own node name, with a zero-byte secret that
# would win the lexicographic comparison against any real secret.
set zero_secret [string repeat "\x00" 40]
set pkt [build_cluster_bus_ping_with_secret $node_id $zero_secret $port]
set fd [socket $host $cport]
fconfigure $fd -translation binary -buffering full
puts -nonewline $fd $pkt
flush $fd
after 500
close $fd
# The internal secret must not have changed.
set secret_after [R 0 debug internal_secret]
assert_equal $secret_before $secret_after
# AUTH with the forged zero-byte secret must be rejected.
assert_error {*WRONGPASS*} {R 0 auth "internal connection" $zero_secret}
}
}