diff --git a/MANIFEST b/MANIFEST index 8a4656055..21c9caad2 100644 --- a/MANIFEST +++ b/MANIFEST @@ -129,6 +129,7 @@ lib/iolog/iolog_clearerr.c lib/iolog/iolog_close.c lib/iolog/iolog_conf.c lib/iolog/iolog_eof.c +lib/iolog/iolog_filter.c lib/iolog/iolog_flush.c lib/iolog/iolog_gets.c lib/iolog/iolog_json.c diff --git a/include/sudo_iolog.h b/include/sudo_iolog.h index 3bd032979..027285ecf 100644 --- a/include/sudo_iolog.h +++ b/include/sudo_iolog.h @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2009-2020 Todd C. Miller + * Copyright (c) 2009-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 @@ -56,6 +56,11 @@ #define IOFD_TIMING 5 #define IOFD_MAX 6 +/* + * Default password prompt regex. + */ +#define PASSPROMPT_REGEX "[Pp]assword[: ]*" + struct timing_closure { struct timespec delay; const char *decimal; @@ -140,4 +145,11 @@ void iolog_set_owner(uid_t uid, uid_t gid); bool iolog_swapids(bool restore); bool iolog_mkdirs(char *path); +/* iolog_filter.c */ +void *iolog_pwfilt_alloc(); +bool iolog_pwfilt_add(void *handle, const char *pattern); +void iolog_pwfilt_free(void *handle); +bool iolog_pwfilt_remove(void *handle, const char *pattern); +bool iolog_pwfilt_run(void *handle, int event, const char *buf, unsigned int len, char **newbuf); + #endif /* SUDO_IOLOG_H */ diff --git a/lib/iolog/Makefile.in b/lib/iolog/Makefile.in index f722ae1c7..68587f0e5 100644 --- a/lib/iolog/Makefile.in +++ b/lib/iolog/Makefile.in @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: ISC # -# Copyright (c) 2011-2021 Todd C. Miller +# Copyright (c) 2011-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 @@ -100,12 +100,12 @@ DEVEL = @DEVEL@ SHELL = @SHELL@ LIBIOLOG_OBJS = host_port.lo hostcheck.lo iolog_clearerr.lo iolog_close.lo \ - iolog_conf.lo iolog_eof.lo iolog_flush.lo iolog_gets.lo \ - iolog_json.lo iolog_legacy.lo iolog_loginfo.lo iolog_mkdirs.lo \ - iolog_mkdtemp.lo iolog_mkpath.lo iolog_nextid.lo \ - iolog_open.lo iolog_openat.lo iolog_path.lo iolog_read.lo \ - iolog_seek.lo iolog_swapids.lo iolog_timing.lo iolog_util.lo \ - iolog_write.lo + iolog_conf.lo iolog_eof.lo iolog_filter.lo iolog_flush.lo \ + iolog_gets.lo iolog_json.lo iolog_legacy.lo iolog_loginfo.lo \ + iolog_mkdirs.lo iolog_mkdtemp.lo iolog_mkpath.lo \ + iolog_nextid.lo iolog_open.lo iolog_openat.lo iolog_path.lo \ + iolog_read.lo iolog_seek.lo iolog_swapids.lo iolog_timing.lo \ + iolog_util.lo iolog_write.lo IOBJS = $(LIBIOLOG_OBJS:.lo=.i) @@ -553,6 +553,20 @@ iolog_eof.i: $(srcdir)/iolog_eof.c $(incdir)/compat/stdbool.h \ $(CC) -E -o $@ $(CPPFLAGS) $< iolog_eof.plog: iolog_eof.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_eof.c --i-file $< --output-file $@ +iolog_filter.lo: $(srcdir)/iolog_filter.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_filter.c +iolog_filter.i: $(srcdir)/iolog_filter.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +iolog_filter.plog: iolog_filter.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_filter.c --i-file $< --output-file $@ iolog_flush.lo: $(srcdir)/iolog_flush.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_iolog.h $(incdir)/sudo_queue.h \ diff --git a/lib/iolog/iolog_filter.c b/lib/iolog/iolog_filter.c new file mode 100644 index 000000000..a4b730e82 --- /dev/null +++ b/lib/iolog/iolog_filter.c @@ -0,0 +1,249 @@ +/* + * 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" + +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); + + 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; + char errbuf[1024]; + int errcode; + 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; + + errcode = regcomp(&filt->regex, pattern, REG_EXTENDED|REG_NOSUB); + if (errcode != 0) { + regerror(errcode, &filt->regex, errbuf, sizeof(errbuf)); + sudo_warnx(U_("invalid regular expression \"%s\": %s"), + pattern, errbuf); + 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; + break; + } + + /* 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_ptr(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') + 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_ptr(NULL); + } + memset(copy, '*', i); + if (i != len) { + /* Done filtering, copy cr/nl and subsequent characters. */ + memcpy(copy + i, buf + i, len - i); + handle->is_filtered = false; + } + *newbuf = copy; + } + } + break; + } + + debug_return_bool(true); +}