fix: escape formula characters in contact CSV export (#15334)
## Description Contact export builds its CSV with the standard library `CSV`, which writes user-supplied values (name, email, phone, custom attributes) verbatim. When a cell begins with a formula character (`=`, `+`, `-`, `@`, and tab/CR), spreadsheet applications interpret it as a formula on open. This switches the export to `CSVSafe` (the `csv-safe` gem already used by the v2 report exports), which prefixes such fields so they are treated as text. No new dependency, no behavioural change beyond neutralising formula-leading cells. Note: phone numbers stored with a leading `+` are now prefixed with a single quote in the exported file, consistent with how the report exports already behave. Ref https://linear.app/chatwoot/issue/CW-7473 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? `bundle exec rspec spec/jobs/account/contacts_export_job_spec.rb` — added a case asserting a formula-leading contact value is neutralised on export; existing cases updated for the phone-number prefix. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
This commit is contained in:
@@ -20,7 +20,7 @@ class Account::ContactsExportJob < ApplicationJob
|
|||||||
contacts_to_export = contacts.to_a
|
contacts_to_export = contacts.to_a
|
||||||
preload_contact_labels(contacts_to_export) if headers.include?(LABELS_COLUMN)
|
preload_contact_labels(contacts_to_export) if headers.include?(LABELS_COLUMN)
|
||||||
|
|
||||||
csv_data = CSV.generate do |csv|
|
csv_data = CSVSafe.generate do |csv|
|
||||||
csv << headers
|
csv << headers
|
||||||
contacts_to_export.each do |contact|
|
contacts_to_export.each do |contact|
|
||||||
csv << headers.map { |header| value_for_header(contact, header) }
|
csv << headers.map { |header| value_for_header(contact, header) }
|
||||||
|
|||||||
@@ -81,7 +81,20 @@ RSpec.describe Account::ContactsExportJob do
|
|||||||
expect(csv_data.length).to eq(account.contacts.count)
|
expect(csv_data.length).to eq(account.contacts.count)
|
||||||
|
|
||||||
expect(emails).to include('test1@text.example', 'test2@text.example')
|
expect(emails).to include('test1@text.example', 'test2@text.example')
|
||||||
expect(phone_numbers).to include('+910808080818', '+910808080808')
|
# Phone numbers lead with "+" (a formula character), so they are prefixed with "'" on export.
|
||||||
|
expect(phone_numbers).to include("'+910808080818", "'+910808080808")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'neutralises formula-leading characters in exported values' do
|
||||||
|
create(:contact, account: account, name: '=HYPERLINK("http://evil.example")', email: 'formula@text.example')
|
||||||
|
|
||||||
|
described_class.perform_now(account.id, user.id, %w[id name email], {})
|
||||||
|
|
||||||
|
csv_content = account.contacts_export.download.force_encoding('UTF-8').delete_prefix("\xEF\xBB\xBF")
|
||||||
|
csv_data = CSV.parse(csv_content, headers: true)
|
||||||
|
row = csv_data.find { |r| r['email'] == 'formula@text.example' }
|
||||||
|
|
||||||
|
expect(row['name']).to start_with("'")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'exports labels when requested through column names' do
|
it 'exports labels when requested through column names' do
|
||||||
|
|||||||
Reference in New Issue
Block a user