@cryptotaxi247 / netdata-1 / commits / 4de2ce54d

Sanitize command arguments. (#14064)

* Sanitize bash arguments. Remove leading dashes and escape single quotes in command arguments. * Quote expanded variable in test

vkalintiris committed Nov 29, 2022 at 17:26 UTC 4de2ce54d59a4128425f8dde5924eed4fc6dad97
8 files changed +287 -49
daemon/main.c
+3
@@ -1024,6 +1024,9 @@ int main(int argc, char **argv) {
1024 fprintf(stderr, "\n\nALL TESTS PASSED\n\n");
1025 return 0;
1026 }
1027 + else if(strcmp(optarg, "escapetest") == 0) {
1028 + return command_argument_sanitization_tests();
1029 + }
1030 #ifdef ENABLE_ML_TESTS
1031 else if(strcmp(optarg, "mltest") == 0) {
1032 return test_ml(argc, argv);
daemon/unit_test.c
+55
@@ -2,6 +2,61 @@
2
3 #include "common.h"
4
5 +static bool cmd_arg_sanitization_test(const char *expected, const char *src, char *dst, size_t dst_size) {
6 + bool ok = sanitize_command_argument_string(dst, src, dst_size);
7 +
8 + if (!expected)
9 + return ok == false;
10 +
11 + return strcmp(expected, dst) == 0;
12 +}
13 +
14 +bool command_argument_sanitization_tests() {
15 + char dst[1024];
16 +
17 + for (size_t i = 0; i != 5; i++) {
18 + const char *expected = i == 4 ? "'\\''" : NULL;
19 + if (cmd_arg_sanitization_test(expected, "'", dst, i) == false) {
20 + fprintf(stderr, "expected: >>>%s<<<, got: >>>%s<<<\n", expected, dst);
21 + return 1;
22 + }
23 + }
24 +
25 + for (size_t i = 0; i != 9; i++) {
26 + const char *expected = i == 8 ? "'\\'''\\''" : NULL;
27 + if (cmd_arg_sanitization_test(expected, "''", dst, i) == false) {
28 + fprintf(stderr, "expected: >>>%s<<<, got: >>>%s<<<\n", expected, dst);
29 + return 1;
30 + }
31 + }
32 +
33 + for (size_t i = 0; i != 7; i++) {
34 + const char *expected = i == 6 ? "'\\''a" : NULL;
35 + if (cmd_arg_sanitization_test(expected, "'a", dst, i) == false) {
36 + fprintf(stderr, "expected: >>>%s<<<, got: >>>%s<<<\n", expected, dst);
37 + return 1;
38 + }
39 + }
40 +
41 + for (size_t i = 0; i != 7; i++) {
42 + const char *expected = i == 6 ? "a'\\''" : NULL;
43 + if (cmd_arg_sanitization_test(expected, "a'", dst, i) == false) {
44 + fprintf(stderr, "expected: >>>%s<<<, got: >>>%s<<<\n", expected, dst);
45 + return 1;
46 + }
47 + }
48 +
49 + for (size_t i = 0; i != 22; i++) {
50 + const char *expected = i == 21 ? "foo'\\''a'\\'''\\'''\\''b" : NULL;
51 + if (cmd_arg_sanitization_test(expected, "--foo'a'''b", dst, i) == false) {
52 + fprintf(stderr, "expected: >>>%s<<<, got: >>>%s<<<\n length: %zu\n", expected, dst, strlen(dst));
53 + return 1;
54 + }
55 + }
56 +
57 + return 0;
58 +}
59 +
60 static int check_number_printing(void) {
61 struct {
62 NETDATA_DOUBLE n;
daemon/unit_test.h
+4
@@ -3,6 +3,8 @@
3 #ifndef NETDATA_UNIT_TEST_H
4 #define NETDATA_UNIT_TEST_H 1
5
6 +#include "stdbool.h"
7 +
8 int unit_test_storage(void);
9 int unit_test(long delay, long shift);
10 int run_all_mockup_tests(void);
@@ -19,4 +21,6 @@ void dbengine_stress_test(unsigned TEST_DURATION_SEC, unsigned DSET_CHARTS, unsi
21
22 #endif
23
24 +bool command_argument_sanitization_tests();
25 +
26 #endif /* NETDATA_UNIT_TEST_H */
health/health.c
+185 -45
@@ -17,6 +17,145 @@
17 #error WORKER_UTILIZATION_MAX_JOB_TYPES has to be at least 10
18 #endif
19
20 +static bool prepare_command(BUFFER *wb,
21 + const char *exec,
22 + const char *recipient,
23 + const char *registry_hostname,
24 + uint32_t unique_id,
25 + uint32_t alarm_id,
26 + uint32_t alarm_event_id,
27 + uint32_t when,
28 + const char *alert_name,
29 + const char *alert_chart_name,
30 + const char *alert_family,
31 + const char *new_status,
32 + const char *old_status,
33 + NETDATA_DOUBLE new_value,
34 + NETDATA_DOUBLE old_value,
35 + const char *alert_source,
36 + uint32_t duration,
37 + uint32_t non_clear_duration,
38 + const char *alert_units,
39 + const char *alert_info,
40 + const char *new_value_string,
41 + const char *old_value_string,
42 + const char *source,
43 + const char *error_msg,
44 + int n_warn,
45 + int n_crit,
46 + const char *warn_alarms,
47 + const char *crit_alarms,
48 + const char *classification,
49 + const char *edit_command,
50 + const char *machine_guid)
51 +{
52 + char buf[8192];
53 + size_t n = 8192 - 1;
54 +
55 + buffer_strcat(wb, "exec");
56 +
57 + if (!sanitize_command_argument_string(buf, exec, n))
58 + return false;
59 + buffer_sprintf(wb, " '%s'", buf);
60 +
61 + if (!sanitize_command_argument_string(buf, recipient, n))
62 + return false;
63 + buffer_sprintf(wb, " '%s'", buf);
64 +
65 + if (!sanitize_command_argument_string(buf, registry_hostname, n))
66 + return false;
67 + buffer_sprintf(wb, " '%s'", buf);
68 +
69 + buffer_sprintf(wb, " '%u'", unique_id);
70 +
71 + buffer_sprintf(wb, " '%u'", alarm_id);
72 +
73 + buffer_sprintf(wb, " '%u'", alarm_event_id);
74 +
75 + buffer_sprintf(wb, " '%u'", when);
76 +
77 + if (!sanitize_command_argument_string(buf, alert_name, n))
78 + return false;
79 + buffer_sprintf(wb, " '%s'", buf);
80 +
81 + if (!sanitize_command_argument_string(buf, alert_chart_name, n))
82 + return false;
83 + buffer_sprintf(wb, " '%s'", buf);
84 +
85 + if (!sanitize_command_argument_string(buf, alert_family, n))
86 + return false;
87 + buffer_sprintf(wb, " '%s'", buf);
88 +
89 + if (!sanitize_command_argument_string(buf, new_status, n))
90 + return false;
91 + buffer_sprintf(wb, " '%s'", buf);
92 +
93 + if (!sanitize_command_argument_string(buf, old_status, n))
94 + return false;
95 + buffer_sprintf(wb, " '%s'", buf);
96 +
97 + buffer_sprintf(wb, " '" NETDATA_DOUBLE_FORMAT_ZERO "'", new_value);
98 +
99 + buffer_sprintf(wb, " '" NETDATA_DOUBLE_FORMAT_ZERO "'", old_value);
100 +
101 + if (!sanitize_command_argument_string(buf, alert_source, n))
102 + return false;
103 + buffer_sprintf(wb, " '%s'", buf);
104 +
105 + buffer_sprintf(wb, " '%u'", duration);
106 +
107 + buffer_sprintf(wb, " '%u'", non_clear_duration);
108 +
109 + if (!sanitize_command_argument_string(buf, alert_units, n))
110 + return false;
111 + buffer_sprintf(wb, " '%s'", buf);
112 +
113 + if (!sanitize_command_argument_string(buf, alert_info, n))
114 + return false;
115 + buffer_sprintf(wb, " '%s'", buf);
116 +
117 + if (!sanitize_command_argument_string(buf, new_value_string, n))
118 + return false;
119 + buffer_sprintf(wb, " '%s'", buf);
120 +
121 + if (!sanitize_command_argument_string(buf, old_value_string, n))
122 + return false;
123 + buffer_sprintf(wb, " '%s'", buf);
124 +
125 + if (!sanitize_command_argument_string(buf, source, n))
126 + return false;
127 + buffer_sprintf(wb, " '%s'", buf);
128 +
129 + if (!sanitize_command_argument_string(buf, error_msg, n))
130 + return false;
131 + buffer_sprintf(wb, " '%s'", buf);
132 +
133 + buffer_sprintf(wb, " '%d'", n_warn);
134 +
135 + buffer_sprintf(wb, " '%d'", n_crit);
136 +
137 + if (!sanitize_command_argument_string(buf, warn_alarms, n))
138 + return false;
139 + buffer_sprintf(wb, " '%s'", buf);
140 +
141 + if (!sanitize_command_argument_string(buf, crit_alarms, n))
142 + return false;
143 + buffer_sprintf(wb, " '%s'", buf);
144 +
145 + if (!sanitize_command_argument_string(buf, classification, n))
146 + return false;
147 + buffer_sprintf(wb, " '%s'", buf);
148 +
149 + if (!sanitize_command_argument_string(buf, edit_command, n))
150 + return false;
151 + buffer_sprintf(wb, " '%s'", buf);
152 +
153 + if (!sanitize_command_argument_string(buf, machine_guid, n))
154 + return false;
155 + buffer_sprintf(wb, " '%s'", buf);
156 +
157 + return true;
158 +}
159
160 unsigned int default_health_enabled = 1;
161 char *silencers_filename;
@@ -235,7 +374,6 @@ static inline RRDCALC_STATUS rrdcalc_value2status(NETDATA_DOUBLE n) {
374 return RRDCALC_STATUS_CLEAR;
375 }
376
238 -#define ALARM_EXEC_COMMAND_LENGTH 8192
377 #define ACTIVE_ALARMS_LIST_EXAMINE 500
378 #define ACTIVE_ALARMS_LIST 15
379
@@ -306,8 +444,6 @@ static inline void health_alarm_execute(RRDHOST *host, ALARM_ENTRY *ae) {
444
445 log_health("[%s]: Sending notification for alarm '%s.%s' status %s.", rrdhost_hostname(host), ae_chart_name(ae), ae_name(ae), rrdcalc_status2string(ae->new_status));
446
309 - static char command_to_run[ALARM_EXEC_COMMAND_LENGTH + 1];
310 -
447 const char *exec = (ae->exec) ? ae_exec(ae) : string2str(host->health_default_exec);
448 const char *recipient = (ae->recipient) ? ae_recipient(ae) : string2str(host->health_default_recipient);
449
@@ -375,49 +511,53 @@ static inline void health_alarm_execute(RRDHOST *host, ALARM_ENTRY *ae) {
511
512 char *edit_command = ae->source ? health_edit_command_from_source(ae_source(ae)) : strdupz("UNKNOWN=0=UNKNOWN");
513
378 - snprintfz(command_to_run, ALARM_EXEC_COMMAND_LENGTH, "exec %s '%s' '%s' '%u' '%u' '%u' '%lu' '%s' '%s' '%s' '%s' '%s' '" NETDATA_DOUBLE_FORMAT_ZERO
379 - "' '" NETDATA_DOUBLE_FORMAT_ZERO
380 - "' '%s' '%u' '%u' '%s' '%s' '%s' '%s' '%s' '%s' '%d' '%d' '%s' '%s' '%s' '%s' '%s'",
381 - exec,
382 - recipient,
383 - rrdhost_registry_hostname(host),
384 - ae->unique_id,
385 - ae->alarm_id,
386 - ae->alarm_event_id,
387 - (unsigned long)ae->when,
388 - ae_name(ae),
389 - ae->chart?ae_chart_name(ae):"NOCHART",
390 - ae->family?ae_family(ae):"NOFAMILY",
391 - rrdcalc_status2string(ae->new_status),
392 - rrdcalc_status2string(ae->old_status),
393 - ae->new_value,
394 - ae->old_value,
395 - ae->source?ae_source(ae):"UNKNOWN",
396 - (uint32_t)ae->duration,
397 - (uint32_t)ae->non_clear_duration,
398 - ae_units(ae),
399 - ae_info(ae),
400 - ae_new_value_string(ae),
401 - ae_old_value_string(ae),
402 - (expr && expr->source)?expr->source:"NOSOURCE",
403 - (expr && expr->error_msg)?buffer_tostring(expr->error_msg):"NOERRMSG",
404 - n_warn,
405 - n_crit,
406 - buffer_tostring(warn_alarms),
407 - buffer_tostring(crit_alarms),
408 - ae->classification?ae_classification(ae):"Unknown",
409 - edit_command,
410 - host != localhost ? host->machine_guid:""
411 - );
412 -
413 - ae->flags |= HEALTH_ENTRY_FLAG_EXEC_RUN;
414 - ae->exec_run_timestamp = now_realtime_sec(); /* will be updated by real time after spawning */
415 -
416 - debug(D_HEALTH, "executing command '%s'", command_to_run);
417 - ae->flags |= HEALTH_ENTRY_FLAG_EXEC_IN_PROGRESS;
418 - ae->exec_spawn_serial = spawn_enq_cmd(command_to_run);
419 - enqueue_alarm_notify_in_progress(ae);
514 + BUFFER *wb = buffer_create(8192);
515 + bool ok = prepare_command(wb,
516 + exec,
517 + recipient,
518 + rrdhost_registry_hostname(host),
519 + ae->unique_id,
520 + ae->alarm_id,
521 + ae->alarm_event_id,
522 + (unsigned long)ae->when,
523 + ae_name(ae),
524 + ae->chart?ae_chart_name(ae):"NOCHART",
525 + ae->family?ae_family(ae):"NOFAMILY",
526 + rrdcalc_status2string(ae->new_status),
527 + rrdcalc_status2string(ae->old_status),
528 + ae->new_value,
529 + ae->old_value,
530 + ae->source?ae_source(ae):"UNKNOWN",
531 + (uint32_t)ae->duration,
532 + (uint32_t)ae->non_clear_duration,
533 + ae_units(ae),
534 + ae_info(ae),
535 + ae_new_value_string(ae),
536 + ae_old_value_string(ae),
537 + (expr && expr->source)?expr->source:"NOSOURCE",
538 + (expr && expr->error_msg)?buffer_tostring(expr->error_msg):"NOERRMSG",
539 + n_warn,
540 + n_crit,
541 + buffer_tostring(warn_alarms),
542 + buffer_tostring(crit_alarms),
543 + ae->classification?ae_classification(ae):"Unknown",
544 + edit_command,
545 + host != localhost ? host->machine_guid:"");
546 +
547 + const char *command_to_run = buffer_tostring(wb);
548 + if (ok) {
549 + ae->flags |= HEALTH_ENTRY_FLAG_EXEC_RUN;
550 + ae->exec_run_timestamp = now_realtime_sec(); /* will be updated by real time after spawning */
551 +
552 + debug(D_HEALTH, "executing command '%s'", command_to_run);
553 + ae->flags |= HEALTH_ENTRY_FLAG_EXEC_IN_PROGRESS;
554 + ae->exec_spawn_serial = spawn_enq_cmd(command_to_run);
555 + enqueue_alarm_notify_in_progress(ae);
556 + } else {
557 + error("Failed to format command arguments");
558 + }
559
560 + buffer_free(wb);
561 freez(edit_command);
562 buffer_free(warn_alarms);
563 buffer_free(crit_alarms);
health/notifications/alarm-notify.sh.in
+1 -1
@@ -250,7 +250,7 @@ fi
250 # -----------------------------------------------------------------------------
251 # find a suitable hostname to use, if netdata did not supply a hostname
252
253 -if [ -z ${args_host} ]; then
253 +if [ -z "${args_host}" ]; then
254 this_host=$(hostname -s 2>/dev/null)
255 host="${this_host}"
256 args_host="${this_host}"
libnetdata/inlined.h
+36
@@ -181,6 +181,42 @@ static inline void sanitize_json_string(char *dst, const char *src, size_t dst_s
181 *dst = '\0';
182 }
183
184 +static inline bool sanitize_command_argument_string(char *dst, const char *src, size_t dst_size) {
185 + // skip leading dashes
186 + while (src[0] == '-')
187 + src++;
188 +
189 + // escape single quotes
190 + while (src[0] != '\0') {
191 + if (src[0] == '\'') {
192 + if (dst_size < 4)
193 + return false;
194 +
195 + dst[0] = '\''; dst[1] = '\\'; dst[2] = '\''; dst[3] = '\'';
196 +
197 + dst += 4;
198 + dst_size -= 4;
199 + } else {
200 + if (dst_size < 1)
201 + return false;
202 +
203 + dst[0] = src[0];
204 +
205 + dst += 1;
206 + dst_size -= 1;
207 + }
208 +
209 + src++;
210 + }
211 +
212 + // make sure we have space to terminate the string
213 + if (dst_size == 0)
214 + return false;
215 + *dst = '\0';
216 +
217 + return true;
218 +}
219 +
220 static inline int read_file(const char *filename, char *buffer, size_t size) {
221 if(unlikely(!size)) return 3;
222
spawn/spawn.c
+2 -2
@@ -8,7 +8,7 @@ int spawn_thread_shutdown;
8
9 struct spawn_queue spawn_cmd_queue;
10
11 -static struct spawn_cmd_info *create_spawn_cmd(char *command_to_run)
11 +static struct spawn_cmd_info *create_spawn_cmd(const char *command_to_run)
12 {
13 struct spawn_cmd_info *cmdinfo;
14
@@ -57,7 +57,7 @@ static void init_spawn_cmd_queue(void)
57 /*
58 * Returns serial number of the enqueued command
59 */
60 -uint64_t spawn_enq_cmd(char *command_to_run)
60 +uint64_t spawn_enq_cmd(const char *command_to_run)
61 {
62 unsigned queue_size;
63 uint64_t serial;
spawn/spawn.h
+1 -1
@@ -84,7 +84,7 @@ void spawn_init(void);
84 void spawn_server(void);
85 void spawn_client(void *arg);
86 void destroy_spawn_cmd(struct spawn_cmd_info *cmdinfo);
87 -uint64_t spawn_enq_cmd(char *command_to_run);
87 +uint64_t spawn_enq_cmd(const char *command_to_run);
88 void spawn_wait_cmd(uint64_t serial, int *exit_status, time_t *exec_run_timestamp);
89 void spawn_deq_cmd(struct spawn_cmd_info *cmdinfo);
90 struct spawn_cmd_info *spawn_get_unprocessed_cmd(void);