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)}`;
|
||||
}
|
||||
|
||||
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 {
|
||||
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 =
|
||||
|
||||
@@ -44,4 +44,31 @@ describe('isValidEmail', () => {
|
||||
it('rejects an email with no domain', () => {
|
||||
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