fix: reject malformed filter payloads (#15398)

Contact and conversation filters now reject malformed numeric/date
custom-attribute values and dangling query operators during validation,
returning the existing invalid-filter response instead of failing later
while building SQL.

## Closes

-
[CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors)
- [Sentry 7663920842](https://chatwoot-p3.sentry.io/issues/7663920842/)
- [Sentry 7663378104](https://chatwoot-p3.sentry.io/issues/7663378104/)

## How to reproduce

Submit a custom numeric attribute filter with an empty value, or end a
filter payload with a nonblank `query_operator`. These inputs previously
reached coercion/query construction and raised production exceptions.

## What changed

- Validate numeric and date custom-attribute values before query
construction.
- Preserve presence operators, which do not require a comparison value.
- Reject a query operator on the final filter condition.
- Add contact and conversation service regression coverage.
This commit is contained in:
Sony Mathew
2026-08-13 17:04:11 +05:30
committed by GitHub
parent 8a778f4f5b
commit dd203438d3
4 changed files with 101 additions and 2 deletions

View File

@@ -161,7 +161,10 @@ class FilterService
when 'date'
Date.iso8601(raw_value.to_s)
when 'numeric'
BigDecimal(raw_value.to_s)
decimal = BigDecimal(raw_value.to_s)
raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: attribute_key) unless decimal.finite?
decimal
else
raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: attribute_key)
end
@@ -195,8 +198,11 @@ class FilterService
end
def validate_query_operator
@params[:payload].each do |query_hash|
@params[:payload].each_with_index do |query_hash, index|
validate_single_condition(query_hash)
next unless index == @params[:payload].length - 1
raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) if query_hash['query_operator'].present?
end
end
end

View File

@@ -20,6 +20,7 @@ module Filters::CustomAttributeFilterHelper
end
def build_custom_attr_query(query_hash, current_index)
validate_custom_attribute_values!(query_hash)
filter_operator_value = filter_operation(query_hash, current_index)
query_operator = query_hash[:query_operator]
table_name = attribute_model == 'conversation_attribute' ? 'conversations' : 'contacts'
@@ -37,6 +38,16 @@ module Filters::CustomAttributeFilterHelper
query + not_in_custom_attr_query(table_name, query_hash, attribute_data_type)
end
def validate_custom_attribute_values!(query_hash)
return unless @attribute_data_type.in?(%w[date numeric])
return if query_hash[:filter_operator].in?(%w[is_present is_not_present])
return if @attribute_data_type == 'date' && query_hash[:filter_operator] == 'days_before'
Array(query_hash[:values]).each do |value|
coerce_lt_gt_value(value, @attribute_data_type, @attribute_key)
end
end
def custom_attribute(attribute_key, account, custom_attribute_type)
current_account = account || Current.account
attribute_model = custom_attribute_type.presence || self.class::ATTRIBUTE_MODEL