From 89af1017cf119626cfdbd0e643076a4262191692 Mon Sep 17 00:00:00 2001 From: leocagli Date: Tue, 23 Jun 2026 13:33:55 -0300 Subject: [PATCH] fix(validation): accept localhost/IP in validateUrl and tighten email regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateUrl rejected localhost and bare IPs because of an overly strict hostname.includes('.') check — legitimate dev/internal URLs were blocked. Removed that check; the URL constructor already enforces a non-empty hostname. validateEmail regex upgraded from /[^\s@]+@[^\s@]+\.[^\s@]+/ to /[^\s@]+@[^\s@.]+(\.[^\s@.]+)+/ to reject double dots in domain (e.g. user@example..com) and missing domain before the dot (user@.com) while avoiding catastrophic backtracking. Adds 8 new edge-case tests covering localhost, IPs, javascript:/data: scheme rejection, double-dot domain, subdomain email, and missing domain. Closes #180 Co-Authored-By: Claude Sonnet 4.6 --- src/shared/utils/validation.test.ts | 36 +++++++++++++++++++++++++---- src/shared/utils/validation.ts | 4 ++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/shared/utils/validation.test.ts b/src/shared/utils/validation.test.ts index 7fce25e..39fe021 100644 --- a/src/shared/utils/validation.test.ts +++ b/src/shared/utils/validation.test.ts @@ -88,10 +88,26 @@ describe('validateUrl', () => { expect(result).toContain('valid URL') }) - it('rejects hostname without dot', () => { - const result = validateUrl('https://localhost') - // localhost has no dot but is technically valid; our rule catches it - expect(result).toBe('URL must have a valid hostname') + it('accepts localhost (no dot required)', () => { + expect(validateUrl('https://localhost')).toBe(true) + }) + + it('accepts localhost with port', () => { + expect(validateUrl('http://localhost:3000')).toBe(true) + }) + + it('accepts IP address', () => { + expect(validateUrl('http://192.168.1.1')).toBe(true) + }) + + it('rejects javascript: scheme', () => { + const result = validateUrl('javascript:alert(1)') + expect(result).toContain('http') + }) + + it('rejects data: scheme', () => { + const result = validateUrl('data:text/html,

hi

') + expect(result).toContain('http') }) }) @@ -135,6 +151,18 @@ describe('validateEmail', () => { it('rejects trailing dot after TLD', () => { expect(validateEmail('user@example.')).toContain('valid email') }) + + it('rejects double dot in domain', () => { + expect(validateEmail('user@example..com')).toContain('valid email') + }) + + it('accepts subdomain email', () => { + expect(validateEmail('user@mail.example.co.uk')).toBe(true) + }) + + it('rejects missing domain before dot', () => { + expect(validateEmail('user@.com')).toContain('valid email') + }) }) describe('validateRequired', () => { diff --git a/src/shared/utils/validation.ts b/src/shared/utils/validation.ts index 0753f33..0819ab9 100644 --- a/src/shared/utils/validation.ts +++ b/src/shared/utils/validation.ts @@ -39,7 +39,7 @@ export function validateUrl(value: string): string | true { if (url.protocol !== 'http:' && url.protocol !== 'https:') { return 'URL must start with http:// or https://' } - if (!url.hostname.includes('.')) { + if (!url.hostname) { return 'URL must have a valid hostname' } return true @@ -58,7 +58,7 @@ export function validateUrl(value: string): string | true { export function validateEmail(value: string): string | true { const trimmed = value.trim() if (!trimmed) return true - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { + if (!/^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/.test(trimmed)) { return 'Please enter a valid email address' } return true