mirror of
https://github.com/redis/redis.git
synced 2026-06-09 08:55:06 -04:00
Fix Sentinel config injection via SENTINEL SET (#14970)
Reject control characters (0x00-0x1F, 0x7F) in user-controlled string arguments to SENTINEL SET, SENTINEL MONITOR, and SENTINEL CONFIG SET to prevent newline injection into the persisted config file. An attacker with access to SENTINEL SET could inject arbitrary config directives (e.g. notification-script) by embedding \r\n in auth-pass or similar fields, leading to code execution on restart. As a defense-in-depth measure, config serialization now uses sdscatrepr (via sentinelSdscatConfigArg) for all user-controlled string fields when they contain characters that require escaping. Simple values remain unquoted for backward compatibility with older config parsers. Add comprehensive Sentinel tests (16-config-injection.tcl) covering control character rejection for all affected commands, verification that injection payloads do not pollute the config file, round-trip persistence of values containing spaces and quotes through restart, and backward compatibility with the old unquoted config format.
This commit is contained in:
parent
2432f55278
commit
3e1afec688
2 changed files with 393 additions and 27 deletions
108
src/sentinel.c
108
src/sentinel.c
|
|
@ -458,6 +458,25 @@ const char *preMonitorCfgName[] = {
|
|||
"announce-hostnames"
|
||||
};
|
||||
|
||||
/* Returns 1 if the string contains control characters (0x00-0x1F or 0x7F),
|
||||
* which must be rejected to prevent config injection via newlines/etc. */
|
||||
int sentinelStringContainsControlChars(sds s) {
|
||||
for (size_t i = 0; i < sdslen(s); i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c < 0x20 || c == 0x7F) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Append an sds value to dest, quoting it with sdscatrepr only if the value
|
||||
* contains characters that need escaping (spaces, quotes, control chars, etc.).
|
||||
* Simple values are appended as-is, preserving the traditional config format. */
|
||||
static sds sentinelSdscatConfigArg(sds dest, sds value) {
|
||||
if (sdsneedsrepr(value))
|
||||
return sdscatrepr(dest, value, sdslen(value));
|
||||
return sdscatsds(dest, value);
|
||||
}
|
||||
|
||||
/* This function overwrites a few normal Redis config default with Sentinel
|
||||
* specific defaults. */
|
||||
void initSentinelConfig(void) {
|
||||
|
|
@ -2048,8 +2067,13 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
/* sentinel monitor */
|
||||
master = dictGetVal(de);
|
||||
master_addr = sentinelGetCurrentMasterAddress(master);
|
||||
|
||||
/* Pre-compute the safely-formatted master name for config serialization.
|
||||
* Only quoted if it contains characters requiring escaping. */
|
||||
sds qname = sentinelSdscatConfigArg(sdsempty(), master->name);
|
||||
|
||||
line = sdscatprintf(sdsempty(),"sentinel monitor %s %s %d %d",
|
||||
master->name, announceSentinelAddr(master_addr), master_addr->port,
|
||||
qname, announceSentinelAddr(master_addr), master_addr->port,
|
||||
master->quorum);
|
||||
rewriteConfigRewriteLine(state,"sentinel monitor",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
|
|
@ -2058,7 +2082,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
if (master->down_after_period != sentinel_default_down_after) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel down-after-milliseconds %s %ld",
|
||||
master->name, (long) master->down_after_period);
|
||||
qname, (long) master->down_after_period);
|
||||
rewriteConfigRewriteLine(state,"sentinel down-after-milliseconds",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
|
@ -2067,7 +2091,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
if (master->failover_timeout != sentinel_default_failover_timeout) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel failover-timeout %s %ld",
|
||||
master->name, (long) master->failover_timeout);
|
||||
qname, (long) master->failover_timeout);
|
||||
rewriteConfigRewriteLine(state,"sentinel failover-timeout",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
|
||||
|
|
@ -2077,42 +2101,38 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
if (master->parallel_syncs != SENTINEL_DEFAULT_PARALLEL_SYNCS) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel parallel-syncs %s %d",
|
||||
master->name, master->parallel_syncs);
|
||||
qname, master->parallel_syncs);
|
||||
rewriteConfigRewriteLine(state,"sentinel parallel-syncs",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
||||
/* sentinel notification-script */
|
||||
if (master->notification_script) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel notification-script %s %s",
|
||||
master->name, master->notification_script);
|
||||
line = sdscatprintf(sdsempty(), "sentinel notification-script %s ", qname);
|
||||
line = sentinelSdscatConfigArg(line, master->notification_script);
|
||||
rewriteConfigRewriteLine(state,"sentinel notification-script",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
||||
/* sentinel client-reconfig-script */
|
||||
if (master->client_reconfig_script) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel client-reconfig-script %s %s",
|
||||
master->name, master->client_reconfig_script);
|
||||
line = sdscatprintf(sdsempty(), "sentinel client-reconfig-script %s ", qname);
|
||||
line = sentinelSdscatConfigArg(line, master->client_reconfig_script);
|
||||
rewriteConfigRewriteLine(state,"sentinel client-reconfig-script",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
||||
/* sentinel auth-pass & auth-user */
|
||||
if (master->auth_pass) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel auth-pass %s %s",
|
||||
master->name, master->auth_pass);
|
||||
line = sdscatprintf(sdsempty(), "sentinel auth-pass %s ", qname);
|
||||
line = sentinelSdscatConfigArg(line, master->auth_pass);
|
||||
rewriteConfigRewriteLine(state,"sentinel auth-pass",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
||||
if (master->auth_user) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel auth-user %s %s",
|
||||
master->name, master->auth_user);
|
||||
line = sdscatprintf(sdsempty(), "sentinel auth-user %s ", qname);
|
||||
line = sentinelSdscatConfigArg(line, master->auth_user);
|
||||
rewriteConfigRewriteLine(state,"sentinel auth-user",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
|
@ -2121,7 +2141,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
if (master->master_reboot_down_after_period != 0) {
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel master-reboot-down-after-period %s %ld",
|
||||
master->name, (long) master->master_reboot_down_after_period);
|
||||
qname, (long) master->master_reboot_down_after_period);
|
||||
rewriteConfigRewriteLine(state,"sentinel master-reboot-down-after-period",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
|
@ -2129,7 +2149,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
/* sentinel config-epoch */
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel config-epoch %s %llu",
|
||||
master->name, (unsigned long long) master->config_epoch);
|
||||
qname, (unsigned long long) master->config_epoch);
|
||||
rewriteConfigRewriteLine(state,"sentinel config-epoch",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
|
||||
|
|
@ -2137,7 +2157,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
/* sentinel leader-epoch */
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel leader-epoch %s %llu",
|
||||
master->name, (unsigned long long) master->leader_epoch);
|
||||
qname, (unsigned long long) master->leader_epoch);
|
||||
rewriteConfigRewriteLine(state,"sentinel leader-epoch",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
|
||||
|
|
@ -2158,7 +2178,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
slave_addr = master->addr;
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel known-replica %s %s %d",
|
||||
master->name, announceSentinelAddr(slave_addr), slave_addr->port);
|
||||
qname, announceSentinelAddr(slave_addr), slave_addr->port);
|
||||
/* try to replace any known-slave option first if found */
|
||||
if (rewriteConfigRewriteLine(state, "sentinel known-slave", sdsdup(line), 0) == 0) {
|
||||
rewriteConfigRewriteLine(state, "sentinel known-replica", line, 1);
|
||||
|
|
@ -2176,7 +2196,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
if (ri->runid == NULL) continue;
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel known-sentinel %s %s %d %s",
|
||||
master->name, announceSentinelAddr(ri->addr), ri->addr->port, ri->runid);
|
||||
qname, announceSentinelAddr(ri->addr), ri->addr->port, ri->runid);
|
||||
rewriteConfigRewriteLine(state,"sentinel known-sentinel",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
|
|
@ -2187,13 +2207,16 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
while((de = dictNext(&di2)) != NULL) {
|
||||
sds oldname = dictGetKey(de);
|
||||
sds newname = dictGetVal(de);
|
||||
line = sdscatprintf(sdsempty(),
|
||||
"sentinel rename-command %s %s %s",
|
||||
master->name, oldname, newname);
|
||||
line = sdscatprintf(sdsempty(), "sentinel rename-command %s ", qname);
|
||||
line = sentinelSdscatConfigArg(line, oldname);
|
||||
line = sdscatlen(line, " ", 1);
|
||||
line = sentinelSdscatConfigArg(line, newname);
|
||||
rewriteConfigRewriteLine(state,"sentinel rename-command",line,1);
|
||||
/* rewriteConfigMarkAsProcessed is handled after the loop */
|
||||
}
|
||||
dictResetIterator(&di2);
|
||||
|
||||
sdsfree(qname);
|
||||
}
|
||||
|
||||
/* sentinel current-epoch is a global state valid for all the masters. */
|
||||
|
|
@ -2221,7 +2244,8 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
|
||||
/* sentinel sentinel-user. */
|
||||
if (sentinel.sentinel_auth_user) {
|
||||
line = sdscatprintf(sdsempty(), "sentinel sentinel-user %s", sentinel.sentinel_auth_user);
|
||||
line = sdsnew("sentinel sentinel-user ");
|
||||
line = sentinelSdscatConfigArg(line, sentinel.sentinel_auth_user);
|
||||
rewriteConfigRewriteLine(state,"sentinel sentinel-user",line,1);
|
||||
} else {
|
||||
rewriteConfigMarkAsProcessed(state,"sentinel sentinel-user");
|
||||
|
|
@ -2229,10 +2253,11 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) {
|
|||
|
||||
/* sentinel sentinel-pass. */
|
||||
if (sentinel.sentinel_auth_pass) {
|
||||
line = sdscatprintf(sdsempty(), "sentinel sentinel-pass %s", sentinel.sentinel_auth_pass);
|
||||
line = sdsnew("sentinel sentinel-pass ");
|
||||
line = sentinelSdscatConfigArg(line, sentinel.sentinel_auth_pass);
|
||||
rewriteConfigRewriteLine(state,"sentinel sentinel-pass",line,1);
|
||||
} else {
|
||||
rewriteConfigMarkAsProcessed(state,"sentinel sentinel-pass");
|
||||
rewriteConfigMarkAsProcessed(state,"sentinel sentinel-pass");
|
||||
}
|
||||
|
||||
dictResetIterator(&di);
|
||||
|
|
@ -3238,6 +3263,11 @@ void sentinelConfigSetCommand(client *c) {
|
|||
if (!(!strcasecmp(val->ptr, "debug") || !strcasecmp(val->ptr, "verbose") ||
|
||||
!strcasecmp(val->ptr, "notice") || !strcasecmp(val->ptr, "warning") ||
|
||||
!strcasecmp(val->ptr, "nothing"))) goto badfmt;
|
||||
} else if (!strcasecmp(option, "announce-ip")) {
|
||||
if (sentinelStringContainsControlChars(val->ptr)) {
|
||||
addReplyErrorFormat(c, "'%s' must not contain control characters", option);
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4045,6 +4075,11 @@ NULL
|
|||
return;
|
||||
}
|
||||
|
||||
if (sentinelStringContainsControlChars(c->argv[2]->ptr)) {
|
||||
addReplyError(c, "Master name must not contain control characters");
|
||||
return;
|
||||
}
|
||||
|
||||
/* If resolve-hostnames is used, actual DNS resolution may take place.
|
||||
* Otherwise just validate address.
|
||||
*/
|
||||
|
|
@ -4388,6 +4423,12 @@ void sentinelSetCommand(client *c) {
|
|||
goto seterr;
|
||||
}
|
||||
|
||||
if (sentinelStringContainsControlChars(value)) {
|
||||
addReplyError(c,
|
||||
"notification-script must not contain control characters");
|
||||
goto seterr;
|
||||
}
|
||||
|
||||
if (strlen(value) && access(value,X_OK) == -1) {
|
||||
addReplyError(c,
|
||||
"Notification script seems non existing or non executable");
|
||||
|
|
@ -4407,6 +4448,12 @@ void sentinelSetCommand(client *c) {
|
|||
goto seterr;
|
||||
}
|
||||
|
||||
if (sentinelStringContainsControlChars(value)) {
|
||||
addReplyError(c,
|
||||
"client-reconfig-script must not contain control characters");
|
||||
goto seterr;
|
||||
}
|
||||
|
||||
if (strlen(value) && access(value,X_OK) == -1) {
|
||||
addReplyError(c,
|
||||
"Client reconfiguration script seems non existing or "
|
||||
|
|
@ -4450,6 +4497,13 @@ void sentinelSetCommand(client *c) {
|
|||
goto badfmt;
|
||||
}
|
||||
|
||||
if (sentinelStringContainsControlChars(oldname) ||
|
||||
sentinelStringContainsControlChars(newname)) {
|
||||
addReplyError(c,
|
||||
"rename-command arguments must not contain control characters");
|
||||
goto seterr;
|
||||
}
|
||||
|
||||
/* Remove any older renaming for this command. */
|
||||
dictDelete(ri->renamed_commands,oldname);
|
||||
|
||||
|
|
|
|||
312
tests/sentinel/tests/16-config-injection.tcl
Normal file
312
tests/sentinel/tests/16-config-injection.tcl
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# Test that control characters are rejected where appropriate, and that
|
||||
# string values are safely quoted when persisted to disk.
|
||||
#
|
||||
# Config injection is prevented by sentinelSdscatConfigArg(), which escapes
|
||||
# values containing special characters at persistence time. Fields like
|
||||
# notification-script, rename-command, master name, and announce-ip also
|
||||
# reject control characters at input time as an additional safeguard.
|
||||
|
||||
source "../tests/includes/init-tests.tcl"
|
||||
|
||||
# Helper: read the sentinel config file for a given sentinel id.
|
||||
proc read_sentinel_config {id} {
|
||||
set configfile [file join "sentinel_${id}" "sentinel.conf"]
|
||||
set fp [open $configfile r]
|
||||
set content [read $fp]
|
||||
close $fp
|
||||
return $content
|
||||
}
|
||||
|
||||
# Helper: count how many lines in the config match a pattern.
|
||||
proc count_config_lines {content pattern} {
|
||||
set count 0
|
||||
foreach line [split $content "\n"] {
|
||||
if {[string match $pattern $line]} {
|
||||
incr count
|
||||
}
|
||||
}
|
||||
return $count
|
||||
}
|
||||
|
||||
# Helper: restart a (already stopped) sentinel and wait until it responds to PING.
|
||||
proc start_sentinel_and_wait {sid} {
|
||||
restart_instance sentinel $sid
|
||||
wait_for_condition 200 50 {
|
||||
[catch {S $sid PING}] == 0
|
||||
} else {
|
||||
fail "Sentinel $sid did not restart in time"
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: kill sentinel, restart it, and wait until it responds to PING.
|
||||
proc restart_sentinel_and_wait {sid} {
|
||||
kill_instance sentinel $sid
|
||||
start_sentinel_and_wait $sid
|
||||
}
|
||||
|
||||
# Helper: assert that the sentinel config file contains the expected substring.
|
||||
proc assert_config_contains {sid expected} {
|
||||
set content [read_sentinel_config $sid]
|
||||
assert {[string first $expected $content] >= 0}
|
||||
}
|
||||
|
||||
# Helper: append lines to a sentinel's config file (sentinel must be stopped).
|
||||
proc append_to_sentinel_config {sid lines} {
|
||||
set configfile [file join "sentinel_${sid}" "sentinel.conf"]
|
||||
set fp [open $configfile a]
|
||||
foreach line $lines {
|
||||
puts $fp $line
|
||||
}
|
||||
close $fp
|
||||
}
|
||||
|
||||
# Helper: create an executable script with spaces in its path.
|
||||
# Returns the full path. Caller should "file delete -force" the directory.
|
||||
proc create_script_with_spaces {sid} {
|
||||
set script_dir [file join [pwd] "sentinel_${sid}" "script dir"]
|
||||
file mkdir $script_dir
|
||||
set script_path [file join $script_dir "my script.sh"]
|
||||
set fp [open $script_path w]
|
||||
puts $fp "#!/bin/sh"
|
||||
close $fp
|
||||
file attributes $script_path -permissions 0755
|
||||
return $script_path
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 1: Control character rejection in SENTINEL SET
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "SENTINEL SET notification-script rejects control characters" {
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL SET mymaster notification-script "/tmp/ok\n/tmp/evil.sh"
|
||||
}
|
||||
}
|
||||
|
||||
test "SENTINEL SET client-reconfig-script rejects control characters" {
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL SET mymaster client-reconfig-script "/tmp/ok\n/tmp/evil.sh"
|
||||
}
|
||||
}
|
||||
|
||||
test "SENTINEL SET rename-command rejects control characters" {
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL SET mymaster rename-command "CONFIG\nEVIL" "NEWCONFIG"
|
||||
}
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL SET mymaster rename-command "CONFIG" "NEW\nCONFIG"
|
||||
}
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 2: Control character rejection in SENTINEL MONITOR
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "SENTINEL MONITOR rejects master name with control characters" {
|
||||
set port [get_instance_attrib redis 0 port]
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL MONITOR "bad\nmaster" 127.0.0.1 $port 2
|
||||
}
|
||||
assert_error "*must not contain control characters*" {
|
||||
S 0 SENTINEL MONITOR "bad\rmaster" 127.0.0.1 $port 2
|
||||
}
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 3: Control character rejection in SENTINEL CONFIG SET
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "SENTINEL CONFIG SET announce-ip rejects control characters" {
|
||||
catch {S 0 SENTINEL CONFIG SET announce-ip "1.2.3.4\nevil-directive"} e
|
||||
assert_match "*must not contain control characters*" $e
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 4: Config injection attempt does not pollute config file
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "Newline injection in auth-pass does not pollute config file" {
|
||||
# Auth-pass accepts control characters, but sentinelSdscatConfigArg
|
||||
# escapes them at persistence time, preventing config injection.
|
||||
S 0 SENTINEL SET mymaster auth-pass "x\nsentinel notification-script mymaster /tmp/evil.sh"
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
assert {[count_config_lines $content "sentinel notification-script mymaster /tmp/evil.sh"] == 0}
|
||||
assert_config_contains 0 {sentinel auth-pass mymaster "x\nsentinel notification-script mymaster /tmp/evil.sh"}
|
||||
S 0 SENTINEL SET mymaster auth-pass ""
|
||||
}
|
||||
|
||||
test "Newline injection in auth-user does not pollute config file" {
|
||||
S 0 SENTINEL SET mymaster auth-user "x\nsentinel notification-script mymaster /tmp/evil.sh"
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
assert {[count_config_lines $content "sentinel notification-script mymaster /tmp/evil.sh"] == 0}
|
||||
assert_config_contains 0 {sentinel auth-user mymaster "x\nsentinel notification-script mymaster /tmp/evil.sh"}
|
||||
S 0 SENTINEL SET mymaster auth-user ""
|
||||
}
|
||||
|
||||
test "Newline injection in sentinel-pass does not pollute config file" {
|
||||
S 0 SENTINEL CONFIG SET sentinel-pass "x\nsentinel notification-script mymaster /tmp/evil.sh"
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
assert {[count_config_lines $content "sentinel notification-script mymaster /tmp/evil.sh"] == 0}
|
||||
assert_config_contains 0 {sentinel sentinel-pass "x\nsentinel notification-script mymaster /tmp/evil.sh"}
|
||||
S 0 SENTINEL CONFIG SET sentinel-pass ""
|
||||
}
|
||||
|
||||
test "Newline injection in sentinel-user does not pollute config file" {
|
||||
S 0 SENTINEL CONFIG SET sentinel-user "x\nsentinel notification-script mymaster /tmp/evil.sh"
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
assert {[count_config_lines $content "sentinel notification-script mymaster /tmp/evil.sh"] == 0}
|
||||
assert_config_contains 0 {sentinel sentinel-user "x\nsentinel notification-script mymaster /tmp/evil.sh"}
|
||||
S 0 SENTINEL CONFIG SET sentinel-user ""
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 5: Values with special characters survive config round-trip
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "auth-pass with special characters persists correctly through restart" {
|
||||
S 0 SENTINEL SET mymaster auth-pass {my "comp#$&^`'!,lex pass}
|
||||
set expected {sentinel auth-pass mymaster "my \"comp#$&^`'!,lex pass"}
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
restart_sentinel_and_wait 0
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
S 0 SENTINEL SET mymaster auth-pass ""
|
||||
}
|
||||
|
||||
test "auth-user with spaces persists correctly through restart" {
|
||||
S 0 SENTINEL SET mymaster auth-user {user with spaces}
|
||||
set expected {sentinel auth-user mymaster "user with spaces"}
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
restart_sentinel_and_wait 0
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
S 0 SENTINEL SET mymaster auth-user ""
|
||||
}
|
||||
|
||||
test "notification-script with spaces persists correctly through restart" {
|
||||
set script_path [create_script_with_spaces 0]
|
||||
S 0 SENTINEL SET mymaster notification-script $script_path
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
# The path must be quoted since it contains spaces.
|
||||
assert {[string first "notification-script" $content] >= 0}
|
||||
restart_sentinel_and_wait 0
|
||||
set info [S 0 SENTINEL MASTER mymaster]
|
||||
set idx [lsearch $info "notification-script"]
|
||||
assert {$idx >= 0}
|
||||
assert_equal [lindex $info [expr {$idx+1}]] $script_path
|
||||
S 0 SENTINEL SET mymaster notification-script ""
|
||||
file delete -force [file dirname $script_path]
|
||||
}
|
||||
|
||||
test "client-reconfig-script with spaces persists correctly through restart" {
|
||||
set script_path [create_script_with_spaces 0]
|
||||
S 0 SENTINEL SET mymaster client-reconfig-script $script_path
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
set content [read_sentinel_config 0]
|
||||
# The path must be quoted since it contains spaces.
|
||||
assert {[string first "client-reconfig-script" $content] >= 0}
|
||||
restart_sentinel_and_wait 0
|
||||
set info [S 0 SENTINEL MASTER mymaster]
|
||||
set idx [lsearch $info "client-reconfig-script"]
|
||||
assert {$idx >= 0}
|
||||
assert_equal [lindex $info [expr {$idx+1}]] $script_path
|
||||
S 0 SENTINEL SET mymaster client-reconfig-script ""
|
||||
file delete -force [file dirname $script_path]
|
||||
}
|
||||
|
||||
test "rename-command persists unquoted through restart" {
|
||||
S 0 SENTINEL SET mymaster rename-command CONFIG CONF_RENAMED
|
||||
set expected {sentinel rename-command mymaster CONFIG CONF_RENAMED}
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
restart_sentinel_and_wait 0
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
S 0 SENTINEL SET mymaster rename-command CONFIG CONFIG
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 6: Backward compatibility -- old unquoted config format still loads
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "Old unquoted config format for auth-pass and auth-user loads correctly" {
|
||||
kill_instance sentinel 0
|
||||
append_to_sentinel_config 0 {
|
||||
"sentinel auth-pass mymaster oldformatpass"
|
||||
"sentinel auth-user mymaster oldformatuser"
|
||||
}
|
||||
start_sentinel_and_wait 0
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 "sentinel auth-pass mymaster oldformatpass"
|
||||
assert_config_contains 0 "sentinel auth-user mymaster oldformatuser"
|
||||
S 0 SENTINEL SET mymaster auth-pass ""
|
||||
S 0 SENTINEL SET mymaster auth-user ""
|
||||
}
|
||||
|
||||
test "Old unquoted config format for rename-command loads correctly" {
|
||||
kill_instance sentinel 0
|
||||
append_to_sentinel_config 0 {
|
||||
"sentinel rename-command mymaster CONFIG NEWCONFIGNAME"
|
||||
}
|
||||
start_sentinel_and_wait 0
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 "sentinel rename-command mymaster CONFIG NEWCONFIGNAME"
|
||||
S 0 SENTINEL SET mymaster rename-command CONFIG CONFIG
|
||||
}
|
||||
|
||||
test "Old unquoted config format for sentinel-pass loads correctly" {
|
||||
kill_instance sentinel 0
|
||||
append_to_sentinel_config 0 {
|
||||
"sentinel sentinel-pass oldsentinelpass"
|
||||
}
|
||||
start_sentinel_and_wait 0
|
||||
set result [S 0 SENTINEL CONFIG GET sentinel-pass]
|
||||
assert_equal [lindex $result 1] "oldsentinelpass"
|
||||
S 0 SENTINEL CONFIG SET sentinel-pass ""
|
||||
}
|
||||
|
||||
test "Old unquoted config format for sentinel-user loads correctly" {
|
||||
kill_instance sentinel 0
|
||||
append_to_sentinel_config 0 {
|
||||
"sentinel sentinel-user oldsentineluser"
|
||||
}
|
||||
start_sentinel_and_wait 0
|
||||
set result [S 0 SENTINEL CONFIG GET sentinel-user]
|
||||
assert_equal [lindex $result 1] "oldsentineluser"
|
||||
S 0 SENTINEL CONFIG SET sentinel-user ""
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Section 7: Values with special characters survive config round-trip
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
test "sentinel-pass with special characters persists correctly through restart" {
|
||||
set test_pass {sentinel pass word}
|
||||
S 0 SENTINEL CONFIG SET sentinel-pass $test_pass
|
||||
set expected {sentinel sentinel-pass "sentinel pass word"}
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
restart_sentinel_and_wait 0
|
||||
set result [S 0 SENTINEL CONFIG GET sentinel-pass]
|
||||
assert_equal [lindex $result 1] $test_pass
|
||||
S 0 SENTINEL CONFIG SET sentinel-pass ""
|
||||
}
|
||||
|
||||
test "sentinel-user with special characters persists correctly through restart" {
|
||||
set test_user {sentinel user name}
|
||||
S 0 SENTINEL CONFIG SET sentinel-user $test_user
|
||||
set expected {sentinel sentinel-user "sentinel user name"}
|
||||
S 0 SENTINEL FLUSHCONFIG
|
||||
assert_config_contains 0 $expected
|
||||
restart_sentinel_and_wait 0
|
||||
set result [S 0 SENTINEL CONFIG GET sentinel-user]
|
||||
assert_equal [lindex $result 1] $test_user
|
||||
S 0 SENTINEL CONFIG SET sentinel-user ""
|
||||
}
|
||||
Loading…
Reference in a new issue