clamd: Add options to toggle SHUTDOWN, RELOAD, STATS and VERSION (#1502)

The `clamd` protocol lacks authentication or authorization controls
needed to limit access to more administrative commands.
Depending on your use case, disabling some commands like `SHUTDOWN`
may improve the security of the scanning daemon.

This commit adds options to enable/disable the `SHUTDOWN`, `RELOAD`,
`STATS` and `VERSION` commands in `clamd.conf`.
When a client sends one of the following commands but it is disabled,
`clamd` will respond with "COMMAND UNAVAILABLE".

The new `clamd.conf` options are:

- `EnableShutdownCommand`: Enable the `SHUTDOWN` command.
  Setting this to no prevents a client to stop `clamd` via the
  protocol.
  Default: yes

- `EnableReloadCommand` Enable the `RELOAD` command.
  Setting this to no prevents a client to reload the database.
  This disables Freshclam's `NotifyClamd` option. 
  `clamd` monitors for database directory changes, so this should 
  Default: yes

- `EnableStatsCommand` Enable the `STATS` command.
  Setting this to no prevents a client from querying statistics.
  This disables the `clamdtop` program.
  Default: yes

- `EnableVersionCommand` Enable the `VERSION` command.
  Setting this to no prevents a client from querying version
  information.
  This disables the `clamdtop` program and will cause `clamdscan` to
  display a warning when using the `--version` option.
  Default: yes

Resolves: https://github.com/Cisco-Talos/clamav/issues/922
Resolves: https://github.com/Cisco-Talos/clamav/issues/1169
Related: https://github.com/Cisco-Talos/clamav/pull/347
This commit is contained in:
ChaoticByte 2025-06-04 16:47:57 +02:00 committed by GitHub
parent e86919789f
commit a3be0d2d45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 169 additions and 16 deletions

View file

@ -551,17 +551,24 @@ int execute_or_dispatch_command(client_conn_t *conn, enum commands cmd, const ch
switch (cmd) {
case COMMAND_SHUTDOWN:
pthread_mutex_lock(&exit_mutex);
progexit = 1;
pthread_mutex_unlock(&exit_mutex);
if (optget(conn->opts, "EnableShutdownCommand")->enabled) {
pthread_mutex_lock(&exit_mutex);
progexit = 1;
pthread_mutex_unlock(&exit_mutex);
} else {
conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
}
return 1;
case COMMAND_RELOAD:
pthread_mutex_lock(&reload_mutex);
reload = 1;
pthread_mutex_unlock(&reload_mutex);
mdprintf(desc, "RELOADING%c", term);
/* we set reload flag, and we'll reload before closing the
* connection */
if (optget(conn->opts, "EnableReloadCommand")->enabled) {
pthread_mutex_lock(&reload_mutex);
reload = 1;
pthread_mutex_unlock(&reload_mutex);
mdprintf(desc, "RELOADING%c", term);
/* we set reload flag, and we'll reload before closing the connection */
} else {
conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
}
return 1;
case COMMAND_PING:
if (conn->group)
@ -570,10 +577,15 @@ int execute_or_dispatch_command(client_conn_t *conn, enum commands cmd, const ch
mdprintf(desc, "PONG%c", term);
return conn->group ? 0 : 1;
case COMMAND_VERSION: {
if (conn->group)
mdprintf(desc, "%u: ", conn->id);
print_ver(desc, conn->term, engine);
return conn->group ? 0 : 1;
if (optget(conn->opts, "EnableVersionCommand")->enabled) {
if (conn->group)
mdprintf(desc, "%u: ", conn->id);
print_ver(desc, conn->term, engine);
return conn->group ? 0 : 1;
} else {
conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
return 1;
}
}
case COMMAND_COMMANDS: {
if (conn->group)
@ -598,9 +610,16 @@ int execute_or_dispatch_command(client_conn_t *conn, enum commands cmd, const ch
conn->mode = MODE_STREAM;
return 0;
}
case COMMAND_STATS: {
if (optget(conn->opts, "EnableStatsCommand")->enabled) {
return dispatch_command(conn, cmd, argument);
} else {
conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
return 1;
}
}
case COMMAND_MULTISCAN:
case COMMAND_CONTSCAN:
case COMMAND_STATS:
case COMMAND_FILDES:
case COMMAND_SCAN:
case COMMAND_INSTREAMSCAN:

View file

@ -357,6 +357,15 @@ int get_clamd_version(const struct optstruct *opts)
logg(LOGG_ERROR, "Error occurred while receiving version information.\n");
break;
}
/* Check if the response was "COMMAND UNAVAILABLE", which means that
clamd has the VERSION command disabled. */
if (len >= 19 && memcmp(buff, "COMMAND UNAVAILABLE", 19) == 0) {
logg(LOGG_WARNING, "VERSION command disabled in clamd, printing the local version.\n");
closesocket(sockd);
return 2;
}
printf("%s\n", buff);
}

View file

@ -110,6 +110,7 @@ static void cleanup(void);
static int send_string_noreconn(conn_t *conn, const char *cmd);
static void send_string(conn_t *conn, const char *cmd);
static int read_version(conn_t *conn);
static int check_stats_available(conn_t *conn);
char *get_ip(const char *ip);
char *get_port(const char *ip);
char *make_ip(const char *host, const char *port);
@ -790,6 +791,7 @@ done:
static int make_connection(const char *soname, conn_t *conn)
{
int rc;
int rv;
if (!soname) {
return -1;
@ -801,8 +803,20 @@ static int make_connection(const char *soname, conn_t *conn)
send_string(conn, "nIDSESSION\nnVERSION\n");
free(conn->version);
conn->version = NULL;
if (!read_version(conn))
return 0;
rv = read_version(conn);
if (rv == -3) {
print_con_info(conn, "VERSION command unavailable, consider enabling it in the clamd configuration.\n");
EXIT_PROGRAM(FAIL_INITIAL_CONN);
} else if (!rv) {
// check if STATS command is available
if (check_stats_available(conn)) {
return 0;
} else {
print_con_info(conn, "STATS command unavailable, consider enabling it in the clamd configuration.\n");
EXIT_PROGRAM(FAIL_INITIAL_CONN);
}
}
/* clamd < 0.95 */
if ((rc = make_connection_real(soname, conn)))
@ -1328,6 +1342,9 @@ static int read_version(conn_t *conn)
return -1;
if (!strcmp(buf, "UNKNOWN COMMAND\n"))
return -2;
// check if VERSION command is available
if (!strcmp(strchr(buf, ':'), ": COMMAND UNAVAILABLE\n"))
return -3;
conn->version = strdup(buf);
OOM_CHECK(conn->version);
@ -1337,6 +1354,17 @@ static int read_version(conn_t *conn)
return 0;
}
static int check_stats_available(conn_t *conn)
{
char buf[1024];
send_string(conn, "nSTATS\n");
if (!recv_line(conn, buf, sizeof(buf)))
return 0;
if (!strcmp(strchr(buf, ':'), ": COMMAND UNAVAILABLE\n"))
return 0;
return 1;
}
static void sigint(int a)
{
UNUSEDPARAM(a);

View file

@ -312,6 +312,14 @@ const struct clam_option __clam_options[] = {
{"TCPSocket", NULL, 0, CLOPT_TYPE_NUMBER, MATCH_NUMBER, -1, NULL, 0, OPT_CLAMD, "A TCP port number the daemon will listen on.", "3310"},
{"EnableShutdownCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the SHUTDOWN command for clamd", "no"},
{"EnableReloadCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the RELOAD command for clamd", "no"},
{"EnableVersionCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the VERSION command for clamd", "yes"},
{"EnableStatsCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the STATS command for clamd", "yes"},
/* FIXME: add a regex for IP addr */
{"TCPAddr", NULL, 0, CLOPT_TYPE_STRING, NULL, -1, NULL, FLAG_MULTIPLE, OPT_CLAMD, "By default clamd binds to INADDR_ANY.\nThis option allows you to restrict the TCP address and provide\nsome degree of protection from the outside world.", "localhost"},

View file

@ -144,6 +144,34 @@ This option allows you to restrict the TCP address and provide some degree of pr
.br
Default: disabled
.TP
\fBEnableShutdownCommand BOOL\fR
Enables the SHUTDOWN command. Setting this to no prevents a client to stop clamd via the protocol.
.br
When disabled, clamd responds to this command with COMMAND UNAVAILABLE.
.br
Default: yes
.TP
\fBEnableReloadCommand BOOL\fR
Enables the RELOAD command. Setting this to no prevents a client to reload the database.
.br
When disabled, clamd responds to this command with COMMAND UNAVAILABLE.
.br
Default: yes
.TP
\fBEnableVersionCommand BOOL\fR
Enables the VERSION command. Setting this to no prevents a client from querying version information.
.br
When disabled, clamd responds to this command with COMMAND UNAVAILABLE.
.br
Default: yes
.TP
\fBEnableStatsCommand BOOL\fR
Enables the STATS command. Setting this to no prevents a client from querying statistics.
.br
When disabled, clamd responds to this command with COMMAND UNAVAILABLE.
.br
Default: yes
.TP
\fBMaxConnectionQueueLength NUMBER\fR
Maximum length the queue of pending connections may grow to.
.br

View file

@ -130,6 +130,31 @@ Example
# Default: no
#TCPAddr localhost
# Enable or disable certain commands.
# Disabling some commands like SHUTDOWN may improve the security of the daemon.
# When a client sends one of the following commands but it is disabled,
# clamd responds with COMMAND UNAVAILABLE.
#
# Enable the SHUTDOWN command.
# Setting this to no prevents a client to stop clamd via the protocol.
# Default: yes
#EnableShutdownCommand no
#
# Enable the RELOAD command
# Setting this to no prevents a client to reload the database.
# Default: yes
#EnableReloadCommand no
#
# Enable the STATS command
# Setting this to no prevents a client from querying statistics.
# Default: yes
#EnableStatsCommand no
#
# Enable the VERSION command
# Setting this to no prevents a client from querying version information.
# Default: yes
#EnableVersionCommand no
# Maximum length the queue of pending connections may grow to.
# Default: 200
#MaxConnectionQueueLength 30

View file

@ -62,6 +62,11 @@ int clamd_connect(const char *cfgfile, const char *option)
return -11;
}
if (!optget(opts, "EnableReloadCommand")->enabled) {
logg(LOGG_WARNING, "Clamd was NOT notified: The RELOAD command is disabled. Consider enabling it in the clamd configuration!\n");
return -1;
}
#ifndef _WIN32
if ((opt = optget(opts, "LocalSocket"))->enabled) {
memset(&server, 0x00, sizeof(server));
@ -163,6 +168,12 @@ int notify(const char *cfgfile)
memset(buff, 0, sizeof(buff));
if ((bread = recv(sockd, buff, sizeof(buff), 0)) > 0) {
if (strstr(buff, "COMMAND UNAVAILABLE")) {
// this will only happen when the running clamd instance has EnableReloadCommand set to no,
// but the config on disk differs (e.g. after a config change without clamd restart)
logg(LOGG_ERROR, "NotifyClamd: RELOAD command unavailable, consider enabling it in the clamd configuration and restarting clamd.\n");
return -1;
}
if (!strstr(buff, "RELOADING")) {
logg(LOGG_ERROR, "NotifyClamd: Unknown answer from clamd: '%s'\n", buff);
closesocket(sockd);

View file

@ -102,6 +102,31 @@ TCPSocket 3310
# Default: no
TCPAddr localhost
# Enable or disable certain commands.
# Disabling some commands like SHUTDOWN may improve the security of the daemon.
# When a client sends one of the following commands but it is disabled,
# clamd responds with COMMAND UNAVAILABLE.
#
# Enable the SHUTDOWN command.
# Setting this to no prevents a client to stop clamd via the protocol.
# Default: yes
#EnableShutdownCommand no
#
# Enable the RELOAD command
# Setting this to no prevents a client to reload the database.
# Default: yes
#EnableReloadCommand no
#
# Enable the STATS command
# Setting this to no prevents a client from querying statistics.
# Default: yes
#EnableStatsCommand no
#
# Enable the VERSION command
# Setting this to no prevents a client from querying version information.
# Default: yes
#EnableVersionCommand no
# Maximum length the queue of pending connections may grow to.
# Default: 200
#MaxConnectionQueueLength 30