summaryrefslogtreecommitdiffstats
path: root/plugins/sudoers/editor.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/sudoers/editor.c')
-rw-r--r--plugins/sudoers/editor.c275
1 files changed, 275 insertions, 0 deletions
diff --git a/plugins/sudoers/editor.c b/plugins/sudoers/editor.c
new file mode 100644
index 0000000..db1e3e0
--- /dev/null
+++ b/plugins/sudoers/editor.c
@@ -0,0 +1,275 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2010-2015, 2020-2022 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include <config.h>
+
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <sudoers.h>
+
+/*
+ * Non-destructive word-split that handles single and double quotes and
+ * escaped white space. Quotes are only recognized at the start of a word.
+ * They are treated as normal characters inside a word.
+ */
+static const char *
+wordsplit(const char *str, const char *endstr, const char **last)
+{
+ const char *cp;
+ debug_decl(wordsplit, SUDOERS_DEBUG_UTIL);
+
+ /* If no str specified, use last ptr (if any). */
+ if (str == NULL) {
+ str = *last;
+ /* Consume end quote if present. */
+ if (*str == '"' || *str == '\'')
+ str++;
+ }
+
+ /* Skip leading white space characters. */
+ while (str < endstr && (*str == ' ' || *str == '\t'))
+ str++;
+
+ /* Empty string? */
+ if (str >= endstr) {
+ *last = endstr;
+ debug_return_ptr(NULL);
+ }
+
+ /* If word is quoted, skip to end quote and return. */
+ if (*str == '"' || *str == '\'') {
+ const char *endquote;
+ for (cp = str + 1; cp < endstr; cp = endquote + 1) {
+ endquote = memchr(cp, *str, (size_t)(endstr - cp));
+ if (endquote == NULL)
+ break;
+ /* ignore escaped quotes */
+ if (endquote[-1] != '\\') {
+ *last = endquote;
+ debug_return_const_ptr(str + 1);
+ }
+ }
+ }
+
+ /* Scan str until we encounter white space. */
+ for (cp = str; cp < endstr; cp++) {
+ if (cp[0] == '\\' && cp[1] != '\0') {
+ /* quoted char, do not interpret */
+ cp++;
+ continue;
+ }
+ if (*cp == ' ' || *cp == '\t') {
+ /* end of word */
+ break;
+ }
+ }
+ *last = cp;
+ debug_return_const_ptr(str);
+}
+
+/* Copy len chars from string, collapsing chars escaped with a backslash. */
+static char *
+copy_arg(const char *src, size_t len)
+{
+ const char *src_end = src + len;
+ char *copy, *dst;
+ debug_decl(copy_arg, SUDOERS_DEBUG_UTIL);
+
+ if ((copy = malloc(len + 1)) != NULL) {
+ sudoers_gc_add(GC_PTR, copy);
+ for (dst = copy; src < src_end; ) {
+ if (src[0] == '\\' && src[1] != '\0')
+ src++;
+ *dst++ = *src++;
+ }
+ *dst = '\0';
+ }
+
+ debug_return_ptr(copy);
+}
+
+/*
+ * Search for the specified editor in the user's PATH, checking
+ * the result against allowlist if non-NULL. An argument vector
+ * suitable for execve() is allocated and stored in argv_out.
+ * If nfiles is non-zero, files[] is added to the end of argv_out.
+ *
+ * Returns the path to be executed on success, else NULL.
+ * The caller is responsible for freeing the returned editor path
+ * as well as the argument vector.
+ */
+static char *
+resolve_editor(const char *ed, size_t edlen, int nfiles, char * const *files,
+ int *argc_out, char ***argv_out, char * const *allowlist)
+{
+ char **nargv = NULL, *editor = NULL, *editor_path = NULL;
+ const char *tmp, *cp, *ep = NULL;
+ const char *edend = ed + edlen;
+ struct stat user_editor_sb;
+ int nargc = 0;
+ debug_decl(resolve_editor, SUDOERS_DEBUG_UTIL);
+
+ /*
+ * Split editor into an argument vector, including files to edit.
+ * The EDITOR and VISUAL environment variables may contain command
+ * line args so look for those and alloc space for them too.
+ */
+ cp = wordsplit(ed, edend, &ep);
+ if (cp == NULL)
+ debug_return_str(NULL);
+ editor = copy_arg(cp, (size_t)(ep - cp));
+ if (editor == NULL)
+ goto oom;
+
+ /* If we can't find the editor in the user's PATH, give up. */
+ if (find_path(editor, &editor_path, &user_editor_sb, getenv("PATH"),
+ false, allowlist) != FOUND) {
+ errno = ENOENT;
+ goto bad;
+ }
+
+ /* Count rest of arguments and allocate editor argv. */
+ for (nargc = 1, tmp = ep; wordsplit(NULL, edend, &tmp) != NULL; )
+ nargc++;
+ if (nfiles != 0)
+ nargc += nfiles + 1;
+ nargv = reallocarray(NULL, (size_t)nargc + 1, sizeof(char *));
+ if (nargv == NULL)
+ goto oom;
+ sudoers_gc_add(GC_PTR, nargv);
+
+ /* Fill in editor argv (assumes files[] is NULL-terminated). */
+ nargv[0] = editor;
+ editor = NULL;
+ for (nargc = 1; (cp = wordsplit(NULL, edend, &ep)) != NULL; nargc++) {
+ /* Copy string, collapsing chars escaped with a backslash. */
+ nargv[nargc] = copy_arg(cp, (size_t)(ep - cp));
+ if (nargv[nargc] == NULL)
+ goto oom;
+
+ /*
+ * We use "--" to separate the editor and arguments from the files
+ * to edit. The editor arguments themselves may not contain "--".
+ */
+ if (strcmp(nargv[nargc], "--") == 0) {
+ sudo_warnx(U_("ignoring editor: %.*s"), (int)edlen, ed);
+ sudo_warnx("%s", U_("editor arguments may not contain \"--\""));
+ errno = EINVAL;
+ goto bad;
+ }
+ }
+ if (nfiles != 0) {
+ nargv[nargc++] = (char *)"--";
+ do
+ nargv[nargc++] = *files++;
+ while (--nfiles > 0);
+ }
+ nargv[nargc] = NULL;
+
+ *argc_out = nargc;
+ *argv_out = nargv;
+ debug_return_str(editor_path);
+oom:
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+bad:
+ sudoers_gc_remove(GC_PTR, editor);
+ free(editor);
+ free(editor_path);
+ if (nargv != NULL) {
+ while (nargc > 0) {
+ sudoers_gc_remove(GC_PTR, nargv[--nargc]);
+ free(nargv[nargc]);
+ }
+ sudoers_gc_remove(GC_PTR, nargv);
+ free(nargv);
+ }
+ debug_return_str(NULL);
+}
+
+/*
+ * Determine which editor to use based on the SUDO_EDITOR, VISUAL and
+ * EDITOR environment variables as well as the editor path in sudoers.
+ *
+ * Returns the path to be executed on success, else NULL.
+ * The caller is responsible for freeing the returned editor path
+ * as well as the argument vector.
+ */
+char *
+find_editor(int nfiles, char * const *files, int *argc_out, char ***argv_out,
+ char * const *allowlist, const char **env_editor)
+{
+ char *editor_path = NULL;
+ const char *ev[3];
+ size_t i;
+ debug_decl(find_editor, SUDOERS_DEBUG_UTIL);
+
+ /*
+ * If any of SUDO_EDITOR, VISUAL or EDITOR are set, choose the first one.
+ */
+ *env_editor = NULL;
+ ev[0] = "SUDO_EDITOR";
+ ev[1] = "VISUAL";
+ ev[2] = "EDITOR";
+ for (i = 0; i < nitems(ev); i++) {
+ char *editor = getenv(ev[i]);
+
+ if (editor != NULL && *editor != '\0') {
+ *env_editor = editor;
+ editor_path = resolve_editor(editor, strlen(editor),
+ nfiles, files, argc_out, argv_out, allowlist);
+ if (editor_path != NULL)
+ break;
+ if (errno != ENOENT)
+ debug_return_str(NULL);
+ }
+ }
+
+ /*
+ * If SUDO_EDITOR, VISUAL and EDITOR were either not set or not
+ * allowed (based on the values of def_editor and def_env_editor),
+ * choose the first one in def_editor that exists.
+ */
+ if (editor_path == NULL) {
+ const char *def_editor_end = def_editor + strlen(def_editor);
+ const char *cp, *ep;
+
+ /* def_editor could be a path, split it up, avoiding strtok() */
+ for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep);
+ cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) {
+ editor_path = resolve_editor(cp, (size_t)(ep - cp), nfiles,
+ files, argc_out, argv_out, allowlist);
+ if (editor_path != NULL)
+ break;
+ if (errno != ENOENT)
+ debug_return_str(NULL);
+ }
+ }
+
+ /* Caller is responsible for freeing editor_path, not g/c'd. */
+ debug_return_str(editor_path);
+}