/src/sudo/plugins/sudoers/editor.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * SPDX-License-Identifier: ISC |
3 | | * |
4 | | * Copyright (c) 2010-2015, 2020-2022 Todd C. Miller <Todd.Miller@sudo.ws> |
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 | | /* |
20 | | * This is an open source non-commercial project. Dear PVS-Studio, please check it. |
21 | | * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com |
22 | | */ |
23 | | |
24 | | #include <config.h> |
25 | | |
26 | | #include <sys/stat.h> |
27 | | #include <stdio.h> |
28 | | #include <stdlib.h> |
29 | | #include <string.h> |
30 | | #include <unistd.h> |
31 | | #include <errno.h> |
32 | | |
33 | | #include <sudoers.h> |
34 | | |
35 | | /* |
36 | | * Non-destructive word-split that handles single and double quotes and |
37 | | * escaped white space. Quotes are only recognized at the start of a word. |
38 | | * They are treated as normal characters inside a word. |
39 | | */ |
40 | | static const char * |
41 | | wordsplit(const char *str, const char *endstr, const char **last) |
42 | 2.90k | { |
43 | 2.90k | const char *cp; |
44 | 2.90k | debug_decl(wordsplit, SUDOERS_DEBUG_UTIL); |
45 | | |
46 | | /* If no str specified, use last ptr (if any). */ |
47 | 2.90k | if (str == NULL) { |
48 | 1.61k | str = *last; |
49 | | /* Consume end quote if present. */ |
50 | 1.61k | if (*str == '"' || *str == '\'') |
51 | 0 | str++; |
52 | 1.61k | } |
53 | | |
54 | | /* Skip leading white space characters. */ |
55 | 2.90k | while (str < endstr && (*str == ' ' || *str == '\t')) |
56 | 0 | str++; |
57 | | |
58 | | /* Empty string? */ |
59 | 2.90k | if (str >= endstr) { |
60 | 1.61k | *last = endstr; |
61 | 1.61k | debug_return_ptr(NULL); |
62 | 1.61k | } |
63 | | |
64 | | /* If word is quoted, skip to end quote and return. */ |
65 | 1.29k | if (*str == '"' || *str == '\'') { |
66 | 0 | const char *endquote; |
67 | 0 | for (cp = str + 1; cp < endstr; cp = endquote + 1) { |
68 | 0 | endquote = memchr(cp, *str, (size_t)(endstr - cp)); |
69 | 0 | if (endquote == NULL) |
70 | 0 | break; |
71 | | /* ignore escaped quotes */ |
72 | 0 | if (endquote[-1] != '\\') { |
73 | 0 | *last = endquote; |
74 | 0 | debug_return_const_ptr(str + 1); |
75 | 0 | } |
76 | 0 | } |
77 | 0 | } |
78 | | |
79 | | /* Scan str until we encounter white space. */ |
80 | 15.5k | for (cp = str; cp < endstr; cp++) { |
81 | 14.2k | if (cp[0] == '\\' && cp[1] != '\0') { |
82 | | /* quoted char, do not interpret */ |
83 | 0 | cp++; |
84 | 0 | continue; |
85 | 0 | } |
86 | 14.2k | if (*cp == ' ' || *cp == '\t') { |
87 | | /* end of word */ |
88 | 0 | break; |
89 | 0 | } |
90 | 14.2k | } |
91 | 1.29k | *last = cp; |
92 | 1.29k | debug_return_const_ptr(str); |
93 | 1.29k | } |
94 | | |
95 | | /* Copy len chars from string, collapsing chars escaped with a backslash. */ |
96 | | static char * |
97 | | copy_arg(const char *src, size_t len) |
98 | 1.29k | { |
99 | 1.29k | const char *src_end = src + len; |
100 | 1.29k | char *copy, *dst; |
101 | 1.29k | debug_decl(copy_arg, SUDOERS_DEBUG_UTIL); |
102 | | |
103 | 1.29k | if ((copy = malloc(len + 1)) != NULL) { |
104 | 1.29k | sudoers_gc_add(GC_PTR, copy); |
105 | 15.5k | for (dst = copy; src < src_end; ) { |
106 | 14.2k | if (src[0] == '\\' && src[1] != '\0') |
107 | 0 | src++; |
108 | 14.2k | *dst++ = *src++; |
109 | 14.2k | } |
110 | 1.29k | *dst = '\0'; |
111 | 1.29k | } |
112 | | |
113 | 1.29k | debug_return_ptr(copy); |
114 | 1.29k | } |
115 | | |
116 | | /* |
117 | | * Search for the specified editor in the user's PATH, checking |
118 | | * the result against allowlist if non-NULL. An argument vector |
119 | | * suitable for execve() is allocated and stored in argv_out. |
120 | | * If nfiles is non-zero, files[] is added to the end of argv_out. |
121 | | * |
122 | | * Returns the path to be executed on success, else NULL. |
123 | | * The caller is responsible for freeing the returned editor path |
124 | | * as well as the argument vector. |
125 | | */ |
126 | | static char * |
127 | | resolve_editor(const char *ed, size_t edlen, int nfiles, char * const *files, |
128 | | int *argc_out, char ***argv_out, char * const *allowlist) |
129 | 1.29k | { |
130 | 1.29k | char **nargv = NULL, *editor = NULL, *editor_path = NULL; |
131 | 1.29k | const char *tmp, *cp, *ep = NULL; |
132 | 1.29k | const char *edend = ed + edlen; |
133 | 1.29k | struct stat user_editor_sb; |
134 | 1.29k | int nargc = 0; |
135 | 1.29k | debug_decl(resolve_editor, SUDOERS_DEBUG_UTIL); |
136 | | |
137 | | /* |
138 | | * Split editor into an argument vector, including files to edit. |
139 | | * The EDITOR and VISUAL environment variables may contain command |
140 | | * line args so look for those and alloc space for them too. |
141 | | */ |
142 | 1.29k | cp = wordsplit(ed, edend, &ep); |
143 | 1.29k | if (cp == NULL) |
144 | 0 | debug_return_str(NULL); |
145 | 1.29k | editor = copy_arg(cp, (size_t)(ep - cp)); |
146 | 1.29k | if (editor == NULL) |
147 | 0 | goto oom; |
148 | | |
149 | | /* If we can't find the editor in the user's PATH, give up. */ |
150 | 1.29k | if (find_path(editor, &editor_path, &user_editor_sb, getenv("PATH"), NULL, |
151 | 1.29k | false, allowlist) != FOUND) { |
152 | 488 | errno = ENOENT; |
153 | 488 | goto bad; |
154 | 488 | } |
155 | | |
156 | | /* Count rest of arguments and allocate editor argv. */ |
157 | 806 | for (nargc = 1, tmp = ep; wordsplit(NULL, edend, &tmp) != NULL; ) |
158 | 0 | nargc++; |
159 | 806 | if (nfiles != 0) |
160 | 140 | nargc += nfiles + 1; |
161 | 806 | nargv = reallocarray(NULL, (size_t)nargc + 1, sizeof(char *)); |
162 | 806 | if (nargv == NULL) |
163 | 0 | goto oom; |
164 | 806 | sudoers_gc_add(GC_PTR, nargv); |
165 | | |
166 | | /* Fill in editor argv (assumes files[] is NULL-terminated). */ |
167 | 806 | nargv[0] = editor; |
168 | 806 | editor = NULL; |
169 | 806 | for (nargc = 1; (cp = wordsplit(NULL, edend, &ep)) != NULL; nargc++) { |
170 | | /* Copy string, collapsing chars escaped with a backslash. */ |
171 | 0 | nargv[nargc] = copy_arg(cp, (size_t)(ep - cp)); |
172 | 0 | if (nargv[nargc] == NULL) |
173 | 0 | goto oom; |
174 | | |
175 | | /* |
176 | | * We use "--" to separate the editor and arguments from the files |
177 | | * to edit. The editor arguments themselves may not contain "--". |
178 | | */ |
179 | 0 | if (strcmp(nargv[nargc], "--") == 0) { |
180 | 0 | sudo_warnx(U_("ignoring editor: %.*s"), (int)edlen, ed); |
181 | 0 | sudo_warnx("%s", U_("editor arguments may not contain \"--\"")); |
182 | 0 | errno = EINVAL; |
183 | 0 | goto bad; |
184 | 0 | } |
185 | 0 | } |
186 | 806 | if (nfiles != 0) { |
187 | 140 | nargv[nargc++] = (char *)"--"; |
188 | 140 | do |
189 | 3.76M | nargv[nargc++] = *files++; |
190 | 3.76M | while (--nfiles > 0); |
191 | 140 | } |
192 | 806 | nargv[nargc] = NULL; |
193 | | |
194 | 806 | *argc_out = nargc; |
195 | 806 | *argv_out = nargv; |
196 | 806 | debug_return_str(editor_path); |
197 | 0 | oom: |
198 | 0 | sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
199 | 488 | bad: |
200 | 488 | sudoers_gc_remove(GC_PTR, editor); |
201 | 488 | free(editor); |
202 | 488 | free(editor_path); |
203 | 488 | if (nargv != NULL) { |
204 | 0 | while (nargc > 0) { |
205 | 0 | sudoers_gc_remove(GC_PTR, nargv[--nargc]); |
206 | 0 | free(nargv[nargc]); |
207 | 0 | } |
208 | 0 | sudoers_gc_remove(GC_PTR, nargv); |
209 | 0 | free(nargv); |
210 | 0 | } |
211 | 488 | debug_return_str(NULL); |
212 | 488 | } |
213 | | |
214 | | /* |
215 | | * Determine which editor to use based on the SUDO_EDITOR, VISUAL and |
216 | | * EDITOR environment variables as well as the editor path in sudoers. |
217 | | * |
218 | | * Returns the path to be executed on success, else NULL. |
219 | | * The caller is responsible for freeing the returned editor path |
220 | | * as well as the argument vector. |
221 | | */ |
222 | | char * |
223 | | find_editor(int nfiles, char * const *files, int *argc_out, char ***argv_out, |
224 | | char * const *allowlist, const char **env_editor) |
225 | 1.29k | { |
226 | 1.29k | char *editor_path = NULL; |
227 | 1.29k | const char *ev[3]; |
228 | 1.29k | size_t i; |
229 | 1.29k | debug_decl(find_editor, SUDOERS_DEBUG_UTIL); |
230 | | |
231 | | /* |
232 | | * If any of SUDO_EDITOR, VISUAL or EDITOR are set, choose the first one. |
233 | | */ |
234 | 1.29k | *env_editor = NULL; |
235 | 1.29k | ev[0] = "SUDO_EDITOR"; |
236 | 1.29k | ev[1] = "VISUAL"; |
237 | 1.29k | ev[2] = "EDITOR"; |
238 | 5.17k | for (i = 0; i < nitems(ev); i++) { |
239 | 3.88k | char *editor = getenv(ev[i]); |
240 | | |
241 | 3.88k | if (editor != NULL && *editor != '\0') { |
242 | 0 | *env_editor = editor; |
243 | 0 | editor_path = resolve_editor(editor, strlen(editor), |
244 | 0 | nfiles, files, argc_out, argv_out, allowlist); |
245 | 0 | if (editor_path != NULL) |
246 | 0 | break; |
247 | 0 | if (errno != ENOENT) |
248 | 0 | debug_return_str(NULL); |
249 | 0 | } |
250 | 3.88k | } |
251 | | |
252 | | /* |
253 | | * If SUDO_EDITOR, VISUAL and EDITOR were either not set or not |
254 | | * allowed (based on the values of def_editor and def_env_editor), |
255 | | * choose the first one in def_editor that exists. |
256 | | */ |
257 | 1.29k | if (editor_path == NULL) { |
258 | 1.29k | const char *def_editor_end = def_editor + strlen(def_editor); |
259 | 1.29k | const char *cp, *ep; |
260 | | |
261 | | /* def_editor could be a path, split it up, avoiding strtok() */ |
262 | 1.29k | for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep); |
263 | 1.78k | cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) { |
264 | 1.29k | editor_path = resolve_editor(cp, (size_t)(ep - cp), nfiles, |
265 | 1.29k | files, argc_out, argv_out, allowlist); |
266 | 1.29k | if (editor_path != NULL) |
267 | 806 | break; |
268 | 488 | if (errno != ENOENT) |
269 | 0 | debug_return_str(NULL); |
270 | 488 | } |
271 | 1.29k | } |
272 | | |
273 | | /* Caller is responsible for freeing editor_path, not g/c'd. */ |
274 | 1.29k | debug_return_str(editor_path); |
275 | 1.29k | } |