/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 | 1.86k | { |
43 | 1.86k | const char *cp; |
44 | 1.86k | debug_decl(wordsplit, SUDOERS_DEBUG_UTIL); |
45 | | |
46 | | /* If no str specified, use last ptr (if any). */ |
47 | 1.86k | if (str == NULL) { |
48 | 1.01k | str = *last; |
49 | | /* Consume end quote if present. */ |
50 | 1.01k | if (*str == '"' || *str == '\'') |
51 | 0 | str++; |
52 | 1.01k | } |
53 | | |
54 | | /* Skip leading white space characters. */ |
55 | 1.86k | while (str < endstr && (*str == ' ' || *str == '\t')) |
56 | 0 | str++; |
57 | | |
58 | | /* Empty string? */ |
59 | 1.86k | if (str >= endstr) { |
60 | 1.01k | *last = endstr; |
61 | 1.01k | debug_return_ptr(NULL); |
62 | 1.01k | } |
63 | | |
64 | | /* If word is quoted, skip to end quote and return. */ |
65 | 853 | 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, 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 | 10.2k | for (cp = str; cp < endstr; cp++) { |
81 | 9.38k | if (cp[0] == '\\' && cp[1] != '\0') { |
82 | | /* quoted char, do not interpret */ |
83 | 0 | cp++; |
84 | 0 | continue; |
85 | 0 | } |
86 | 9.38k | if (*cp == ' ' || *cp == '\t') { |
87 | | /* end of word */ |
88 | 0 | break; |
89 | 0 | } |
90 | 9.38k | } |
91 | 853 | *last = cp; |
92 | 853 | debug_return_const_ptr(str); |
93 | 853 | } |
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 | 853 | { |
99 | 853 | const char *src_end = src + len; |
100 | 853 | char *copy, *dst; |
101 | 853 | debug_decl(copy_arg, SUDOERS_DEBUG_UTIL); |
102 | | |
103 | 853 | if ((copy = malloc(len + 1)) != NULL) { |
104 | 853 | sudoers_gc_add(GC_PTR, copy); |
105 | 10.2k | for (dst = copy; src < src_end; ) { |
106 | 9.38k | if (src[0] == '\\' && src[1] != '\0') |
107 | 0 | src++; |
108 | 9.38k | *dst++ = *src++; |
109 | 9.38k | } |
110 | 853 | *dst = '\0'; |
111 | 853 | } |
112 | | |
113 | 853 | debug_return_ptr(copy); |
114 | 853 | } |
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 | 853 | { |
130 | 853 | char **nargv = NULL, *editor = NULL, *editor_path = NULL; |
131 | 853 | const char *tmp, *cp, *ep = NULL; |
132 | 853 | const char *edend = ed + edlen; |
133 | 853 | struct stat user_editor_sb; |
134 | 853 | int nargc = 0; |
135 | 853 | 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 | 853 | cp = wordsplit(ed, edend, &ep); |
143 | 853 | if (cp == NULL) |
144 | 0 | debug_return_str(NULL); |
145 | 853 | editor = copy_arg(cp, ep - cp); |
146 | 853 | if (editor == NULL) |
147 | 0 | goto oom; |
148 | | |
149 | | /* If we can't find the editor in the user's PATH, give up. */ |
150 | 853 | if (find_path(editor, &editor_path, &user_editor_sb, getenv("PATH"), |
151 | 853 | 0, allowlist) != FOUND) { |
152 | 345 | errno = ENOENT; |
153 | 345 | goto bad; |
154 | 345 | } |
155 | | |
156 | | /* Count rest of arguments and allocate editor argv. */ |
157 | 508 | for (nargc = 1, tmp = ep; wordsplit(NULL, edend, &tmp) != NULL; ) |
158 | 0 | nargc++; |
159 | 508 | if (nfiles != 0) |
160 | 132 | nargc += nfiles + 1; |
161 | 508 | nargv = reallocarray(NULL, nargc + 1, sizeof(char *)); |
162 | 508 | if (nargv == NULL) |
163 | 0 | goto oom; |
164 | 508 | sudoers_gc_add(GC_PTR, nargv); |
165 | | |
166 | | /* Fill in editor argv (assumes files[] is NULL-terminated). */ |
167 | 508 | nargv[0] = editor; |
168 | 508 | editor = NULL; |
169 | 508 | 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, 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 | 508 | if (nfiles != 0) { |
187 | 132 | nargv[nargc++] = (char *)"--"; |
188 | 1.27M | while (nfiles--) |
189 | 1.27M | nargv[nargc++] = *files++; |
190 | 132 | } |
191 | 508 | nargv[nargc] = NULL; |
192 | | |
193 | 508 | *argc_out = nargc; |
194 | 508 | *argv_out = nargv; |
195 | 508 | debug_return_str(editor_path); |
196 | 0 | oom: |
197 | 0 | sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
198 | 345 | bad: |
199 | 345 | sudoers_gc_remove(GC_PTR, editor); |
200 | 345 | free(editor); |
201 | 345 | free(editor_path); |
202 | 345 | if (nargv != NULL) { |
203 | 0 | while (nargc--) { |
204 | 0 | sudoers_gc_remove(GC_PTR, nargv[nargc]); |
205 | 0 | free(nargv[nargc]); |
206 | 0 | } |
207 | 0 | sudoers_gc_remove(GC_PTR, nargv); |
208 | 0 | free(nargv); |
209 | 0 | } |
210 | 345 | debug_return_str(NULL); |
211 | 345 | } |
212 | | |
213 | | /* |
214 | | * Determine which editor to use based on the SUDO_EDITOR, VISUAL and |
215 | | * EDITOR environment variables as well as the editor path in sudoers. |
216 | | * |
217 | | * Returns the path to be executed on success, else NULL. |
218 | | * The caller is responsible for freeing the returned editor path |
219 | | * as well as the argument vector. |
220 | | */ |
221 | | char * |
222 | | find_editor(int nfiles, char * const *files, int *argc_out, char ***argv_out, |
223 | | char * const *allowlist, const char **env_editor) |
224 | 853 | { |
225 | 853 | char *editor_path = NULL; |
226 | 853 | const char *ev[3]; |
227 | 853 | unsigned int i; |
228 | 853 | debug_decl(find_editor, SUDOERS_DEBUG_UTIL); |
229 | | |
230 | | /* |
231 | | * If any of SUDO_EDITOR, VISUAL or EDITOR are set, choose the first one. |
232 | | */ |
233 | 853 | *env_editor = NULL; |
234 | 853 | ev[0] = "SUDO_EDITOR"; |
235 | 853 | ev[1] = "VISUAL"; |
236 | 853 | ev[2] = "EDITOR"; |
237 | 3.41k | for (i = 0; i < nitems(ev); i++) { |
238 | 2.55k | char *editor = getenv(ev[i]); |
239 | | |
240 | 2.55k | if (editor != NULL && *editor != '\0') { |
241 | 0 | *env_editor = editor; |
242 | 0 | editor_path = resolve_editor(editor, strlen(editor), |
243 | 0 | nfiles, files, argc_out, argv_out, allowlist); |
244 | 0 | if (editor_path != NULL) |
245 | 0 | break; |
246 | 0 | if (errno != ENOENT) |
247 | 0 | debug_return_str(NULL); |
248 | 0 | } |
249 | 2.55k | } |
250 | | |
251 | | /* |
252 | | * If SUDO_EDITOR, VISUAL and EDITOR were either not set or not |
253 | | * allowed (based on the values of def_editor and def_env_editor), |
254 | | * choose the first one in def_editor that exists. |
255 | | */ |
256 | 853 | if (editor_path == NULL) { |
257 | 853 | const char *def_editor_end = def_editor + strlen(def_editor); |
258 | 853 | const char *cp, *ep; |
259 | | |
260 | | /* def_editor could be a path, split it up, avoiding strtok() */ |
261 | 853 | for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep); |
262 | 1.19k | cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) { |
263 | 853 | editor_path = resolve_editor(cp, (size_t)(ep - cp), nfiles, |
264 | 853 | files, argc_out, argv_out, allowlist); |
265 | 853 | if (editor_path != NULL) |
266 | 508 | break; |
267 | 345 | if (errno != ENOENT) |
268 | 0 | debug_return_str(NULL); |
269 | 345 | } |
270 | 853 | } |
271 | | |
272 | | /* Caller is responsible for freeing editor_path, not g/c'd. */ |
273 | 853 | debug_return_str(editor_path); |
274 | 853 | } |