From ce8c8e9a11e5e5684f060f9cb858aa71728ea568 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 8 Jul 2026 05:04:24 +0400 Subject: [PATCH] fix: sanitize control characters in team names (#14865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Team names created via the API could contain control characters (for example a trailing newline). Because the team-delete confirmation dialog requires you to retype the team name and matches it against the stored value, a hidden control character meant the typed name never matched — leaving the team impossible to delete from the UI. This sanitizes team names on save so they stay clean and deletable. #### How to reproduce 1. Create a team via `POST /api/v1/accounts/{account_id}/teams` with `{"name": "test\n"}`. 2. The team is created with the trailing newline stored in `name`. 3. In **Settings → Teams**, click delete and type the team name to confirm — the match fails, so the team cannot be deleted. #### What changed - `app/models/team.rb`: the existing `before_validation` now strips control characters and surrounding whitespace before downcasing the name. Names that reduce to blank (e.g. only newlines/tabs) are rejected loudly by the existing `presence` validation. - Fixing at the model layer covers the API and every other create/update path, rather than relying on the frontend confirm-dialog `.trim()` (which only handles leading/trailing whitespace, not internal control characters). Note: this prevents new malformed names. Any team already saved with a control character can be made deletable again simply by renaming it (an update re-runs the same sanitization). | Input | Stored as | Result | |---|---|---| | `"test\n"` | `"test"` | valid, deletable | | `"te\nst"` (internal) | `"test"` | valid | | `"\t\n "` (only control/ws) | — | rejected: "Name must not be blank" | | `"Customer Support"` | `"customer support"` | unchanged behavior | 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) --- app/models/team.rb | 2 +- spec/models/team_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/models/team.rb b/app/models/team.rb index 48990b488..15de8ab56 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -30,7 +30,7 @@ class Team < ApplicationRecord uniqueness: { scope: :account_id } before_validation do - self.name = name.downcase if attribute_present?('name') + self.name = name.gsub(/[[:cntrl:]]/, '').strip.downcase if attribute_present?('name') end # Adds multiple members to the team diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb index cb55dba61..8272b5925 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/team_spec.rb @@ -7,6 +7,31 @@ RSpec.describe Team do it { is_expected.to have_many(:team_members) } end + describe 'name normalization' do + let(:account) { create(:account) } + + it 'downcases the name' do + team = create(:team, account: account, name: 'Customer Support') + expect(team.name).to eq('customer support') + end + + it 'strips control characters and surrounding whitespace' do + team = create(:team, account: account, name: " Sales\n") + expect(team.name).to eq('sales') + end + + it 'removes control characters embedded within the name' do + team = create(:team, account: account, name: "su\npport") + expect(team.name).to eq('support') + end + + it 'is invalid when the name reduces to blank after sanitization' do + team = build(:team, account: account, name: "\t\n ") + expect(team).not_to be_valid + expect(team.errors[:name]).to include(I18n.t('errors.validations.presence')) + end + end + describe '#add_members' do let(:team) { FactoryBot.create(:team) }