Skip to content

Commit 4b3a5e7

Browse files
committed
find_path: collapse extra "/" and "./" components in the command path
When fully-qualifying the command path, collapse multiple consecutive '/' characters and remove "./" path components. This does not attempt to resolve "../" components, which will be performed during canonicalization.
1 parent 9080a37 commit 4b3a5e7

6 files changed

Lines changed: 241 additions & 10 deletions

File tree

MANIFEST

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ plugins/sudoers/prompt.c
770770
plugins/sudoers/pwutil.c
771771
plugins/sudoers/pwutil.h
772772
plugins/sudoers/pwutil_impl.c
773+
plugins/sudoers/rationalize.c
773774
plugins/sudoers/redblack.c
774775
plugins/sudoers/redblack.h
775776
plugins/sudoers/regress/check_symbols/check_symbols.c
@@ -895,6 +896,7 @@ plugins/sudoers/regress/parser/check_digest.c
895896
plugins/sudoers/regress/parser/check_digest.out.ok
896897
plugins/sudoers/regress/parser/check_fill.c
897898
plugins/sudoers/regress/parser/check_gentime.c
899+
plugins/sudoers/regress/rationalize/check_rationalize.c
898900
plugins/sudoers/regress/serialize_list/check_serialize_list.c
899901
plugins/sudoers/regress/starttime/check_starttime.c
900902
plugins/sudoers/regress/sudoers/test1.in

plugins/sudoers/Makefile.in

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# SPDX-License-Identifier: ISC
33
#
4-
# Copyright (c) 1996, 1998-2005, 2007-2024
4+
# Copyright (c) 1996, 1998-2005, 2007-2026
55
# Todd C. Miller <[email protected]>
66
#
77
# Permission to use, copy, modify, and distribute this software for any
@@ -157,9 +157,9 @@ PROGS = sudoers.la visudo sudoreplay cvtsudoers testsudoers tsdump
157157

158158
# Regression tests
159159
TEST_PROGS = check_addr check_digest check_editor check_env_pattern \
160-
check_exptilde check_fill check_gentime \
161-
check_iolog_plugin check_serialize_list \
162-
check_starttime check_unesc @SUDOERS_TEST_PROGS@
160+
check_exptilde check_fill check_gentime check_iolog_plugin \
161+
check_rationalize check_serialize_list check_starttime \
162+
check_unesc @SUDOERS_TEST_PROGS@
163163
TEST_VERBOSE =
164164
HARNESS = $(SHELL) regress/harness $(TEST_VERBOSE)
165165

@@ -189,16 +189,16 @@ SUDOERS_OBJS = $(AUTH_OBJS) audit.lo boottime.lo check.lo check_util.lo \
189189
file.lo find_path.lo fmtsudoers.lo gc.lo goodpath.lo \
190190
group_plugin.lo interfaces.lo iolog.lo iolog_path_escapes.lo \
191191
locale.lo log_client.lo logging.lo lookup.lo policy.lo \
192-
prompt.lo serialize_list.lo set_perms.lo sethost.lo \
193-
starttime.lo strlcpy_unesc.lo strvec_join.lo sudo_nss.lo \
194-
sudoers.lo sudoers_cb.lo sudoers_ctx_free.lo timestamp.lo \
195-
unesc_str.lo @SUDOERS_OBJS@
192+
prompt.lo rationalize.lo serialize_list.lo set_perms.lo \
193+
sethost.lo starttime.lo strlcpy_unesc.lo strvec_join.lo \
194+
sudo_nss.lo sudoers.lo sudoers_cb.lo sudoers_ctx_free.lo \
195+
timestamp.lo unesc_str.lo @SUDOERS_OBJS@
196196

197197
SUDOERS_IOBJS = $(SUDOERS_OBJS:.lo=.i)
198198

199199
VISUDO_OBJS = check_aliases.o editor.lo find_path.lo gc.lo goodpath.lo \
200-
locale.lo sethost.lo stubs.o sudo_printf.o sudoers_ctx_free.lo \
201-
visudo.o visudo_cb.o
200+
locale.lo rationalize.lo sethost.lo stubs.o sudo_printf.o \
201+
sudoers_ctx_free.lo visudo.o visudo_cb.o
202202

203203
VISUDO_IOBJS = sudo_printf.i visudo.i
204204

@@ -247,6 +247,8 @@ CHECK_IOLOG_PLUGIN_OBJS = check_iolog_plugin.o iolog.lo log_client.lo \
247247
locale.lo pwutil.lo pwutil_impl.lo redblack.lo \
248248
strlist.lo sudoers_debug.lo unesc_str.lo
249249

250+
CHECK_RATIONALIZE_OBJS = check_rationalize.lo rationalize.lo sudoers_debug.lo
251+
250252
CHECK_SYMBOLS_OBJS = check_symbols.o
251253

252254
CHECK_STARTTIME_OBJS = check_starttime.o starttime.lo sudoers_debug.lo
@@ -394,6 +396,9 @@ check_gentime: $(CHECK_GENTIME_OBJS) $(LIBUTIL)
394396
check_iolog_plugin: $(CHECK_IOLOG_PLUGIN_OBJS) $(LIBUTIL) $(LIBIOLOG) $(LIBLOGSRV)
395397
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_PLUGIN_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBIOLOG) $(LIBLOGSRV) @LIBTLS@
396398

399+
check_rationalize: $(CHECK_RATIONALIZE_OBJS) $(LIBUTIL)
400+
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_RATIONALIZE_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS)
401+
397402
check_serialize_list: $(CHECK_SERIALIZE_LIST_OBJS) $(LIBUTIL)
398403
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_SERIALIZE_LIST_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS)
399404

@@ -662,6 +667,7 @@ check: $(TEST_PROGS) visudo testsudoers cvtsudoers check-fuzzer
662667
./check_gentime $(TEST_VERBOSE) || rval=`expr $$rval + $$?`; \
663668
mkdir -p regress/iolog_plugin; \
664669
./check_iolog_plugin $(TEST_VERBOSE) regress/iolog_plugin/iolog || rval=`expr $$rval + $$?`; \
670+
./check_rationalize $(TEST_VERBOSE) || rval=`expr $$rval + $$?`; \
665671
./check_serialize_list $(TEST_VERBOSE) || rval=`expr $$rval + $$?`; \
666672
./check_starttime $(TEST_VERBOSE) || rval=`expr $$rval + $$?`; \
667673
./check_unesc $(TEST_VERBOSE) || rval=`expr $$rval + $$?`; \
@@ -1107,6 +1113,32 @@ check_iolog_plugin.i: $(srcdir)/regress/iolog_plugin/check_iolog_plugin.c \
11071113
$(CPP) $(CPPFLAGS) $(srcdir)/regress/iolog_plugin/check_iolog_plugin.c > $@
11081114
check_iolog_plugin.plog: check_iolog_plugin.i
11091115
rm -f $@; pvs-studio --cfg $(PVS_CFG) --source-file $(srcdir)/regress/iolog_plugin/check_iolog_plugin.c --i-file check_iolog_plugin.i --output-file $@
1116+
check_rationalize.lo: $(srcdir)/regress/rationalize/check_rationalize.c \
1117+
$(devdir)/def_data.h $(incdir)/compat/stdbool.h \
1118+
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
1119+
$(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
1120+
$(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
1121+
$(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
1122+
$(incdir)/sudo_util.h $(srcdir)/defaults.h \
1123+
$(srcdir)/logging.h $(srcdir)/parse.h \
1124+
$(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
1125+
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
1126+
$(top_builddir)/pathnames.h
1127+
$(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/rationalize/check_rationalize.c
1128+
check_rationalize.i: $(srcdir)/regress/rationalize/check_rationalize.c \
1129+
$(devdir)/def_data.h $(incdir)/compat/stdbool.h \
1130+
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
1131+
$(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
1132+
$(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
1133+
$(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
1134+
$(incdir)/sudo_util.h $(srcdir)/defaults.h \
1135+
$(srcdir)/logging.h $(srcdir)/parse.h \
1136+
$(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
1137+
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
1138+
$(top_builddir)/pathnames.h
1139+
$(CPP) $(CPPFLAGS) $(srcdir)/regress/rationalize/check_rationalize.c > $@
1140+
check_rationalize.plog: check_rationalize.i
1141+
rm -f $@; pvs-studio --cfg $(PVS_CFG) --source-file $(srcdir)/regress/rationalize/check_rationalize.c --i-file check_rationalize.i --output-file $@
11101142
check_serialize_list.lo: \
11111143
$(srcdir)/regress/serialize_list/check_serialize_list.c \
11121144
$(devdir)/def_data.h $(incdir)/compat/stdbool.h \
@@ -2525,6 +2557,30 @@ pwutil_impl.i: $(srcdir)/pwutil_impl.c $(devdir)/def_data.h \
25252557
$(CPP) $(CPPFLAGS) $(srcdir)/pwutil_impl.c > $@
25262558
pwutil_impl.plog: pwutil_impl.i
25272559
rm -f $@; pvs-studio --cfg $(PVS_CFG) --source-file $(srcdir)/pwutil_impl.c --i-file pwutil_impl.i --output-file $@
2560+
rationalize.lo: $(srcdir)/rationalize.c $(devdir)/def_data.h \
2561+
$(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
2562+
$(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
2563+
$(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
2564+
$(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
2565+
$(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
2566+
$(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
2567+
$(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
2568+
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
2569+
$(top_builddir)/pathnames.h
2570+
$(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/rationalize.c
2571+
rationalize.i: $(srcdir)/rationalize.c $(devdir)/def_data.h \
2572+
$(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
2573+
$(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
2574+
$(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
2575+
$(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
2576+
$(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
2577+
$(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
2578+
$(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
2579+
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
2580+
$(top_builddir)/pathnames.h
2581+
$(CPP) $(CPPFLAGS) $(srcdir)/rationalize.c > $@
2582+
rationalize.plog: rationalize.i
2583+
rm -f $@; pvs-studio --cfg $(PVS_CFG) --source-file $(srcdir)/rationalize.c --i-file rationalize.i --output-file $@
25282584
redblack.lo: $(srcdir)/redblack.c $(devdir)/def_data.h \
25292585
$(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
25302586
$(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \

plugins/sudoers/find_path.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ cmnd_allowed(char *cmnd, size_t cmnd_size, const char *runchroot,
4545
char * const *al;
4646
debug_decl(cmnd_allowed, SUDOERS_DEBUG_UTIL);
4747

48+
/* First, collapse extra "/" and "./" components. */
49+
rationalize_path(cmnd);
50+
4851
if (!sudo_goodpath(cmnd, runchroot, cmnd_sbp))
4952
debug_return_bool(false);
5053

plugins/sudoers/rationalize.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* SPDX-License-Identifier: ISC
3+
*
4+
* Copyright (c) 2026 Todd C. Miller <[email protected]>
5+
*
6+
* Permission to use, copy, modify, and distribute this software for any
7+
* purpose with or without fee is hereby granted, provided that the above
8+
* copyright notice and this permission notice appear in all copies.
9+
*
10+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
19+
#include <config.h>
20+
21+
#include <string.h>
22+
23+
#include <sudoers.h>
24+
25+
/*
26+
* Rationalize a path by removing consecutive '/' and "/./" path elements.
27+
*/
28+
char *
29+
rationalize_path(char *path)
30+
{
31+
char *cp, *ep, *path_end;
32+
debug_decl(rationalize_path, SUDOERS_DEBUG_UTIL);
33+
34+
path_end = path + strlen(path);
35+
for (cp = path; *cp != '\0';) {
36+
if (cp[0] == '/') {
37+
/* Collapse consecutive '/' characters. */
38+
if (cp[1] == '/') {
39+
for (ep = cp + 1; ep[1] == '/'; ep++)
40+
continue;
41+
42+
/* The size argument includes the terminating NUL byte. */
43+
memmove(cp, ep, (size_t)(path_end - ep) + 1);
44+
continue;
45+
}
46+
47+
/* Remove "/./" in path or "/." at path_end. */
48+
if (cp[1] == '.' && (cp[2] == '/' || cp[2] == '\0')) {
49+
/* /./foo -> /foo OR /foo/. -> /foo */
50+
ep = cp + 2;
51+
52+
/* The size argument includes the terminating NUL byte. */
53+
memmove(cp, ep, (size_t)(path_end - ep) + 1);
54+
continue;
55+
}
56+
}
57+
cp++;
58+
}
59+
debug_return_str(path);
60+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* SPDX-License-Identifier: ISC
3+
*
4+
* Copyright (c) 2026 Todd C. Miller <[email protected]>
5+
*
6+
* Permission to use, copy, modify, and distribute this software for any
7+
* purpose with or without fee is hereby granted, provided that the above
8+
* copyright notice and this permission notice appear in all copies.
9+
*
10+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
19+
#include <config.h>
20+
21+
#include <stdio.h>
22+
#include <stdlib.h>
23+
#include <string.h>
24+
#include <errno.h>
25+
26+
#define SUDO_ERROR_WRAP 0
27+
28+
#include <sudoers.h>
29+
30+
struct test_data {
31+
const char *input;
32+
const char *result;
33+
};
34+
35+
static struct test_data rationalize_test_data[] = {
36+
{ "./bin/ls", "./bin/ls" }, /* 1 */
37+
{ "/usr//bin/who", "/usr/bin/who" }, /* 2 */
38+
{ "//usr//bin/who", "/usr/bin/who" }, /* 3 */
39+
{ "/etc/./shadow", "/etc/shadow" }, /* 4 */
40+
{ "//etc/./shadow", "/etc/shadow" }, /* 5 */
41+
{ "/usr/bin//", "/usr/bin/" }, /* 6 */
42+
{ "//usr///bin////", "/usr/bin/" }, /* 7 */
43+
{ "/./usr/bin", "/usr/bin" }, /* 8 */
44+
{ "/usr/bin/.//", "/usr/bin/" }, /* 9 */
45+
{ "/usr/bin/.", "/usr/bin" }, /* 10 */
46+
{ "/usr/bin//.", "/usr/bin" }, /* 11 */
47+
{ NULL }
48+
};
49+
50+
sudo_dso_public int main(int argc, char *argv[]);
51+
52+
static void
53+
test_rationalize_path(int *ntests_out, int *errors_out)
54+
{
55+
int ntests = *ntests_out;
56+
int errors = *errors_out;
57+
struct test_data *td;
58+
char buf[1024];
59+
60+
for (td = rationalize_test_data; td->input != NULL; td++) {
61+
ntests++;
62+
if (strlcpy(buf, td->input, sizeof(buf)) >= sizeof(buf)) {
63+
errno = ENAMETOOLONG;
64+
sudo_warn("%d: \"%s\"", ntests, td->input);
65+
errors++;
66+
continue;
67+
}
68+
if (strcmp(td->result, rationalize_path(buf)) != 0) {
69+
sudo_warnx("%d: \"%s\": got \"%s\", expected \"%s\"",
70+
ntests, td->input, buf, td->result);
71+
errors++;
72+
}
73+
}
74+
75+
*ntests_out = ntests;
76+
*errors_out = errors;
77+
}
78+
79+
int
80+
main(int argc, char *argv[])
81+
{
82+
int ch, ntests = 0, errors = 0;
83+
84+
initprogname(argc > 0 ? argv[0] : "check_rationalize");
85+
86+
while ((ch = getopt(argc, argv, "v")) != -1) {
87+
switch (ch) {
88+
case 'v':
89+
/* ignored */
90+
break;
91+
default:
92+
fprintf(stderr, "usage: %s [-v]\n", getprogname());
93+
return EXIT_FAILURE;
94+
}
95+
}
96+
argc -= optind;
97+
argv += optind;
98+
99+
test_rationalize_path(&ntests, &errors);
100+
101+
if (ntests != 0) {
102+
printf("%s: %d tests run, %d errors, %d%% success rate\n",
103+
getprogname(), ntests, errors, (ntests - errors) * 100 / ntests);
104+
}
105+
106+
exit(errors);
107+
}

plugins/sudoers/sudoers.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ int find_path(const char *infile, char **outfile, struct stat *sbp,
320320
const char *path, const char *runchroot, bool ignore_dot,
321321
char * const *allowlist);
322322

323+
/* rationalize.c */
324+
char *rationalize_path(char *cmnd);
325+
323326
/* resolve_cmnd.c */
324327
int resolve_cmnd(struct sudoers_context *ctx, const char *infile,
325328
char **outfile, const char *path, const char *runchroot);

0 commit comments

Comments
 (0)