Commit message: auth: Fix auth-policy crash when username is NULL

If SASL request is invalid, or incomplete, and username
is left NULL, handle it gracefully by adding just
NUL byte in auth policy digest for username.
CVE: CVE-2016-8652
CWE: CWE-20
Vulnerability description: The auth component in Dovecot before 2.2.27, when auth-policy is configured, allows a remote attackers to cause a denial of service (crash) by aborting authentication without setting a username.

Project: core

void auth_policy_parse_response(struct policy_lookup_ctx *context)
{
	enum json_type type;
	const char *value;
	int ret;

	while((ret = json_parse_next(context->parser, &type, &value)) == 1) {
		if (context->parse_state == POLICY_RESULT) {
			if (type != JSON_TYPE_OBJECT_KEY)
				break;
			else if (strcmp(value, "status") == 0)
				context->parse_state = POLICY_RESULT_VALUE_STATUS;
			else if (strcmp(value, "msg") == 0)
				context->parse_state = POLICY_RESULT_VALUE_MESSAGE;
			else break;
		} else if (context->parse_state == POLICY_RESULT_VALUE_STATUS) {
			if (type != JSON_TYPE_NUMBER || str_to_int(value, &(context->result)) != 0)
				break;
			context->parse_state = POLICY_RESULT;
		} else if (context->parse_state == POLICY_RESULT_VALUE_MESSAGE) {
			if (type != JSON_TYPE_STRING)
				break;
			if (*value != '\0')
				context->message = p_strdup(context->pool, value);
			context->parse_state = POLICY_RESULT;
		} else {
			break;
		}
	}

	if (ret == 0 && !context->payload->eof)
		return;

	context->parse_error = TRUE;

	io_remove(&(context->io));

	if (context->payload->stream_errno != 0) {
		auth_request_log_error(context->request, "policy",
			"Error reading policy server result: %s",
			i_stream_get_error(context->payload));
	} else if (ret == 0 && context->payload->eof) {
		auth_request_log_error(context->request, "policy",
			"Policy server result was too short");
	} else if (ret == 1) {
		auth_request_log_error(context->request, "policy",
			"Policy server response was malformed");
	} else {
		const char *error = "unknown";
		if (json_parser_deinit(&(context->parser), &error) != 0)
			auth_request_log_error(context->request, "policy",
				"Policy server response JSON parse error: %s", error);
		else if (context->parse_state == POLICY_RESULT)
			context->parse_error = FALSE;
	}
	i_stream_unref(&(context->payload));

	if (context->parse_error) {
		context->result = (context->set->policy_reject_on_fail ? -1 : 0);
	}

	context->request->policy_refusal = FALSE;

	if (context->result < 0) {
		if (context->message != NULL) {
			/* set message here */
			auth_request_log_debug(context->request, "policy",
				"Policy response %d with message: %s",
				context->result, context->message);
			auth_request_set_field(context->request, "reason", context->message, NULL);
		}
		context->request->policy_refusal = TRUE;
	} else {
		auth_request_log_debug(context->request, "policy",
			"Policy response %d", context->result);
	}

	if (context->request->policy_refusal == TRUE && context->set->verbose == TRUE) {
		auth_request_log_info(context->request, "policy", "Authentication failure due to policy server refusal%s%s",
			(context->message!=NULL?": ":""),
			(context->message!=NULL?context->message:""));
	}

	if (context->callback != NULL) {
		context->callback(context->result, context->callback_context);
	}
};

Project: core

void auth_policy_create_json(struct policy_lookup_ctx *context,
	const char *password, bool include_success)
{
	const struct var_expand_table *var_table;
	context->json = str_new(context->pool, 64);
	unsigned char *ptr;
	const struct hash_method *digest = hash_method_lookup(context->set->policy_hash_mech);

	i_assert(digest != NULL);

	void *ctx = t_malloc(digest->context_size);
	string_t *buffer = t_str_new(64);

	digest->init(ctx);
	digest->loop(ctx,
		context->set->policy_hash_nonce,
		strlen(context->set->policy_hash_nonce));
	/* use +1 to make sure \0 gets included */
	if (context->request->user == NULL)
		digest->loop(ctx, "\0", 1);
	else
		digest->loop(ctx, context->request->user, strlen(context->request->user) + 1);
	if (password != NULL)
		digest->loop(ctx, password, strlen(password));
	ptr = (unsigned char*)str_c_modifiable(buffer);
	digest->result(ctx, ptr);
	str_truncate(buffer, digest->digest_size);
	if (context->set->policy_hash_truncate > 0) {
		buffer_truncate_rshift_bits(buffer, context->set->policy_hash_truncate);
	}
	const char *hashed_password = binary_to_hex(str_data(buffer), str_len(buffer));
	str_append_c(context->json, '{');
	var_table = policy_get_var_expand_table(context->request, hashed_password);
	auth_request_var_expand_with_table(context->json, auth_policy_json_template,
					   context->request, var_table,
					   auth_policy_escape_function);
	if (include_success) {
		str_append(context->json, ",\"success\":");
		if (!context->request->failed && context->request->successful &&
		    !context->request->internal_failure)
			str_append(context->json, "true");
		else
			str_append(context->json, "false");
		str_append(context->json, ",\"policy_reject\":");
		str_append(context->json, context->request->policy_refusal ? "true" : "false");
	}
	str_append_c(context->json, '}');
	auth_request_log_debug(context->request, "policy",
		"Policy server request JSON: %s", str_c(context->json));
}

Project: core

void auth_policy_process_response(const struct http_response *response,
	void *ctx)
{
	struct policy_lookup_ctx *context = ctx;

	context->payload = response->payload;

	if ((response->status / 10) != 20) {
		auth_request_log_error(context->request, "policy",
			"Policy server HTTP error: %d %s", response->status, response->reason);
		if (context->callback != NULL)
			context->callback(context->result, context->callback_context);
		return;
	}

	if (response->payload == NULL) {
		if (context->expect_result)
			auth_request_log_error(context->request, "policy",
				"Policy server result was empty");
		if (context->callback != NULL)
			context->callback(context->result, context->callback_context);
		return;
	}

	if (context->expect_result) {
		i_stream_ref(response->payload);
		context->io = io_add_istream(response->payload, auth_policy_parse_response, context);
		context->parser = json_parser_init(response->payload);
		auth_policy_parse_response(ctx);
	} else {
		auth_request_log_debug(context->request, "policy",
			"Policy response %d", context->result);
		if (context->callback != NULL)
			context->callback(context->result, context->callback_context);
	}
}

Project: core

const char *auth_policy_escape_function(const char *string,
	const struct auth_request *auth_request ATTR_UNUSED)
{
	string_t *tmp = t_str_new(64);
	json_append_escaped(tmp, string);
	return str_c(tmp);
}

Project: core

void auth_policy_send_request(struct policy_lookup_ctx *context)
{
	const char *error;
	struct http_url *url;
	if (http_url_parse(context->url, NULL, HTTP_URL_ALLOW_USERINFO_PART,
			   context->pool, &url, &error) != 0) {
		auth_request_log_error(context->request, "policy",
			"Could not parse url %s: %s", context->url, error);
		auth_policy_finish(context);
		return;
	}
	context->http_request = http_client_request_url(http_client,
		"POST", url, auth_policy_process_response, (void*)context);
	http_client_request_set_destroy_callback(context->http_request, auth_policy_finish, context);
	http_client_request_add_header(context->http_request, "Content-Type", "application/json");
	if (*context->set->policy_server_api_header != 0) {
		const char *ptr;
		if ((ptr = strstr(context->set->policy_server_api_header, ":")) != NULL) {
			const char *header = t_strcut(context->set->policy_server_api_header, ':');
			http_client_request_add_header(context->http_request, header, ptr + 1);
		} else {
			http_client_request_add_header(context->http_request,
				"X-API-Key", context->set->policy_server_api_header);
		}
	}
	if (url->user != NULL) {
		/* allow empty password */
		http_client_request_set_auth_simple(context->http_request, url->user,
			(url->password != NULL ? url->password : ""));
	}
	struct istream *is = i_stream_create_from_buffer(context->json);
	http_client_request_set_payload(context->http_request, is, FALSE);
	i_stream_unref(&is);
	http_client_request_submit(context->http_request);
	auth_request_ref(context->request);
}

Project: core

int auth_policy_strptrcmp(const char *a0, const char *a1,
			  const char *b0, const char *b1)
{
	i_assert(a0 <= a1 && b0 <= b1);
	return memcmp(a0, b0, I_MIN((a1-a0),(b1-b0)));
}

Project: core

void auth_policy_report(struct auth_request *request)
{
	if (request->master != NULL)
		return;

	if (*(request->set->policy_server_url) == '\0')
		return;
	struct policy_lookup_ctx *ctx = p_new(request->pool, struct policy_lookup_ctx, 1);
	ctx->pool = request->pool;
	ctx->request = request;
	ctx->expect_result = FALSE;
	ctx->set = request->set;
	auth_policy_url(ctx, "report");
	auth_request_log_debug(request, "policy", "Policy request %s", ctx->url);
	T_BEGIN {
		auth_policy_create_json(ctx, request->mech_password, TRUE);
	} T_END;
	auth_policy_send_request(ctx);
}

Project: core

int auth_policy_attribute_comparator(const struct policy_template_keyvalue *a,
	const struct policy_template_keyvalue *b)
{
	return strcmp(a->key, b->key);
}

Project: core

void auth_policy_url(struct policy_lookup_ctx *context, const char *command)
{
	size_t len = strlen(context->set->policy_server_url);
	if (context->set->policy_server_url[len-1] == '&')
		context->url = p_strdup_printf(context->pool, "%scommand=%s",
			context->set->policy_server_url, command);
	else
		context->url = p_strdup_printf(context->pool, "%s?command=%s",
			context->set->policy_server_url, command);
}

Project: core

const struct var_expand_table *policy_get_var_expand_table(struct auth_request *auth_request,
	const char *hashed_password)
{
	struct var_expand_table *table;
	unsigned int count = 1;

	table = auth_request_get_var_expand_table_full(auth_request, auth_policy_escape_function,
						       &count);
	table[0].key = '\0';
	table[0].long_key = "hashed_password";
	table[0].value = hashed_password;
	if (table[0].value != NULL)
		table[0].value = auth_policy_escape_function(table[0].value, auth_request);

	return table;
}

Project: core

void auth_policy_init(void)
{
	http_client_set.request_absolute_timeout_msecs = global_auth_settings->policy_server_timeout_msecs;
	if (global_auth_settings->debug)
		http_client_set.debug = 1;
	http_client = http_client_init(&http_client_set);

	/* prepare template */

	ARRAY(struct policy_template_keyvalue) attribute_pairs;
	const struct policy_template_keyvalue *kvptr;
	string_t *template = t_str_new(64);
	const char **ptr;
	const char *key = NULL;
	const char **list = t_strsplit_spaces(global_auth_settings->policy_request_attributes, "= ");

	t_array_init(&attribute_pairs, 8);
	for(ptr = list; *ptr != NULL; ptr++) {
		struct policy_template_keyvalue pair;
		if (key == NULL) {
			key = *ptr;
		} else {
			pair.key = key;
			pair.value = *ptr;
			key = NULL;
			array_append(&attribute_pairs, &pair, 1);
		}
	}
	if (key != NULL) {
		i_fatal("auth_policy_request_attributes contains invalid value");
	}

	/* then we sort it */
	array_sort(&attribute_pairs, auth_policy_attribute_comparator);

	/* and build a template string */
	const char *prevkey = "";

	array_foreach(&attribute_pairs, kvptr) {
		const char *kptr = strchr(kvptr->key, '/');
		auth_policy_open_and_close_to_key(prevkey, kvptr->key, template);
		str_append_c(template,'"');
		json_append_escaped(template, (kptr != NULL?kptr+1:kvptr->key));
		str_append_c(template,'"');
		str_append_c(template,':');
		str_append_c(template,'"');
		str_append(template,kvptr->value);
		str_append_c(template,'"');
		str_append_c(template,',');
		prevkey = kvptr->key;
	}

	auth_policy_open_and_close_to_key(prevkey, "", template);
	str_truncate(template, str_len(template)-1);
	auth_policy_json_template = i_strdup(str_c(template));
}

Project: core

void auth_policy_open_and_close_to_key(const char *fromkey, const char *tokey, string_t *template)
{
	const char *fptr,*tptr,*fdash,*tdash;

	fptr = strrchr(fromkey, '/');
	tptr = strrchr(tokey, '/');

	if (fptr == NULL && tptr == NULL) return; /* nothing to do */

	if (fptr == NULL && tptr != NULL) {
		auth_policy_open_key(tokey, template);
		return;
	}

	if (fptr != NULL && tptr == NULL) {
		str_truncate(template, str_len(template)-1);

		auth_policy_close_key(fromkey, template);
		str_append_c(template, ',');
		return;
	}

	if (auth_policy_strptrcmp(fromkey, fptr, tokey, tptr) == 0) {
		/* nothing to do, again */
		return;
	}

	fptr = fromkey;
	tptr = tokey;

	while (fptr != NULL && tptr != NULL) {
		fdash = strchr(fptr, '/');
		tdash = strchr(tptr, '/');

		if (fdash == NULL) {
			auth_policy_open_key(tptr, template);
			break;
		}
		if (tdash == NULL) {
			str_truncate(template, str_len(template)-1);
			auth_policy_close_key(fptr, template);
			str_append_c(template, ',');
			break;
		}
		if (auth_policy_strptrcmp(fptr, fdash, tptr, tdash) != 0) {
			str_truncate(template, str_len(template)-1);
			auth_policy_close_key(fptr, template);
			str_append_c(template, ',');
			auth_policy_open_key(tptr, template);
			break;
		}
		fptr = fdash+1;
		tptr = tdash+1;
	}
}

Project: core

void auth_policy_open_key(const char *key, string_t *template)
{
	const char *ptr;
	while((ptr = strchr(key, '/')) != NULL) {
		str_append_c(template,'"');
		json_append_escaped(template, t_strndup(key, (ptr-key)));
		str_append_c(template,'"');
		str_append_c(template,':');
		str_append_c(template,'{');
		key = ptr+1;
	}
}

Project: core

void auth_policy_finish(void *ctx)
{
	struct policy_lookup_ctx *context = ctx;

	if (context->parser != NULL) {
		const char *error ATTR_UNUSED;
		(void)json_parser_deinit(&(context->parser), &error);
	}
	if (context->http_request != NULL)
		http_client_request_abort(&(context->http_request));
	if (context->request != NULL)
		auth_request_unref(&context->request);
}

Project: core

void auth_policy_check(struct auth_request *request, const char *password,
	auth_policy_callback_t cb, void *context)
{
	if (request->master != NULL || *(request->set->policy_server_url) == '\0') {
		cb(0, context);
		return;
	}
	struct policy_lookup_ctx *ctx = p_new(request->pool, struct policy_lookup_ctx, 1);
	ctx->pool = request->pool;
	ctx->request = request;
	ctx->expect_result = TRUE;
	ctx->callback = cb;
	ctx->callback_context = context;
	ctx->set = request->set;

	auth_policy_url(ctx, "allow");
	ctx->result = (ctx->set->policy_reject_on_fail ? -1 : 0);
	auth_request_log_debug(request, "policy", "Policy request %s", ctx->url);
	T_BEGIN {
		auth_policy_create_json(ctx, password, FALSE);
	} T_END;
	auth_policy_send_request(ctx);
}

Project: core

void auth_policy_deinit(void)
{
	if (http_client != NULL)
		http_client_deinit(&http_client);
	i_free(auth_policy_json_template);
}

Project: core

void auth_policy_close_key(const char *key, string_t *template)
{
	while((key = strchr(key, '/')) != NULL) { str_append_c(template,'}'); key++; }
}