Skip to content

Commit ad96965

Browse files
committed
cli: restrict copy and erase commands
This is a follow-up to PR #717 where path traversal protection was discussed. A year later and it's clear that having a user-friendly copy tool in the shell is a good thing, but that we proably want to restrict what it can do when called from the CLI. A sanitize flag (-s) is added to control the behavior, when used in the shell without -s, both commands act like traditional UNIX tools and do assume . for relative paths, and allow ../, whereas when running from the CLI only /media/ is allowed and otherwise files are assumed to be in $HOME or /cfg Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent 92e80b4 commit ad96965

File tree

5 files changed

+87
-61
lines changed

5 files changed

+87
-61
lines changed

src/bin/copy.c

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ static const char *prognm = "copy";
3636
static int timeout;
3737
static int dry_run;
3838
static int quiet;
39+
static int sanitize;
3940

4041

4142
/*
@@ -204,7 +205,7 @@ static int replace_running(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
204205

205206
if (dry_run) {
206207
if (!quiet) {
207-
printf("Configuration validated, %s: OK\n", file);
208+
printf("Configuration validated, %s OK\n", file);
208209
fflush(stdout);
209210
}
210211
lyd_free_all(data);
@@ -334,10 +335,10 @@ static int copy(const char *src, const char *dst, const char *remote_user)
334335
if (dstds && dstds->path)
335336
fn = dstds->path;
336337
else
337-
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
338+
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
338339

339340
if (!fn) {
340-
fprintf(stderr, ERRMSG "invalid destination path.\n");
341+
fprintf(stderr, ERRMSG "file not found.\n");
341342
rc = -1;
342343
goto err;
343344
}
@@ -371,9 +372,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
371372
tmpfn = mktemp(temp_file);
372373
fn = tmpfn;
373374
} else {
374-
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
375+
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
375376
if (!fn) {
376-
fprintf(stderr, ERRMSG "invalid source file location.\n");
377+
fprintf(stderr, ERRMSG "file not found.\n");
377378
rc = 1;
378379
goto err;
379380
}
@@ -423,9 +424,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
423424
}
424425

425426
if (strstr(src, "://")) {
426-
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
427+
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
427428
if (!fn) {
428-
fprintf(stderr, ERRMSG "invalid destination file location.\n");
429+
fprintf(stderr, ERRMSG "file not found.\n");
429430
rc = 1;
430431
goto err;
431432
}
@@ -439,9 +440,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
439440

440441
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
441442
} else if (strstr(dst, "://")) {
442-
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
443+
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
443444
if (!fn) {
444-
fprintf(stderr, ERRMSG "invalid source file location.\n");
445+
fprintf(stderr, ERRMSG "file not found.\n");
445446
rc = 1;
446447
goto err;
447448
}
@@ -479,6 +480,7 @@ static int usage(int rc)
479480
" -h This help text\n"
480481
" -n Dry-run, validate configuration without applying\n"
481482
" -q Quiet mode, suppress informational messages\n"
483+
" -s Sanitize paths for CLI use (restrict path traversal)\n"
482484
" -u USER Username for remote commands, like scp\n"
483485
" -t SEEC Timeout for the operation, or default %d sec\n"
484486
" -v Show version\n", prognm, timeout);
@@ -493,7 +495,7 @@ int main(int argc, char *argv[])
493495

494496
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
495497

496-
while ((c = getopt(argc, argv, "hnqt:u:v")) != EOF) {
498+
while ((c = getopt(argc, argv, "hnqst:u:v")) != EOF) {
497499
switch(c) {
498500
case 'h':
499501
return usage(0);
@@ -503,6 +505,9 @@ int main(int argc, char *argv[])
503505
case 'q':
504506
quiet = 1;
505507
break;
508+
case 's':
509+
sanitize = 1;
510+
break;
506511
case 't':
507512
timeout = atoi(optarg);
508513
break;

src/bin/erase.c

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,25 @@
1010
#include "util.h"
1111

1212
static const char *prognm = "erase";
13-
13+
static int sanitize;
1414

1515
static int do_erase(const char *path)
1616
{
17-
char *fn;
18-
19-
if (access(path, F_OK)) {
20-
size_t len = strlen(path) + 10;
17+
char buf[PATH_MAX];
18+
const char *fn;
2119

22-
fn = alloca(len);
23-
if (!fn) {
24-
fprintf(stderr, ERRMSG "failed allocating memory.\n");
25-
return -1;
26-
}
27-
28-
cfg_adjust(path, NULL, fn, len);
29-
} else
30-
fn = (char *)path;
20+
fn = cfg_adjust(path, NULL, buf, sizeof(buf), sanitize);
21+
if (!fn) {
22+
fprintf(stderr, ERRMSG "file not found.\n");
23+
return 1;
24+
}
3125

32-
if (!yorn("Remove %s, are you sure", fn))
26+
if (!yorn("Remove %s, are you sure", path))
3327
return 0;
3428

3529
if (remove(fn)) {
36-
fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno));
37-
return -1;
30+
fprintf(stderr, ERRMSG "failed removing %s: %s\n", path, strerror(errno));
31+
return 11;
3832
}
3933

4034
return 0;
@@ -46,6 +40,7 @@ static int usage(int rc)
4640
"\n"
4741
"Options:\n"
4842
" -h This help text\n"
43+
" -s Sanitize paths for CLI use (restrict path traversal)\n"
4944
" -v Show version\n", prognm);
5045

5146
return rc;
@@ -55,10 +50,13 @@ int main(int argc, char *argv[])
5550
{
5651
int c;
5752

58-
while ((c = getopt(argc, argv, "hv")) != EOF) {
53+
while ((c = getopt(argc, argv, "hsv")) != EOF) {
5954
switch(c) {
6055
case 'h':
6156
return usage(0);
57+
case 's':
58+
sanitize = 1;
59+
break;
6260
case 'v':
6361
puts(PACKAGE_VERSION);
6462
return 0;

src/bin/util.c

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <errno.h>
66
#include <stdarg.h>
77
#include <stdio.h>
8+
#include <stdlib.h>
89
#include <string.h>
910
#include <termios.h>
1011

@@ -67,52 +68,73 @@ int has_ext(const char *fn, const char *ext)
6768
return 0;
6869
}
6970

70-
static const char *basenm(const char *fn)
71+
const char *basenm(const char *fn)
7172
{
7273
const char *ptr;
7374

7475
if (!fn)
7576
return "";
7677

7778
ptr = strrchr(fn, '/');
78-
if (!ptr)
79+
if (ptr)
80+
ptr++;
81+
else
7982
ptr = fn;
8083

8184
return ptr;
8285
}
8386

84-
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len)
87+
static int path_allowed(const char *path)
8588
{
86-
if (strstr(fn, "../"))
87-
return NULL; /* relative paths not allowed */
88-
89-
if (fn[0] == '/') {
90-
strlcpy(buf, fn, len);
91-
return buf; /* allow absolute paths */
89+
const char *accepted[] = {
90+
"/media/",
91+
"/cfg/",
92+
getenv("HOME"),
93+
NULL
94+
};
95+
96+
for (int i = 0; accepted[i]; i++) {
97+
if (!strncmp(path, accepted[i], strlen(accepted[i])))
98+
return 1;
9299
}
93100

94-
/* Files in /cfg must end in .cfg */
95-
if (!strncmp(fn, "/cfg/", 5)) {
96-
strlcpy(buf, fn, len);
97-
if (!has_ext(fn, ".cfg"))
98-
strlcat(buf, ".cfg", len);
101+
return 0;
102+
}
99103

100-
return buf;
101-
}
104+
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len, int sanitize)
105+
{
106+
char tmp[256], resolved[PATH_MAX];
107+
108+
if (strlen(basenm(fn)) == 0) {
109+
if (!tmpl)
110+
return NULL;
102111

103-
/* Files ending with .cfg belong in /cfg */
104-
if (has_ext(fn, ".cfg")) {
105-
snprintf(buf, len, "/cfg/%s", fn);
106-
return buf;
112+
fn = basenm(tmpl);
113+
/* Fall through */
107114
}
108115

109-
if (strlen(fn) > 0 && fn[0] == '.' && tmpl) {
110-
if (fn[1] == '/' && fn[2] != 0)
111-
strlcpy(buf, fn, len);
112-
else
113-
strlcpy(buf, basenm(tmpl), len);
114-
} else
115-
strlcpy(buf, fn, len);
116+
if (sanitize) {
117+
if (strstr(fn, "../"))
118+
return NULL;
119+
120+
if (fn[0] == '/') {
121+
if (!path_allowed(fn))
122+
return NULL;
123+
} else {
124+
snprintf(tmp, sizeof(tmp), "/cfg/%s", fn);
125+
if (!has_ext(tmp, ".cfg"))
126+
strlcat(tmp, ".cfg", sizeof(tmp));
127+
fn = tmp;
128+
}
129+
130+
/* If file exists, resolve symlinks and verify still in whitelist */
131+
if (!access(fn, F_OK) && realpath(fn, resolved)) {
132+
if (!path_allowed(resolved))
133+
return NULL;
134+
fn = resolved;
135+
}
136+
}
116137

138+
strlcpy(buf, fn, len);
117139
return buf;
118140
}

src/bin/util.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
#define ERRMSG "Error: "
88
#define INFMSG "Note: "
99

10-
int yorn (const char *fmt, ...);
10+
int yorn (const char *fmt, ...);
1111

12-
int files (const char *path, const char *stripext);
12+
int files (const char *path, const char *stripext);
1313

14-
int has_ext (const char *fn, const char *ext);
15-
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len);
14+
const char *basenm (const char *fn);
15+
int has_ext (const char *fn, const char *ext);
16+
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len, int sanitize);
1617

1718
#endif /* BIN_UTIL_H_ */

src/klish-plugin-infix/src/infix.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ int infix_erase(kcontext_t *ctx)
120120

121121
cd_home(ctx);
122122

123-
return systemf("erase %s", path);
123+
return systemf("erase -s %s", path);
124124
}
125125

126126
int infix_files(kcontext_t *ctx)
@@ -221,7 +221,7 @@ int infix_copy(kcontext_t *ctx)
221221
strlcpy(validate, "-n", sizeof(validate));
222222

223223
/* Ensure we run the copy command as the logged-in user, not root (klishd) */
224-
return systemf("doas -u %s copy %s %s %s %s", cd_home(ctx), validate, user, src, dst);
224+
return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst);
225225
}
226226

227227
int infix_shell(kcontext_t *ctx)

0 commit comments

Comments
 (0)