From a3be0d2d452023e367f3a5425f88179c732a50cd Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 4 Jun 2025 16:47:57 +0200 Subject: [PATCH] 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 --- clamd/session.c | 47 +++++++++++++++++++-------- clamdscan/client.c | 9 +++++ clamdtop/clamdtop.c | 32 ++++++++++++++++-- common/optparser.c | 8 +++++ docs/man/clamd.conf.5.in | 28 ++++++++++++++++ etc/clamd.conf.sample | 25 ++++++++++++++ freshclam/notify.c | 11 +++++++ win32/conf_examples/clamd.conf.sample | 25 ++++++++++++++ 8 files changed, 169 insertions(+), 16 deletions(-) diff --git a/clamd/session.c b/clamd/session.c index 53706dcbe..e01359be5 100644 --- a/clamd/session.c +++ b/clamd/session.c @@ -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: diff --git a/clamdscan/client.c b/clamdscan/client.c index 54f28c225..7ab0d23a8 100644 --- a/clamdscan/client.c +++ b/clamdscan/client.c @@ -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); } diff --git a/clamdtop/clamdtop.c b/clamdtop/clamdtop.c index 9721ed649..f3f5efcc6 100644 --- a/clamdtop/clamdtop.c +++ b/clamdtop/clamdtop.c @@ -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); diff --git a/common/optparser.c b/common/optparser.c index 122bedfa1..dac0749fa 100644 --- a/common/optparser.c +++ b/common/optparser.c @@ -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"}, diff --git a/docs/man/clamd.conf.5.in b/docs/man/clamd.conf.5.in index 29eff1b0e..0011bf06e 100644 --- a/docs/man/clamd.conf.5.in +++ b/docs/man/clamd.conf.5.in @@ -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 diff --git a/etc/clamd.conf.sample b/etc/clamd.conf.sample index 969e1bec0..975380e48 100644 --- a/etc/clamd.conf.sample +++ b/etc/clamd.conf.sample @@ -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 diff --git a/freshclam/notify.c b/freshclam/notify.c index fb30b545d..5e7f410ba 100644 --- a/freshclam/notify.c +++ b/freshclam/notify.c @@ -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); diff --git a/win32/conf_examples/clamd.conf.sample b/win32/conf_examples/clamd.conf.sample index 027415958..325b451f2 100644 --- a/win32/conf_examples/clamd.conf.sample +++ b/win32/conf_examples/clamd.conf.sample @@ -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