Compare commits

...
Author SHA1 Message Date
Vishnu Narayanan 7d6b62d2a1 fix: do not merge into an identified contact without a matching identifier
ContactIdentifyAction merges an incoming contact into an existing one that
matches by identifier, email, or phone_number. merge_contacts? returned true
whenever the request supplied no identifier, short-circuiting before the guard
that prevents merging contacts with mismatched identifiers. As a result a
request with no identifier could merge into (and overwrite) a contact that is
already identified, purely by matching its email or phone_number.

Drop the blank-identifier short-circuit so the identifier-mismatch guard runs in
that case too: a contact that owns an identifier is only merged into when the
request supplies the same identifier. Merges into non-identified contacts
(anonymous dedup) are unchanged.
2026-07-06 17:00:50 +05:30
2 changed files with 23 additions and 3 deletions
+5 -3
View File
@@ -72,9 +72,11 @@ class ContactIdentifyAction
def merge_contacts?(existing_contact, key) def merge_contacts?(existing_contact, key)
return if existing_contact.blank? return if existing_contact.blank?
return true if params[:identifier].blank? # Never merge into a contact that already owns an identifier unless the request
# supplies the matching one. This also covers the blank-identifier case: an
# we want to prevent merging contacts with different identifiers # unverified/anonymous request must not take over an identified contact just by
# matching its email or phone_number. Merges into non-identified contacts
# (anonymous dedup) are still allowed.
if existing_contact.identifier.present? && existing_contact.identifier != params[:identifier] if existing_contact.identifier.present? && existing_contact.identifier != params[:identifier]
# we will remove attribute from update list # we will remove attribute from update list
@attributes_to_update.delete(key) @attributes_to_update.delete(key)
@@ -107,6 +107,24 @@ describe ContactIdentifyAction do
end end
end end
context 'when a request without an identifier matches an already identified contact' do
it 'does not merge into the identified contact via a matching email' do
victim = create(:contact, account: account, identifier: 'victim_id', email: 'victim@test.com', name: 'Victim')
params = { email: 'victim@test.com', name: 'Attacker' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).not_to eq victim.id
expect(victim.reload.name).to eq 'Victim'
end
it 'does not merge into the identified contact via a matching phone_number' do
victim = create(:contact, account: account, identifier: 'victim_id', phone_number: '+919999888877', name: 'Victim')
params = { phone_number: '+919999888877', name: 'Attacker' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).not_to eq victim.id
expect(victim.reload.name).to eq 'Victim'
end
end
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
it 'updates the attributes of contact passed in to identify action' do it 'updates the attributes of contact passed in to identify action' do
create(:contact, account: account, identifier: '') create(:contact, account: account, identifier: '')