Commit 04aa482212 for asterisk.org

commit 04aa482212e43b292817444eb4de35499f4b1568
Author: Milan Kyselica <mil.kyselica@gmail.com>
Date:   Mon Mar 23 15:20:27 2026 +0100

    res_config_ldap: Escape LDAP filter values per RFC 4515

    The LDAP realtime driver constructs search filters by directly
    concatenating user-supplied values without RFC 4515 escaping.
    When LDAP is used as a realtime backend for endpoint
    identification, characters with special meaning in LDAP filters
    (*, (, ), \) can be injected via the SIP From header username.

    Add ldap_filter_escape_value() that escapes RFC 4515 special
    characters to their \HH hex representation, and apply it to
    non-LIKE query values. The LIKE query path preserves the existing
    wildcard conversion behavior with a note for maintainers.

    Resolves: #GHSA-r6c2-hwc2-j4mp

diff --git a/res/res_config_ldap.c b/res/res_config_ldap.c
index 295a5cee63..730727fb34 100644
--- a/res/res_config_ldap.c
+++ b/res/res_config_ldap.c
@@ -733,6 +733,40 @@ static int replace_string_in_string(char *string, const char *search, const char
 	return replaced;
 }

+/*!
+ * \internal
+ * \brief Escape a value for safe inclusion in an LDAP filter per RFC 4515.
+ *
+ * Characters that have special meaning in LDAP filters are escaped
+ * to their \\HH hex representation: * ( ) \\ and NUL.
+ *
+ * \param value The raw value to escape
+ * \param escaped Output buffer (caller-allocated ast_str)
+ */
+static void ldap_filter_escape_value(const char *value, struct ast_str **escaped)
+{
+	ast_str_reset(*escaped);
+	for (; *value; value++) {
+		switch (*value) {
+		case '*':
+			ast_str_append(escaped, 0, "\\2a");
+			break;
+		case '(':
+			ast_str_append(escaped, 0, "\\28");
+			break;
+		case ')':
+			ast_str_append(escaped, 0, "\\29");
+			break;
+		case '\\':
+			ast_str_append(escaped, 0, "\\5c");
+			break;
+		default:
+			ast_str_append(escaped, 0, "%c", *value);
+			break;
+		}
+	}
+}
+
 /*! \brief Append a name=value filter string. The filter string can grow.
  */
 static void append_var_and_value_to_filter(struct ast_str **filter,
@@ -742,9 +776,15 @@ static void append_var_and_value_to_filter(struct ast_str **filter,
 	char *new_name = NULL;
 	char *new_value = NULL;
 	const char *like_pos = strstr(name, " LIKE");
+	struct ast_str *escaped_value;

 	ast_debug(2, "name='%s' value='%s'\n", name, value);

+	escaped_value = ast_str_create(256);
+	if (!escaped_value) {
+		return;
+	}
+
 	if (like_pos) {
 		int len = like_pos - name;

@@ -753,11 +793,20 @@ static void append_var_and_value_to_filter(struct ast_str **filter,
 		value = new_value = ast_strdupa(value);
 		replace_string_in_string(new_value, "\\_", "_");
 		replace_string_in_string(new_value, "%", "*");
+		name = convert_attribute_name_to_ldap(table_config, name);
+		/* Note: The LIKE path preserves original wildcard behavior.
+		 * A more comprehensive escaping of the LIKE path is left
+		 * to the maintainers familiar with the query semantics.
+		 */
+		ast_str_append(filter, 0, "(%s=%s)", name, value);
+	} else {
+		name = convert_attribute_name_to_ldap(table_config, name);
+		ldap_filter_escape_value(value, &escaped_value);
+		ast_str_append(filter, 0, "(%s=%s)", name,
+			ast_str_buffer(escaped_value));
 	}

-	name = convert_attribute_name_to_ldap(table_config, name);
-
-	ast_str_append(filter, 0, "(%s=%s)", name, value);
+	ast_free(escaped_value);
 }

 /*!