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>
This commit is contained in:
google-labs-jules[bot]
2026-01-30 04:56:33 +00:00
parent 15c4b88535
commit c9877db3bb
3 changed files with 50 additions and 8 deletions

View File

@@ -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.

View File

@@ -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);
});
});
});

View File

@@ -17,9 +17,22 @@ export function sanitizeInput(input: string): string {
.replace(/'/g, "&#039;");
}
// 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);
}