Commit message: auth: Do not double-expand key in passdb dict when authenticating

Broken by 79042f8c
CVE: CVE-2017-2669
CWE: CWE-284
Vulnerability description: Dovecot before version 2.2.29 is vulnerable to a denial of service. When 'dict' passdb and userdb were used for user authentication, the username sent by the IMAP/POP3 client was sent through var_expand() to perform %variable expansion. Sending specially crafted %variable fields could result in excessive memory usage causing the process to crash (and restart), or excessive CPU usage causing all authentications to hang.

Project: core

bool db_dict_value_iter_next(struct db_dict_value_iter *iter,
			     const char **key_r, const char **value_r)
{
	static struct var_expand_func_table var_funcs_table[] = {
		{ "dict", db_dict_field_find },
		{ NULL, NULL }
	};
	const struct db_dict_field *field;
	const char *error;

	if (iter->field_idx == array_count(iter->fields))
		return db_dict_value_iter_object_next(iter, key_r, value_r);
	field = array_idx(iter->fields, iter->field_idx++);

	str_truncate(iter->tmpstr, 0);
	if (var_expand_with_funcs(iter->tmpstr, field->value,
				  iter->var_expand_table, var_funcs_table,
				  iter, &error) <= 0) {
		iter->error = p_strdup_printf(iter->pool,
			"Failed to expand %s=%s: %s",
			field->name, field->value, error);
		return FALSE;
	}
	*key_r = field->name;
	*value_r = str_c(iter->tmpstr);
	return TRUE;
}

Project: core

db_dict_field_find(const char *data, void *context,
		   const char **value_r,
		   const char **error_r ATTR_UNUSED)
{
	struct db_dict_value_iter *iter = context;
	struct db_dict_iter_key *key;
	const char *name, *value, *dotname = strchr(data, '.');
	string_t *tmpstr;

	*value_r = NULL;

	if (dotname != NULL)
		data = t_strdup_until(data, dotname++);
	key = db_dict_iter_find_key(iter, data);
	if (key == NULL)
		return 1;

	switch (key->key->parsed_format) {
	case DB_DICT_VALUE_FORMAT_VALUE:
		*value_r = dotname != NULL ? NULL :
			(key->value == NULL ? "" : key->value);
		return 1;
	case DB_DICT_VALUE_FORMAT_JSON:
		if (dotname == NULL)
			return 1;
		db_dict_value_iter_json_init(iter, key->value);
		*value_r = "";
		tmpstr = t_str_new(64);
		while (db_dict_value_iter_json_next(iter, tmpstr, &name, &value)) {
			if (strcmp(name, dotname) == 0) {
				*value_r = t_strdup(value);
				break;
			}
		}
		(void)json_parser_deinit(&iter->json_parser, &iter->error);
		return 1;
	}
	i_unreached();
}

Project: core

struct dict_connection *db_dict_init(const char *config_path)
{
	struct dict_settings dict_set;
	struct dict_settings_parser_ctx ctx;
	struct dict_connection *conn;
	const char *error;
	pool_t pool;

	conn = dict_conn_find(config_path);
	if (conn != NULL) {
		conn->refcount++;
		return conn;
	}

	if (*config_path == '\0')
		i_fatal("dict: Configuration file path not given");

	pool = pool_alloconly_create("dict_connection", 1024);
	conn = p_new(pool, struct dict_connection, 1);
	conn->pool = pool;

	conn->refcount = 1;

	conn->config_path = p_strdup(pool, config_path);
	conn->set = default_dict_settings;
	p_array_init(&conn->set.keys, pool, 8);
	p_array_init(&conn->set.passdb_fields, pool, 8);
	p_array_init(&conn->set.userdb_fields, pool, 8);
	p_array_init(&conn->set.parsed_passdb_objects, pool, 2);
	p_array_init(&conn->set.parsed_userdb_objects, pool, 2);

	i_zero(&ctx);
	ctx.conn = conn;
	if (!settings_read(config_path, NULL, parse_setting,
			   parse_section, &ctx, &error))
		i_fatal("dict %s: %s", config_path, error);
	db_dict_settings_parse(&conn->set);

	if (conn->set.uri == NULL)
		i_fatal("dict %s: Empty uri setting", config_path);

	i_zero(&dict_set);
	dict_set.username = "";
	dict_set.base_dir = global_auth_settings->base_dir;
	if (dict_init(conn->set.uri, &dict_set, &conn->dict, &error) < 0)
		i_fatal("dict %s: Failed to init dict: %s", config_path, error);

	conn->next = connections;
	connections = conn;
	return conn;
}

Project: core

static int db_dict_iter_lookup_key_values(struct db_dict_value_iter *iter)
{
	struct db_dict_iter_key *key;
	string_t *path;
	const char *error;
	int ret;

	/* sort the keys so that we'll first lookup the keys without
	   default value. if their lookup fails, the user doesn't exist. */
	array_sort(&iter->keys, db_dict_iter_key_cmp);

	path = t_str_new(128);
	str_append(path, DICT_PATH_SHARED);

	array_foreach_modifiable(&iter->keys, key) {
		if (!key->used)
			continue;

		str_truncate(path, strlen(DICT_PATH_SHARED));
		str_append(path, key->key->key);
		ret = dict_lookup(iter->conn->dict, iter->pool,
				  str_c(path), &key->value, &error);
		if (ret > 0) {
			auth_request_log_debug(iter->auth_request, AUTH_SUBSYS_DB,
					       "Lookup: %s = %s", str_c(path),
					       key->value);
		} else if (ret < 0) {
			auth_request_log_error(iter->auth_request, AUTH_SUBSYS_DB,
				"Failed to lookup key %s: %s", str_c(path), error);
			return -1;
		} else if (key->key->default_value != NULL) {
			auth_request_log_debug(iter->auth_request, AUTH_SUBSYS_DB,
				"Lookup: %s not found, using default value %s",
				str_c(path), key->key->default_value);
			key->value = key->key->default_value;
		} else {
			return 0;
		}
	}
	return 1;
}

Project: core

int db_dict_value_iter_init(struct dict_connection *conn,
			    struct auth_request *auth_request,
			    const ARRAY_TYPE(db_dict_field) *fields,
			    const ARRAY_TYPE(db_dict_key_p) *objects,
			    struct db_dict_value_iter **iter_r)
{
	struct db_dict_value_iter *iter;
	struct db_dict_iter_key *iterkey;
	const struct db_dict_key *key;
	pool_t pool;
	int ret;

	pool = pool_alloconly_create(MEMPOOL_GROWING"auth dict lookup", 1024);
	iter = p_new(pool, struct db_dict_value_iter, 1);
	iter->pool = pool;
	iter->conn = conn;
	iter->fields = fields;
	iter->objects = objects;
	iter->tmpstr = str_new(pool, 128);
	iter->auth_request = auth_request;
	iter->var_expand_table = auth_request_get_var_expand_table(auth_request, NULL);

	/* figure out what keys we need to lookup, and lookup them */
	p_array_init(&iter->keys, pool, array_count(&conn->set.keys));
	array_foreach(&conn->set.keys, key) {
		iterkey = array_append_space(&iter->keys);
		struct db_dict_key *new_key = p_new(iter->pool, struct db_dict_key, 1);
		memcpy(new_key, key, sizeof(struct db_dict_key));
		string_t *expanded_key = str_new(iter->pool, strlen(key->key));
		const char *error;
		if (auth_request_var_expand_with_table(expanded_key, key->key, auth_request,
						       iter->var_expand_table,
						       NULL, &error) <= 0) {
			auth_request_log_error(iter->auth_request, AUTH_SUBSYS_DB,
				"Failed to expand key %s: %s", key->key, error);
			pool_unref(&pool);
			return -1;
		}
		new_key->key = str_c(expanded_key);
		iterkey->key = new_key;
	}
	T_BEGIN {
		db_dict_iter_find_used_keys(iter);
		db_dict_iter_find_used_objects(iter);
		ret = db_dict_iter_lookup_key_values(iter);
	} T_END;
	if (ret <= 0) {
		pool_unref(&pool);
		return ret;
	}
	*iter_r = iter;
	return 1;
}