diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
commit | f7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch) | |
tree | a3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/auth/db-checkpassword.c | |
parent | Initial commit. (diff) | |
download | dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.tar.xz dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.zip |
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/auth/db-checkpassword.c')
-rw-r--r-- | src/auth/db-checkpassword.c | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/src/auth/db-checkpassword.c b/src/auth/db-checkpassword.c new file mode 100644 index 0000000..7d52eef --- /dev/null +++ b/src/auth/db-checkpassword.c @@ -0,0 +1,563 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined(PASSDB_CHECKPASSWORD) || defined(USERDB_CHECKPASSWORD) + +#include "lib-signals.h" +#include "array.h" +#include "buffer.h" +#include "str.h" +#include "ioloop.h" +#include "hash.h" +#include "execv-const.h" +#include "env-util.h" +#include "safe-memset.h" +#include "strescape.h" +#include "child-wait.h" +#include "db-checkpassword.h" + +#include <unistd.h> +#include <sys/wait.h> + +#define CHECKPASSWORD_MAX_REQUEST_LEN 512 + +struct chkpw_auth_request { + struct db_checkpassword *db; + struct auth_request *request; + char *auth_password; + + db_checkpassword_callback_t *callback; + void (*request_callback)(); + + pid_t pid; + int fd_out, fd_in; + struct io *io_out, *io_in; + + string_t *input_buf; + size_t output_pos, output_len; + + int exit_status; + bool exited:1; +}; + +struct db_checkpassword { + char *checkpassword_path, *checkpassword_reply_path; + + HASH_TABLE(void *, struct chkpw_auth_request *) clients; + struct child_wait *child_wait; +}; + +static void +env_put_extra_fields(const ARRAY_TYPE(auth_field) *extra_fields) +{ + const struct auth_field *field; + const char *key, *value; + + array_foreach(extra_fields, field) { + key = t_str_ucase(field->key); + value = field->value != NULL ? field->value : "1"; + env_put(key, value); + } +} + +static void checkpassword_request_close(struct chkpw_auth_request *request) +{ + io_remove(&request->io_in); + io_remove(&request->io_out); + + i_close_fd(&request->fd_in); + i_close_fd(&request->fd_out); +} + +static void checkpassword_request_free(struct chkpw_auth_request **_request) +{ + struct chkpw_auth_request *request = *_request; + + *_request = NULL; + + if (!request->exited) { + hash_table_remove(request->db->clients, + POINTER_CAST(request->pid)); + child_wait_remove_pid(request->db->child_wait, request->pid); + } + checkpassword_request_close(request); + + if (request->auth_password != NULL) { + safe_memset(request->auth_password, 0, + strlen(request->auth_password)); + i_free(request->auth_password); + } + auth_request_unref(&request->request); + str_free(&request->input_buf); + i_free(request); +} + +static void checkpassword_finish(struct chkpw_auth_request **_request, + enum db_checkpassword_status status) +{ + struct chkpw_auth_request *request = *_request; + const char *const *extra_fields; + + *_request = NULL; + + extra_fields = t_strsplit_tabescaped(str_c(request->input_buf)); + request->callback(request->request, status, extra_fields, + request->request_callback); + checkpassword_request_free(&request); +} + +static void checkpassword_internal_failure(struct chkpw_auth_request **request) +{ + checkpassword_finish(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE); +} + +static void +checkpassword_request_finish_auth(struct chkpw_auth_request *request) +{ + switch (request->exit_status) { + /* standard checkpassword exit codes: */ + case 1: + e_info(authdb_event(request->request), + "Login failed (status=%d)", + request->exit_status); + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE); + break; + case 0: + if (request->input_buf->used == 0) { + e_error(authdb_event(request->request), + "Received no input"); + checkpassword_internal_failure(&request); + break; + } + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK); + break; + case 2: + /* checkpassword is called with wrong parameters? unlikely */ + e_error(authdb_event(request->request), + "Child %s exited with status 2 (tried to use " + "userdb-only checkpassword program for passdb?)", + dec2str(request->pid)); + checkpassword_internal_failure(&request); + break; + case 111: + /* temporary problem, treat as internal error */ + default: + /* whatever error.. */ + e_error(authdb_event(request->request), + "Child %s exited with status %d", + dec2str(request->pid), request->exit_status); + checkpassword_internal_failure(&request); + break; + } +} + +static void +checkpassword_request_finish_lookup(struct chkpw_auth_request *request) +{ + switch (request->exit_status) { + case 3: + /* User does not exist. */ + e_info(authdb_event(request->request), + "User unknown"); + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE); + break; + case 2: + /* This is intentionally not 0. checkpassword-reply exits with + 2 on success when AUTHORIZED is set. */ + if (request->input_buf->used == 0) { + e_error(authdb_event(request->request), + "Received no input"); + checkpassword_internal_failure(&request); + break; + } + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK); + break; + default: + /* whatever error... */ + e_error(authdb_event(request->request), + "Child %s exited with status %d", + dec2str(request->pid), request->exit_status); + checkpassword_internal_failure(&request); + break; + } +} + +static void +checkpassword_request_half_finish(struct chkpw_auth_request *request) +{ + /* the process must have exited, and the input fd must have closed */ + if (!request->exited || request->fd_in != -1) + return; + + if (request->auth_password != NULL) + checkpassword_request_finish_auth(request); + else + checkpassword_request_finish_lookup(request); +} + +static void env_put_auth_vars(struct auth_request *request) +{ + const struct var_expand_table *tab; + unsigned int i; + + tab = auth_request_get_var_expand_table(request, NULL); + for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) { + /* avoid keeping passwords in environment .. just in case + an attacker might find it from there. environment is no + longer world-readable in modern OSes, but maybe the attacker + could be running with the same UID. of course then the + attacker could usually ptrace() the process, except that is + disabled on some secured systems. so, although I find it + highly unlikely anyone could actually attack Dovecot this + way in a real system, be safe just in case. besides, lets + try to keep at least minimally compatible with the + checkpassword API. */ + if (tab[i].long_key != NULL && tab[i].value != NULL && + strcasecmp(tab[i].long_key, "password") != 0) { + env_put(t_strdup_printf("AUTH_%s", + t_str_ucase(tab[i].long_key)), + tab[i].value); + } + } +} + +static void checkpassword_setup_env(struct auth_request *request) +{ + const struct auth_request_fields *fields = &request->fields; + + /* Besides passing the standard username and password in a + pipe, also pass some other possibly interesting information + via environment. Use UCSPI names for local/remote IPs. */ + env_put("PROTO", "TCP"); /* UCSPI */ + env_put("ORIG_UID", dec2str(getuid())); + env_put("SERVICE", fields->service); + if (fields->local_ip.family != 0) { + env_put("TCPLOCALIP", net_ip2addr(&fields->local_ip)); + /* FIXME: for backwards compatibility only, + remove some day */ + env_put("LOCAL_IP", net_ip2addr(&fields->local_ip)); + } + if (fields->remote_ip.family != 0) { + env_put("TCPREMOTEIP", net_ip2addr(&fields->remote_ip)); + /* FIXME: for backwards compatibility only, + remove some day */ + env_put("REMOTE_IP", net_ip2addr(&fields->remote_ip)); + } + if (fields->local_port != 0) + env_put("TCPLOCALPORT", dec2str(fields->local_port)); + if (fields->remote_port != 0) + env_put("TCPREMOTEPORT", dec2str(fields->remote_port)); + if (fields->master_user != NULL) + env_put("MASTER_USER", fields->master_user); + if (!auth_fields_is_empty(fields->extra_fields)) { + const ARRAY_TYPE(auth_field) *extra_fields = + auth_fields_export(fields->extra_fields); + + /* extra fields could come from master db */ + env_put_extra_fields(extra_fields); + } + env_put_auth_vars(request); +} + +static const char * +checkpassword_get_cmd(struct auth_request *request, const char *args, + const char *checkpassword_reply_path) +{ + string_t *str; + const char *error; + + str = t_str_new(256); + if (auth_request_var_expand(str, args, request, NULL, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand checkpassword_path=%s: %s", + args, error); + } + + return t_strconcat(str_c(str), " ", checkpassword_reply_path, NULL); +} + +static void checkpassword_child_input(struct chkpw_auth_request *request) +{ + unsigned char buf[1024]; + ssize_t ret; + + ret = read(request->fd_in, buf, sizeof(buf)); + if (ret > 0) { + str_append_data(request->input_buf, buf, ret); + return; + } + + if (ret < 0) { + e_error(authdb_event(request->request), + "read() failed: %m"); + checkpassword_internal_failure(&request); + } else if (memchr(str_data(request->input_buf), '\0', + str_len(request->input_buf)) != NULL) { + e_error(authdb_event(request->request), + "NUL characters in checkpassword reply"); + checkpassword_internal_failure(&request); + } else if (strchr(str_c(request->input_buf), '\n') != NULL) { + e_error(authdb_event(request->request), + "LF characters in checkpassword reply"); + checkpassword_internal_failure(&request); + } else { + e_debug(authdb_event(request->request), + "Received input: %s", str_c(request->input_buf)); + checkpassword_request_close(request); + checkpassword_request_half_finish(request); + } +} + +static void checkpassword_child_output(struct chkpw_auth_request *request) +{ + /* Send: username \0 password \0 timestamp \0. + Must be 512 bytes or less. The "timestamp" parameter is actually + useful only for APOP authentication. We don't support it, so + keep it empty */ + struct auth_request *auth_request = request->request; + buffer_t *buf; + const unsigned char *data; + size_t size; + ssize_t ret; + + buf = t_buffer_create(CHECKPASSWORD_MAX_REQUEST_LEN); + buffer_append(buf, auth_request->fields.user, + strlen(auth_request->fields.user)+1); + if (request->auth_password != NULL) { + buffer_append(buf, request->auth_password, + strlen(request->auth_password)+1); + } else { + buffer_append_c(buf, '\0'); + } + buffer_append_c(buf, '\0'); + data = buffer_get_data(buf, &size); + + i_assert(size == request->output_len); + /* already checked this */ + i_assert(size <= CHECKPASSWORD_MAX_REQUEST_LEN); + + ret = write(request->fd_out, data + request->output_pos, + size - request->output_pos); + if (ret <= 0) { + if (ret < 0) { + e_error(authdb_event(request->request), + "write() failed: %m"); + } else { + e_error(authdb_event(request->request), + "write() returned 0"); + } + checkpassword_internal_failure(&request); + return; + } + + request->output_pos += ret; + if (request->output_pos < size) + return; + + /* finished sending the data */ + io_remove(&request->io_out); + + if (close(request->fd_out) < 0) + e_error(authdb_event(request->request), "close() failed: %m"); + request->fd_out = -1; +} + +static void ATTR_NORETURN +checkpassword_exec(struct db_checkpassword *db, struct auth_request *request, + int fd_in, int fd_out, bool authenticate) +{ + const char *cmd, *const *args; + + /* fd 3 is used to send the username+password for the script + fd 4 is used to communicate with checkpassword-reply */ + if (dup2(fd_out, 3) < 0 || dup2(fd_in, 4) < 0) { + e_error(authdb_event(request), + "dup2() failed: %m"); + lib_exit(111); + } + + if (!authenticate) { + /* We want to retrieve passdb/userdb data and don't do + authorization, so we need to signalize the + checkpassword program that the password shall be + ignored by setting AUTHORIZED. This needs a + special checkpassword program which knows how to + handle this. */ + env_put("AUTHORIZED", "1"); + if (request->wanted_credentials_scheme != NULL) { + /* passdb credentials lookup */ + env_put("CREDENTIALS_LOOKUP", "1"); + env_put("SCHEME", request->wanted_credentials_scheme); + } + } + checkpassword_setup_env(request); + cmd = checkpassword_get_cmd(request, db->checkpassword_path, + db->checkpassword_reply_path); + e_debug(authdb_event(request), "execute: %s", cmd); + + /* very simple argument splitting. */ + args = t_strsplit(cmd, " "); + execv_const(args[0], args); +} + +static void sigchld_handler(const struct child_wait_status *status, + struct db_checkpassword *db) +{ + struct chkpw_auth_request *request = + hash_table_lookup(db->clients, POINTER_CAST(status->pid)); + + i_assert(request != NULL); + + hash_table_remove(db->clients, POINTER_CAST(status->pid)); + request->exited = TRUE; + + if (WIFSIGNALED(status->status)) { + e_error(authdb_event(request->request), + "Child %s died with signal %d", + dec2str(status->pid), WTERMSIG(status->status)); + checkpassword_internal_failure(&request); + } else if (WIFEXITED(status->status)) { + request->exit_status = WEXITSTATUS(status->status); + + e_debug(authdb_event(request->request), + "exit_status=%d", request->exit_status); + checkpassword_request_half_finish(request); + } else { + /* shouldn't happen */ + e_debug(authdb_event(request->request), + "Child %s exited with status=%d", + dec2str(status->pid), status->status); + checkpassword_internal_failure(&request); + } +} + +void db_checkpassword_call(struct db_checkpassword *db, + struct auth_request *request, + const char *auth_password, + db_checkpassword_callback_t *callback, + void (*request_callback)()) +{ + struct chkpw_auth_request *chkpw_auth_request; + size_t output_len; + int fd_in[2], fd_out[2]; + pid_t pid; + + /* <username> \0 <password> \0 timestamp \0 */ + output_len = strlen(request->fields.user) + 3; + if (auth_password != NULL) + output_len += strlen(auth_password); + if (output_len > CHECKPASSWORD_MAX_REQUEST_LEN) { + e_info(authdb_event(request), + "Username+password combination too long (%zu bytes)", + output_len); + callback(request, DB_CHECKPASSWORD_STATUS_FAILURE, + NULL, request_callback); + return; + } + + fd_in[0] = -1; + if (pipe(fd_in) < 0 || pipe(fd_out) < 0) { + e_error(authdb_event(request), + "pipe() failed: %m"); + if (fd_in[0] != -1) { + i_close_fd(&fd_in[0]); + i_close_fd(&fd_in[1]); + } + callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE, + NULL, request_callback); + return; + } + + pid = fork(); + if (pid == -1) { + e_error(authdb_event(request), + "fork() failed: %m"); + i_close_fd(&fd_in[0]); + i_close_fd(&fd_in[1]); + i_close_fd(&fd_out[0]); + i_close_fd(&fd_out[1]); + callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE, + NULL, request_callback); + return; + } + + if (pid == 0) { + /* child */ + i_close_fd(&fd_in[0]); + i_close_fd(&fd_out[1]); + checkpassword_exec(db, request, fd_in[1], fd_out[0], + auth_password != NULL); + /* not reached */ + } + + if (close(fd_in[1]) < 0) { + e_error(authdb_event(request), + "close(fd_in[1]) failed: %m"); + } + if (close(fd_out[0]) < 0) { + e_error(authdb_event(request), + "close(fd_out[0]) failed: %m"); + } + + auth_request_ref(request); + chkpw_auth_request = i_new(struct chkpw_auth_request, 1); + chkpw_auth_request->db = db; + chkpw_auth_request->pid = pid; + chkpw_auth_request->fd_in = fd_in[0]; + chkpw_auth_request->fd_out = fd_out[1]; + chkpw_auth_request->auth_password = i_strdup(auth_password); + chkpw_auth_request->request = request; + chkpw_auth_request->output_len = output_len; + chkpw_auth_request->input_buf = str_new(default_pool, 256); + chkpw_auth_request->callback = callback; + chkpw_auth_request->request_callback = request_callback; + + chkpw_auth_request->io_in = + io_add(fd_in[0], IO_READ, checkpassword_child_input, + chkpw_auth_request); + chkpw_auth_request->io_out = + io_add(fd_out[1], IO_WRITE, checkpassword_child_output, + chkpw_auth_request); + + hash_table_insert(db->clients, POINTER_CAST(pid), chkpw_auth_request); + child_wait_add_pid(db->child_wait, pid); +} + +struct db_checkpassword * +db_checkpassword_init(const char *checkpassword_path, + const char *checkpassword_reply_path) +{ + struct db_checkpassword *db; + + db = i_new(struct db_checkpassword, 1); + db->checkpassword_path = i_strdup(checkpassword_path); + db->checkpassword_reply_path = i_strdup(checkpassword_reply_path); + hash_table_create_direct(&db->clients, default_pool, 0); + db->child_wait = + child_wait_new_with_pid((pid_t)-1, sigchld_handler, db); + return db; +} + +void db_checkpassword_deinit(struct db_checkpassword **_db) +{ + struct db_checkpassword *db = *_db; + struct hash_iterate_context *iter; + void *key; + struct chkpw_auth_request *request; + + *_db = NULL; + + iter = hash_table_iterate_init(db->clients); + while (hash_table_iterate(iter, db->clients, &key, &request)) + checkpassword_internal_failure(&request); + hash_table_iterate_deinit(&iter); + + child_wait_free(&db->child_wait); + hash_table_destroy(&db->clients); + i_free(db->checkpassword_reply_path); + i_free(db->checkpassword_path); + i_free(db); +} + +#endif |