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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user