From a0865a67fdcd2eee04c98add31f8f2b6f3b73e12 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 30 May 2026 22:40:48 +0200 Subject: [PATCH] fix(clients): reject spaces, '/', '\' and control chars in client email Client emails containing a slash broke the path-param routes (edit/delete/view returned 404 / "client not found"), leaving stale records that could only be cleared with manual SQLite edits. Validate the email on both the backend (Create + Update, which also covers the bulk paths) and the frontend (Zod) so these characters are rejected at save time with a clear, localized message across all 13 locales. Closes #4695 --- frontend/src/schemas/client.ts | 15 +++++++++- web/service/client.go | 15 ++++++++++ web/service/client_email_validation_test.go | 32 +++++++++++++++++++++ web/translation/ar-EG.json | 1 + web/translation/en-US.json | 1 + web/translation/es-ES.json | 1 + web/translation/fa-IR.json | 1 + web/translation/id-ID.json | 1 + web/translation/ja-JP.json | 1 + web/translation/pt-BR.json | 1 + web/translation/ru-RU.json | 1 + web/translation/tr-TR.json | 1 + web/translation/uk-UA.json | 1 + web/translation/vi-VN.json | 1 + web/translation/zh-CN.json | 1 + web/translation/zh-TW.json | 1 + 16 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 web/service/client_email_validation_test.go diff --git a/frontend/src/schemas/client.ts b/frontend/src/schemas/client.ts index 386f905b..e5c327ed 100644 --- a/frontend/src/schemas/client.ts +++ b/frontend/src/schemas/client.ts @@ -119,8 +119,21 @@ export const GroupSummarySchema = z.object({ export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []); +export function emailHasForbiddenChars(value: string): boolean { + if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true; + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + if (code < 0x20 || code === 0x7f) return true; + } + return false; +} + export const ClientFormSchema = z.object({ - email: z.string().trim().min(1, 'pages.clients.email'), + email: z + .string() + .trim() + .min(1, 'pages.clients.email') + .refine((v) => !emailHasForbiddenChars(v), 'pages.clients.emailInvalidChars'), subId: z.string(), uuid: z.string(), password: z.string(), diff --git a/web/service/client.go b/web/service/client.go index ffaacda7..b164d63e 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -408,6 +408,15 @@ type ClientCreatePayload struct { InboundIds []int `json:"inboundIds"` } +func validateClientEmail(email string) error { + for _, r := range email { + if r == '/' || r == '\\' || r == ' ' || r < 0x20 || r == 0x7f { + return common.NewError("client email contains an invalid character:", email) + } + } + return nil +} + func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreatePayload) (bool, error) { if payload == nil { return false, common.NewError("empty payload") @@ -416,6 +425,9 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate if strings.TrimSpace(client.Email) == "" { return false, common.NewError("client email is required") } + if err := validateClientEmail(client.Email); err != nil { + return false, err + } if len(payload.InboundIds) == 0 { return false, common.NewError("at least one inbound is required") } @@ -581,6 +593,9 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model if strings.TrimSpace(updated.Email) == "" { return false, common.NewError("client email is required") } + if err := validateClientEmail(updated.Email); err != nil { + return false, err + } if updated.SubID == "" { updated.SubID = existing.SubID } diff --git a/web/service/client_email_validation_test.go b/web/service/client_email_validation_test.go new file mode 100644 index 00000000..974404dd --- /dev/null +++ b/web/service/client_email_validation_test.go @@ -0,0 +1,32 @@ +package service + +import "testing" + +func TestValidateClientEmail(t *testing.T) { + valid := []string{ + "alice", + "alice@example.com", + "user-123_test.name", + "имя", + } + for _, email := range valid { + if err := validateClientEmail(email); err != nil { + t.Errorf("validateClientEmail(%q) = %v, want nil", email, err) + } + } + + invalid := []string{ + "i6dui/", + "a/b", + "client with spaces", + "back\\slash", + "tab\there", + "new\nline", + "\x7fdelete", + } + for _, email := range invalid { + if err := validateClientEmail(email); err == nil { + t.Errorf("validateClientEmail(%q) = nil, want error", email) + } + } +} diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index e8690b0a..aa1594d9 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -646,6 +646,7 @@ "subId": "معرّف الاشتراك", "online": "متصل", "email": "البريد", + "emailInvalidChars": "لا يمكن أن يحتوي البريد الإلكتروني على مسافات أو '/' أو '\\' أو أحرف تحكم", "group": "المجموعة", "groupDesc": "تسمية منطقية لتجميع العملاء (مثل فريق، عميل، منطقة). يمكن تصفيتها من شريط الأدوات.", "groupPlaceholder": "مثلاً customer-a", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 1176cad1..7f2a1c6b 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -646,6 +646,7 @@ "subId": "Subscription ID", "online": "Online", "email": "Email", + "emailInvalidChars": "Email cannot contain spaces, '/', '\\', or control characters", "group": "Group", "groupDesc": "Logical label used to bucket related clients (e.g. team, customer, region). Filterable from the toolbar.", "groupPlaceholder": "e.g. customer-a", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index b093c4d3..141a30be 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -646,6 +646,7 @@ "subId": "ID de suscripción", "online": "En línea", "email": "Email", + "emailInvalidChars": "El correo no puede contener espacios, '/', '\\' ni caracteres de control", "group": "Grupo", "groupDesc": "Etiqueta lógica para agrupar clientes relacionados (p. ej. equipo, cliente, región). Filtrable desde la barra de herramientas.", "groupPlaceholder": "p. ej. customer-a", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index 9aaec2d2..a42d010d 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -646,6 +646,7 @@ "subId": "شناسه اشتراک", "online": "آنلاین", "email": "ایمیل", + "emailInvalidChars": "ایمیل نمی‌تواند شامل فاصله، '/'، '\\' یا کاراکترهای کنترلی باشد", "group": "گروه", "groupDesc": "برچسبی منطقی برای دسته‌بندی کاربران مرتبط (مثل تیم، مشتری، منطقه). از نوار ابزار قابل فیلتر است.", "groupPlaceholder": "مثلاً customer-a", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 712084b3..83547827 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -646,6 +646,7 @@ "subId": "ID Langganan", "online": "Online", "email": "Email", + "emailInvalidChars": "Email tidak boleh mengandung spasi, '/', '\\', atau karakter kontrol", "group": "Grup", "groupDesc": "Label logis untuk mengelompokkan klien terkait (mis. tim, pelanggan, wilayah). Dapat difilter dari toolbar.", "groupPlaceholder": "mis. customer-a", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 8af3d94a..efe33679 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -646,6 +646,7 @@ "subId": "サブスクリプション ID", "online": "オンライン", "email": "メール", + "emailInvalidChars": "メールアドレスにスペース、'/'、'\\'、または制御文字を含めることはできません", "group": "グループ", "groupDesc": "関連クライアントをまとめる論理ラベル(チーム、顧客、地域など)。ツールバーからフィルタ可能。", "groupPlaceholder": "例: customer-a", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 945cfd07..6f1a997b 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -646,6 +646,7 @@ "subId": "ID da assinatura", "online": "Online", "email": "Email", + "emailInvalidChars": "O e-mail não pode conter espaços, '/', '\\' ou caracteres de controle", "group": "Grupo", "groupDesc": "Rótulo lógico para agrupar clientes relacionados (ex.: equipe, cliente, região). Filtrável pela barra de ferramentas.", "groupPlaceholder": "ex.: customer-a", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index 4f5322fe..996982a8 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -646,6 +646,7 @@ "subId": "ID подписки", "online": "В сети", "email": "Email", + "emailInvalidChars": "Email не может содержать пробелы, '/', '\\' или управляющие символы", "group": "Группа", "groupDesc": "Логическая метка для группировки связанных клиентов (например, команда, клиент, регион). Фильтруется из панели инструментов.", "groupPlaceholder": "например, customer-a", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 0c42711e..da212a7d 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -646,6 +646,7 @@ "subId": "Abonelik ID'si", "online": "Çevrimiçi", "email": "Email", + "emailInvalidChars": "E-posta boşluk, '/', '\\' veya kontrol karakterleri içeremez", "group": "Grup", "groupDesc": "İlgili istemcileri gruplamak için mantıksal etiket (ekip, müşteri, bölge). Araç çubuğundan filtrelenebilir.", "groupPlaceholder": "örn. customer-a", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 70ebe23a..654a7d0b 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -646,6 +646,7 @@ "subId": "ID підписки", "online": "У мережі", "email": "Email", + "emailInvalidChars": "Email не може містити пробіли, '/', '\\' або керуючі символи", "group": "Група", "groupDesc": "Логічна мітка для групування пов'язаних клієнтів (напр. команда, клієнт, регіон). Фільтрується з панелі інструментів.", "groupPlaceholder": "напр. customer-a", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index cbae8691..f240e46b 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -646,6 +646,7 @@ "subId": "ID đăng ký", "online": "Trực tuyến", "email": "Email", + "emailInvalidChars": "Email không được chứa khoảng trắng, '/', '\\' hoặc ký tự điều khiển", "group": "Nhóm", "groupDesc": "Nhãn logic để gom các client liên quan (nhóm, khách hàng, khu vực). Có thể lọc từ thanh công cụ.", "groupPlaceholder": "ví dụ customer-a", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 1e609318..9831e805 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -646,6 +646,7 @@ "subId": "订阅 ID", "online": "在线", "email": "邮箱", + "emailInvalidChars": "邮箱不能包含空格、'/'、'\\' 或控制字符", "group": "分组", "groupDesc": "用于对相关客户端进行分桶的逻辑标签(如团队、客户、地区)。可从工具栏筛选。", "groupPlaceholder": "如 customer-a", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 297d8dfc..d6e10e5a 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -646,6 +646,7 @@ "subId": "訂閱 ID", "online": "上線", "email": "電子郵件", + "emailInvalidChars": "電子郵件不能包含空格、'/'、'\\' 或控制字元", "group": "群組", "groupDesc": "用於將相關客戶端歸類的邏輯標籤(如團隊、客戶、地區)。可從工具列篩選。", "groupPlaceholder": "如 customer-a",