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