sudo/lib/iolog/iolog_filter.c

249 lines
6.2 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 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/types.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif
#include <regex.h>
#include <string.h>
#include <time.h>
#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,
size_t 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);
}