From c9877db3bba1831ac487b3f9f661b3782800b0f2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 04:56:33 +0000 Subject: [PATCH] feat(security): add blocked domains and strict TLD validation - Adds `BLOCKED_DOMAINS` list to reject disposable/invalid email domains. - Enforces TLD length >= 2 chars in `isValidEmail`. - Updates tests to cover new validation rules. Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ src/utils/security.test.ts | 28 +++++++++++++++++++++++----- src/utils/security.ts | 25 ++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 030d946..fa5439f 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -22,3 +22,8 @@ **Vulnerability:** Standard email validation often allows characters like quotes (`"`) or backticks (`` ` ``) which can be vectors for XSS in HTML attribute contexts. **Learning:** While RFCs allow quoted local parts (e.g., `"user"@example.com`), these are rare in practice and pose significant security risks in web applications. It is often safer to explicitly reject them. However, apostrophes (`'`) are common in names (O'Connor) and should be allowed, relying on sanitization (Defense in Depth) rather than validation to handle them safely. **Prevention:** Use a hybrid approach: Strict validation (reject quotes/backticks) for high-risk characters, coupled with output sanitization (`sanitizeInput`) for characters that must be allowed but are still risky (apostrophes). + +## 2026-02-14 - Blocking Disposable Domains +**Vulnerability:** Allowing users to register or submit forms with disposable email addresses (e.g., mailinator.com) can lead to spam, abuse, and polluted data. +**Learning:** While true email verification requires a backend or API, a simple client-side blocklist of common disposable domains is a highly effective, low-cost first line of defense. +**Prevention:** Maintain a list of known disposable domains (e.g., `BLOCKED_DOMAINS`) and check the domain part of the email address during validation. diff --git a/src/utils/security.test.ts b/src/utils/security.test.ts index d12c1c3..428c373 100644 --- a/src/utils/security.test.ts +++ b/src/utils/security.test.ts @@ -29,14 +29,14 @@ describe('Security Utils', () => { describe('isValidEmail', () => { it('accepts valid email addresses', () => { - expect(isValidEmail('test@example.com')).toBe(true); + expect(isValidEmail('test@valid-domain.com')).toBe(true); expect(isValidEmail('john.doe@sub.domain.co.uk')).toBe(true); - expect(isValidEmail('user+tag@example.com')).toBe(true); + expect(isValidEmail('user+tag@valid-domain.com')).toBe(true); }); it('rejects invalid email formats', () => { expect(isValidEmail('plainaddress')).toBe(false); - expect(isValidEmail('@example.com')).toBe(false); + expect(isValidEmail('@valid-domain.com')).toBe(false); expect(isValidEmail('user@')).toBe(false); expect(isValidEmail('user@.com')).toBe(false); expect(isValidEmail('user@com')).toBe(false); // Missing dot in domain part (simple regex might allow, but strict one requires dot) @@ -54,13 +54,31 @@ describe('Security Utils', () => { }); it('accepts emails with apostrophes', () => { - expect(isValidEmail("user'name@example.com")).toBe(true); - expect(isValidEmail("o'connor@example.com")).toBe(true); + expect(isValidEmail("user'name@valid-domain.com")).toBe(true); + expect(isValidEmail("o'connor@valid-domain.com")).toBe(true); }); it('rejects emails with whitespace', () => { expect(isValidEmail('user @example.com')).toBe(false); expect(isValidEmail('user@ example.com')).toBe(false); }); + + it('rejects short TLDs (less than 2 chars)', () => { + expect(isValidEmail('user@domain.c')).toBe(false); + expect(isValidEmail('user@domain.1')).toBe(false); + }); + + it('rejects blocked/disposable domains', () => { + expect(isValidEmail('user@example.com')).toBe(false); + expect(isValidEmail('test@test.com')).toBe(false); + expect(isValidEmail('spam@mailinator.com')).toBe(false); + expect(isValidEmail('bot@yopmail.com')).toBe(false); + expect(isValidEmail('temp@temp-mail.org')).toBe(false); + }); + + it('rejects blocked domains regardless of case', () => { + expect(isValidEmail('user@EXAMPLE.COM')).toBe(false); + expect(isValidEmail('user@MaIlInAtOr.CoM')).toBe(false); + }); }); }); diff --git a/src/utils/security.ts b/src/utils/security.ts index 049b1b4..ec68c79 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -17,9 +17,22 @@ export function sanitizeInput(input: string): string { .replace(/'/g, "'"); } +// Common disposable email providers and invalid domains +const BLOCKED_DOMAINS = new Set([ + "example.com", + "test.com", + "mailinator.com", + "yopmail.com", + "temp-mail.org", + "guerrillamail.com", + "10minutemail.com", + "trashmail.com", +]); + /** * Validates an email address format securely. * Rejects inputs containing dangerous characters like <, >, or whitespace. + * Also validates domain validity (blocked domains, TLD length). * * @param email - The email string to validate. * @returns True if the email is valid and safe, false otherwise. @@ -30,7 +43,13 @@ export function isValidEmail(email: string): boolean { // @ : Literal @ // [^\s@<>]+ : Domain part - no whitespace, @, <, or > // \. : Literal . - // [^\s@<>]+ : TLD part - no whitespace, @, <, or > - const emailRegex = /^[^\s@<>,"`]+@[^\s@<>,"`]+\.[^\s@<>,"`]+$/; - return emailRegex.test(email); + // [^\s@<>]+ : TLD part - no whitespace, @, <, or > (min 2 chars) + const emailRegex = /^[^\s@<>,"`]+@[^\s@<>,"`]+\.[^\s@<>,"`]{2,}$/; + if (!emailRegex.test(email)) { + return false; + } + + // Check against blocked domains + const domain = email.split("@")[1].toLowerCase(); + return !BLOCKED_DOMAINS.has(domain); }