Skip to content

Commit fa1b38e

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 0b804d3 commit fa1b38e

File tree

5 files changed

+91
-35
lines changed

5 files changed

+91
-35
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
/*
@@ -205,7 +206,7 @@ static int replace_running(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
205206

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

340341
if (!fn) {
341-
fprintf(stderr, ERRMSG "invalid destination path.\n");
342+
fprintf(stderr, ERRMSG "file not found.\n");
342343
rc = -1;
343344
goto err;
344345
}
@@ -372,9 +373,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
372373
tmpfn = mktemp(temp_file);
373374
fn = tmpfn;
374375
} else {
375-
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
376+
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
376377
if (!fn) {
377-
fprintf(stderr, ERRMSG "invalid source file location.\n");
378+
fprintf(stderr, ERRMSG "file not found.\n");
378379
rc = 1;
379380
goto err;
380381
}
@@ -416,9 +417,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
416417
}
417418

418419
if (strstr(src, "://")) {
419-
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
420+
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
420421
if (!fn) {
421-
fprintf(stderr, ERRMSG "invalid destination file location.\n");
422+
fprintf(stderr, ERRMSG "file not found.\n");
422423
rc = 1;
423424
goto err;
424425
}
@@ -432,9 +433,9 @@ static int copy(const char *src, const char *dst, const char *remote_user)
432433

433434
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
434435
} else if (strstr(dst, "://")) {
435-
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
436+
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
436437
if (!fn) {
437-
fprintf(stderr, ERRMSG "invalid source file location.\n");
438+
fprintf(stderr, ERRMSG "file not found.\n");
438439
rc = 1;
439440
goto err;
440441
}
@@ -472,6 +473,7 @@ static int usage(int rc)
472473
" -h This help text\n"
473474
" -n Dry-run, validate configuration without applying\n"
474475
" -q Quiet mode, suppress informational messages\n"
476+
" -s Sanitize paths for CLI use (restrict path traversal)\n"
475477
" -u USER Username for remote commands, like scp\n"
476478
" -t SEEC Timeout for the operation, or default %d sec\n"
477479
" -v Show version\n", prognm, timeout);
@@ -486,7 +488,7 @@ int main(int argc, char *argv[])
486488

487489
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
488490

489-
while ((c = getopt(argc, argv, "hnqt:u:v")) != EOF) {
491+
while ((c = getopt(argc, argv, "hnqst:u:v")) != EOF) {
490492
switch(c) {
491493
case 'h':
492494
return usage(0);
@@ -496,6 +498,9 @@ int main(int argc, char *argv[])
496498
case 'q':
497499
quiet = 1;
498500
break;
501+
case 's':
502+
sanitize = 1;
503+
break;
499504
case 't':
500505
timeout = atoi(optarg);
501506
break;

src/bin/erase.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
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
{
@@ -25,7 +25,7 @@ static int do_erase(const char *path)
2525
return -1;
2626
}
2727

28-
cfg_adjust(path, NULL, fn, len);
28+
cfg_adjust(path, NULL, fn, len, sanitize);
2929
} else
3030
fn = (char *)path;
3131

@@ -46,6 +46,7 @@ static int usage(int rc)
4646
"\n"
4747
"Options:\n"
4848
" -h This help text\n"
49+
" -s Sanitize paths for CLI use (restrict path traversal)\n"
4950
" -v Show version\n", prognm);
5051

5152
return rc;
@@ -55,10 +56,13 @@ int main(int argc, char *argv[])
5556
{
5657
int c;
5758

58-
while ((c = getopt(argc, argv, "hv")) != EOF) {
59+
while ((c = getopt(argc, argv, "hsv")) != EOF) {
5960
switch(c) {
6061
case 'h':
6162
return usage(0);
63+
case 's':
64+
sanitize = 1;
65+
break;
6266
case 'v':
6367
puts(PACKAGE_VERSION);
6468
return 0;

src/bin/util.c

Lines changed: 66 additions & 19 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

@@ -81,38 +82,84 @@ static const char *basenm(const char *fn)
8182
return ptr;
8283
}
8384

84-
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len)
85+
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len, int sanitize)
8586
{
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 */
92-
}
93-
94-
/* Files in /cfg must end in .cfg */
95-
if (!strncmp(fn, "/cfg/", 5)) {
87+
if (sanitize) {
88+
char testpath[PATH_MAX];
89+
/* CLI mode - restricted and helpful */
90+
91+
/* Block path traversal attacks */
92+
if (strstr(fn, "../"))
93+
return NULL;
94+
95+
/* Handle absolute paths */
96+
if (fn[0] == '/') {
97+
/* Only allow /media/ prefix for USB sticks */
98+
if (strncmp(fn, "/media/", 7) == 0) {
99+
strlcpy(buf, fn, len);
100+
return buf;
101+
}
102+
/* Block other absolute paths */
103+
return NULL;
104+
}
105+
106+
/* Relative paths: try CWD first (user's $HOME), then /cfg */
107+
108+
/* Check if file exists in current directory */
109+
if (access(fn, F_OK) == 0) {
110+
/* Resolve to absolute path from CWD */
111+
if (realpath(fn, buf))
112+
return buf;
113+
/* If realpath fails, use as-is */
114+
strlcpy(buf, fn, len);
115+
return buf;
116+
}
117+
118+
/* Not in CWD, try /cfg with .cfg extension */
119+
snprintf(testpath, sizeof(testpath), "/cfg/%s", fn);
120+
if (!has_ext(testpath, ".cfg"))
121+
strlcat(testpath, ".cfg", sizeof(testpath));
122+
123+
if (access(testpath, F_OK) == 0) {
124+
strlcpy(buf, testpath, len);
125+
return buf;
126+
}
127+
128+
/* Try /cfg without adding extension */
129+
snprintf(testpath, sizeof(testpath), "/cfg/%s", fn);
130+
if (access(testpath, F_OK) == 0) {
131+
strlcpy(buf, testpath, len);
132+
return buf;
133+
}
134+
135+
/* File not found in either location, try CWD anyway (for new files) */
96136
strlcpy(buf, fn, len);
97-
if (!has_ext(fn, ".cfg"))
98-
strlcat(buf, ".cfg", len);
99-
100137
return buf;
138+
101139
}
102140

103-
/* Files ending with .cfg belong in /cfg */
104-
if (has_ext(fn, ".cfg")) {
105-
snprintf(buf, len, "/cfg/%s", fn);
141+
/* Shell mode - standard UNIX filesystem semantics */
142+
143+
/* Allow all absolute paths */
144+
if (fn[0] == '/') {
145+
strlcpy(buf, fn, len);
106146
return buf;
107147
}
108148

109-
if (strlen(fn) > 0 && fn[0] == '.' && tmpl) {
149+
/* Relative paths - resolve from CWD, no magic */
150+
if (strlen(fn) > 0 && fn[0] == '.' && fn[1] == '/' && tmpl) {
151+
/* Handle ./ prefix explicitly */
152+
strlcpy(buf, fn, len);
153+
} else if (strlen(fn) > 0 && fn[0] == '.' && tmpl) {
154+
/* Handle . or .. with template */
110155
if (fn[1] == '/' && fn[2] != 0)
111156
strlcpy(buf, fn, len);
112157
else
113158
strlcpy(buf, basenm(tmpl), len);
114-
} else
159+
} else {
160+
/* Plain relative path - use as-is */
115161
strlcpy(buf, fn, len);
162+
}
116163

117164
return buf;
118165
}

src/bin/util.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ int yorn (const char *fmt, ...);
1212
int files (const char *path, const char *stripext);
1313

1414
int has_ext (const char *fn, const char *ext);
15-
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len);
15+
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len, int sanitize);
1616

1717
#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)