diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 100f4aa..ee0622d 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -156,12 +156,15 @@ defmodule Mv.Membership.Member do if is_binary(q) and String.trim(q) != "" do q2 = String.trim(q) - pat = "%" <> q2 <> "%" + # Sanitize for LIKE patterns (escape % and _), limit length to 100 chars + q2_sanitized = sanitize_search_query(q2) + pat = "%" <> q2_sanitized <> "%" # Build search filters grouped by search type for maintainability # Priority: FTS > Substring > Custom Fields > Fuzzy Matching + # Note: FTS and fuzzy use q2 (unsanitized), LIKE-based filters use pat (sanitized) fts_match = build_fts_filter(q2) - substring_match = build_substring_filter(q2, pat) + substring_match = build_substring_filter(q2_sanitized, pat) custom_field_match = build_custom_field_filter(pat) fuzzy_match = build_fuzzy_filter(q2, threshold) @@ -505,6 +508,31 @@ defmodule Mv.Membership.Member do end end + # ============================================================================ + # Search Input Sanitization + # ============================================================================ + + # Sanitizes search input to prevent LIKE pattern injection. + # Escapes SQL LIKE wildcards (% and _) and limits query length. + # + # ## Examples + # + # iex> sanitize_search_query("test%injection") + # "test\\%injection" + # + # iex> sanitize_search_query("very_long_search") + # "very\\_long\\_search" + # + defp sanitize_search_query(query) when is_binary(query) do + query + |> String.slice(0, 100) + |> String.replace("\\", "\\\\") + |> String.replace("%", "\\%") + |> String.replace("_", "\\_") + end + + defp sanitize_search_query(_), do: "" + # ============================================================================ # Search Filter Builders # ============================================================================ @@ -590,11 +618,13 @@ defmodule Mv.Membership.Member do if has_search do # Search query provided: return email-match OR fuzzy-search candidates trimmed_search = String.trim(search_query) + # Sanitize for LIKE patterns (contains uses ILIKE internally) + sanitized_search = sanitize_search_query(trimmed_search) # Build search filters - excluding custom_field_filter for performance fts_match = build_fts_filter(trimmed_search) fuzzy_match = build_fuzzy_filter(trimmed_search, @default_similarity_threshold) - email_substring_match = expr(contains(email, ^trimmed_search)) + email_substring_match = expr(contains(email, ^sanitized_search)) query |> Ash.Query.filter(