fix: sonarqube denial of service issue
SonarQube Analysis / sonarqube (pull_request) Successful in 4m0s
Tests / backend-unit (pull_request) Successful in 48s
Tests / backend-integration (pull_request) Failing after 4s
Tests / frontend-e2e (pull_request) Failing after 5s

This commit is contained in:
2026-08-14 08:36:16 -05:00
parent 8ffd05f73d
commit dd0a647677
2 changed files with 54 additions and 2 deletions
+27 -2
View File
@@ -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 =
+27
View File
@@ -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);
});
});