/* * SPDX-License-Identifier: ISC * * Copyright (c) 2022 Todd C. Miller * * 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 #include #include #include #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif #include #include #include #include "sudo_compat.h" #include "sudo_debug.h" #include "sudo_fatal.h" #include "sudo_gettext.h" #include "sudo_iolog.h" #include "sudo_queue.h" #include "sudo_util.h" struct pwfilt_regex { TAILQ_ENTRY(pwfilt_regex) entries; char *pattern; regex_t regex; }; TAILQ_HEAD(pwfilt_regex_list, pwfilt_regex); struct pwfilt_handle { struct pwfilt_regex_list filters; bool is_filtered; }; /* * Allocate a new filter handle. */ void * iolog_pwfilt_alloc(void) { struct pwfilt_handle *handle; debug_decl(iolog_pwfilt_alloc, SUDO_DEBUG_UTIL); handle = malloc(sizeof(*handle)); if (handle != NULL) { TAILQ_INIT(&handle->filters); handle->is_filtered = false; } debug_return_ptr(handle); } /* * Unlink filt from filters and free it. */ static void iolog_pwfilt_free_filter(struct pwfilt_regex_list *filters, struct pwfilt_regex *filt) { debug_decl(iolog_pwfilt_free_filter, SUDO_DEBUG_UTIL); if (filt != NULL) { TAILQ_REMOVE(filters, filt, entries); regfree(&filt->regex); free(filt->pattern); free(filt); } debug_return; } /* * Free the given password filter handle. */ void iolog_pwfilt_free(void *vhandle) { struct pwfilt_handle *handle = vhandle; struct pwfilt_regex *filt; debug_decl(iolog_pwfilt_free, SUDO_DEBUG_UTIL); if (handle != NULL) { while ((filt = TAILQ_FIRST(&handle->filters)) != NULL) { iolog_pwfilt_free_filter(&handle->filters, filt); } free(handle); } debug_return; } /* * Add a pattern to the password filter list. */ bool iolog_pwfilt_add(void *vhandle, const char *pattern) { struct pwfilt_handle *handle = vhandle; struct pwfilt_regex *filt; const char *errstr; debug_decl(iolog_pwfilt_add, SUDO_DEBUG_UTIL); filt = malloc(sizeof(*filt)); if (filt == NULL) goto oom; filt->pattern = strdup(pattern); if (filt->pattern == NULL) goto oom; if (!sudo_regex_compile(&filt->regex, filt->pattern, &errstr)) { sudo_warnx(U_("invalid regular expression \"%s\": %s"), pattern, U_(errstr)); goto bad; } TAILQ_INSERT_TAIL(&handle->filters, filt, entries); debug_return_bool(true); oom: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); bad: if (filt != NULL) { free(filt->pattern); free(filt); } debug_return_bool(false); } /* * Remove a pattern from the password filter list. */ bool iolog_pwfilt_remove(void *vhandle, const char *pattern) { struct pwfilt_handle *handle = vhandle; struct pwfilt_regex *filt, *next; bool ret = false; debug_decl(iolog_pwfilt_remove, SUDO_DEBUG_UTIL); TAILQ_FOREACH_SAFE(filt, &handle->filters, entries, next) { if (strcmp(filt->pattern, pattern) == 0) { iolog_pwfilt_free_filter(&handle->filters, filt); ret = true; } } debug_return_bool(ret); } /* * If logging output and filtering is _not_ enabled, match buf against the * password filter list patterns and, if there is a match, enable filtering. * If logging output and filtering _is_ enabled, disable filtering. * If logging input and filtering is enabled, replace all characters in * buf with stars ('*') up to the next linefeed or carriage return. */ bool iolog_pwfilt_run(void *vhandle, int event, const char *buf, unsigned int len, char **newbuf) { struct pwfilt_handle *handle = vhandle; struct pwfilt_regex *filt; char *copy; debug_decl(iolog_pwfilt_run, SUDO_DEBUG_UTIL); /* * We only filter ttyin/ttyout. It is only possible to disable * echo when a tty is present. Filtering passwords in the input * log when they appear in the output is pointless. This does * assume that the password prompt is written to the tty as well. */ switch (event) { case IO_EVENT_TTYOUT: /* If filtering passwords and we receive output, disable it. */ if (handle->is_filtered) handle->is_filtered = false; /* Make a copy of buf that is NUL-terminated. */ copy = malloc(len + 1); if (copy == NULL) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_bool(false); } memcpy(copy, buf, len); copy[len] = '\0'; /* Check output for a password prompt. */ TAILQ_FOREACH(filt, &handle->filters, entries) { if (regexec(&filt->regex, copy, 0, NULL, 0) == 0) { handle->is_filtered = true; break; } } free(copy); break; case IO_EVENT_TTYIN: if (handle->is_filtered) { unsigned int i; for (i = 0; i < len; i++) { /* We will stop filtering after reaching cr/nl. */ if (buf[i] == '\r' || buf[i] == '\n') { handle->is_filtered = false; break; } } if (i != 0) { /* Filtered, replace buffer with '*' chars. */ copy = malloc(len); if (copy == NULL) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_bool(false); } memset(copy, '*', i); if (i != len) { /* Done filtering, copy cr/nl and subsequent characters. */ memcpy(copy + i, buf + i, len - i); } *newbuf = copy; } } break; } debug_return_bool(true); }