fix: sonarqube denial of service issue
This commit is contained in:
+27
-2
@@ -10,9 +10,34 @@ export function formatPrice(cents: number): string {
|
|||||||
return `$${(cents / 100).toFixed(2)}`;
|
return `$${(cents / 100).toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
// RFC 5321 caps an address at 254 characters; reject anything longer up front so
|
||||||
|
// validation cost stays bounded regardless of what a client posts.
|
||||||
|
const MAX_EMAIL_LENGTH = 254;
|
||||||
|
|
||||||
|
// Both patterns are anchored single character classes with no overlapping
|
||||||
|
// alternatives, so they match in linear time. Splitting on '@' and '.' in code
|
||||||
|
// rather than in one combined pattern avoids the ambiguous (and backtracking)
|
||||||
|
// `[^\s@]+\.[^\s@]+` domain match.
|
||||||
|
const LOCAL_PART_RE = /^[^\s@]+$/;
|
||||||
|
const DOMAIN_LABEL_RE = /^[^\s@.]+$/;
|
||||||
|
|
||||||
export function isValidEmail(email: string): boolean {
|
export function isValidEmail(email: string): boolean {
|
||||||
return EMAIL_RE.test(email.trim());
|
const trimmed = email.trim();
|
||||||
|
if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = trimmed.indexOf('@');
|
||||||
|
if (at === -1 || at !== trimmed.lastIndexOf('@')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LOCAL_PART_RE.test(trimmed.slice(0, at))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = trimmed.slice(at + 1).split('.');
|
||||||
|
return labels.length >= 2 && labels.every((label) => DOMAIN_LABEL_RE.test(label));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MARKETING_CONSENT_TEXT =
|
export const MARKETING_CONSENT_TEXT =
|
||||||
|
|||||||
@@ -44,4 +44,31 @@ describe('isValidEmail', () => {
|
|||||||
it('rejects an email with no domain', () => {
|
it('rejects an email with no domain', () => {
|
||||||
expect(isValidEmail('thom@')).toBe(false);
|
expect(isValidEmail('thom@')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts a multi-label domain', () => {
|
||||||
|
expect(isValidEmail('thom@mail.example.co.uk')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a domain with no dot', () => {
|
||||||
|
expect(isValidEmail('thom@localhost')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an empty domain label', () => {
|
||||||
|
expect(isValidEmail('thom@example..com')).toBe(false);
|
||||||
|
expect(isValidEmail('thom@.com')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects more than one @', () => {
|
||||||
|
expect(isValidEmail('thom@foo@example.com')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an address longer than 254 characters', () => {
|
||||||
|
expect(isValidEmail(`${'a'.repeat(250)}@example.com`)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a long dotless domain without super-linear backtracking', () => {
|
||||||
|
const start = Date.now();
|
||||||
|
expect(isValidEmail(`thom@${'a'.repeat(60_000)}`)).toBe(false);
|
||||||
|
expect(Date.now() - start).toBeLessThan(1000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user