security: add input sanitization for search queries
All checks were successful
continuous-integration/drone/push Build is passing

- Escape SQL LIKE wildcards (% and _) to prevent pattern injection
- Limit search query length to 100 characters
- Apply sanitization in both :search action and linking filters
- FTS and fuzzy search use unsanitized query (wildcards not special there)
This commit is contained in:
Moritz 2025-12-11 13:45:45 +01:00
parent 1ec6188884
commit ca5fad0dcc
Signed by: moritz
GPG key ID: 1020A035E5DD0824

View file

@ -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(