diff --git a/src/action/a_game.c b/src/action/a_game.c index 64aeff1db..69775c04f 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1660,12 +1660,21 @@ void ReadLrconConfig(void) game.lrcon_config.allowed_cvars_count = 0; game.lrcon_config.modes_count = 0; - // Get config filename from cvar + // Get config filename from cvar. + // Validate value: must be a plain filename within the action/ directory. + // Without this, lrcon_config "../../../etc/crontab" would open arbitrary + // filesystem paths. Also replaces unbounded sprintf with Q_snprintf. lrcon_config_cvar = gi.cvar("lrcon_config", "lrcon.cfg", 0); - if (lrcon_config_cvar->string && *(lrcon_config_cvar->string)) - sprintf(cfgpath, "%s/%s", GAMEVERSION, lrcon_config_cvar->string); - else - sprintf(cfgpath, "%s/%s", GAMEVERSION, "lrcon.cfg"); + { + const char *name = (lrcon_config_cvar->string && *lrcon_config_cvar->string) + ? lrcon_config_cvar->string : "lrcon.cfg"; + if (strstr(name, "..") || strchr(name, '/') || strchr(name, '\\') || + strchr(name, ':')) { + gi.dprintf("LRCON: refusing lrcon_config '%s' — must be a plain filename within action/\n", name); + return; + } + Q_snprintf(cfgpath, sizeof(cfgpath), "%s/%s", GAMEVERSION, name); + } // Try to open config file config_file = fopen(cfgpath, "r"); diff --git a/src/action/a_match.c b/src/action/a_match.c index 1ba9adb27..d5d71c362 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -656,7 +656,10 @@ void Cmd_Teamnone_f(edict_t *ent) return; } - if (gi.argc() < 1) { + /* gi.argc() always returns at least 1 (the command name itself), so the + * previous `< 1` guard was dead code — missing-arg silently fell through + * with playernum=0 from atoi(""). */ + if (gi.argc() < 2) { gi.cprintf(ent, PRINT_HIGH, "You need to provide a playernum for this command\nUse 'playerlist' to get a list of playernums\n"); return; } diff --git a/src/action/p_client.c b/src/action/p_client.c index df182179f..1b27a2724 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3849,11 +3849,14 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) IRC_printf(IRC_T_SERVER, "%n@%s connected", value, ipaddr_buf); } - // LRCON: Check if reconnecting claimer and restore claim + // LRCON: Check if reconnecting claimer and restore claim. + // Use Q_stricmp to match Lrcon_CheckClaimer's case-insensitive compare; + // otherwise a claimer reconnecting as "admin" (was "Admin") passes the + // permission check at command time but silently fails restore here. value = Info_ValueForKey(userinfo, "name"); if (game.lrcon_config.enabled && lrcon_claimer_name->string && *lrcon_claimer_name->string && - !strcmp(lrcon_claimer_name->string, value) && - !strcmp(lrcon_claimer_ip->string, ipaddr_buf)) { + !Q_stricmp(lrcon_claimer_name->string, value) && + !Q_stricmp(lrcon_claimer_ip->string, ipaddr_buf)) { level.lrcon.claimed = true; Q_strncpyz(level.lrcon.claimer_name, lrcon_claimer_name->string, sizeof(level.lrcon.claimer_name)); @@ -3913,7 +3916,12 @@ void ClientDisconnect(edict_t * ent) if (!ent->client) return; - if (esp->value && matchmode->value) { + /* Only fire the captain-disconnect broadcast for actual captains. + * Previously this ran for every disconnect from an esp+matchmode game, + * including spectators (resp.team == NOTEAM), which would read + * teams[NOTEAM].name — empty/stale slot. */ + if (esp->value && matchmode->value && + ent->client->resp.team != NOTEAM && IS_CAPTAIN(ent)) { char tempmsg[128]; // We have to kill him first before he is removed as captain/leader killPlayer(ent, false); diff --git a/src/server/main.c b/src/server/main.c index 8031a6bd0..3e35abce8 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -914,8 +914,14 @@ static void SVC_RulesExt(void) return; } - // Check if we need to rebuild the cache - if (svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { + // Check if we need to rebuild the cache. + // Force a build on first use (chunk_count == 0): otherwise during the + // first RULESEXT_CACHE_TIME (~5s) of uptime, svs.realtime and timestamp + // are both 0 so the staleness check is false and the cache stays empty, + // leaving all chunk replies empty until the threshold passes. + // Mirrors the SVC_StatusExt build-condition. + if (!rulesext_cache.chunk_count || + svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { len = SV_BuildExtendedRules(rulesext_cache.data, sizeof(rulesext_cache.data)); rulesext_cache.total_size = len; rulesext_cache.chunk_count = (len + RULESEXT_CHUNK_SIZE - 1) / RULESEXT_CHUNK_SIZE;