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);