File clamav-disable-administrative-commands.patch of Package clamav
From a3be0d2d452023e367f3a5425f88179c732a50cd Mon Sep 17 00:00:00 2001
From: ChaoticByte <jmlr@keemail.me>
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(-)
--- clamd/session.c.orig
+++ clamd/session.c
@@ -551,17 +551,24 @@ int execute_or_dispatch_command(client_c
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_c
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_c
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:
--- clamdscan/client.c.orig
+++ clamdscan/client.c
@@ -357,6 +357,15 @@ int get_clamd_version(const struct optst
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);
}
--- clamdtop/clamdtop.c.orig
+++ 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 *s
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);
--- common/optparser.c.orig
+++ common/optparser.c
@@ -296,6 +296,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"},
--- docs/man/clamd.conf.5.in.orig
+++ docs/man/clamd.conf.5.in
@@ -139,6 +139,34 @@ This option allows you to restrict the T
.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
--- etc/clamd.conf.sample.orig
+++ etc/clamd.conf.sample
@@ -121,6 +121,31 @@ LocalSocket /run/clamav/clamd.sock
# 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
--- freshclam/notify.c.orig
+++ freshclam/notify.c
@@ -62,6 +62,11 @@ int clamd_connect(const char *cfgfile, c
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);
--- win32/conf_examples/clamd.conf.sample.orig
+++ win32/conf_examples/clamd.conf.sample
@@ -97,6 +97,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