From b5b19d549f4b0aab6f54d87230c91b8518d1165b Mon Sep 17 00:00:00 2001 From: shaggy Date: Thu, 27 Aug 2026 23:36:09 +0530 Subject: [PATCH] Avoid catastrophic backtracking in the RFC3966 domainname check The domainlabel pattern [alnum]+((\-)*[alnum])* is ambiguous, so a failing phone-context such as "aa.aa.aa...aa!" forces backtracking regex engines to explore exponentially many equivalent splits. The JavaScript build matches this pattern with the engine's backtracking matcher and hangs for over two minutes on a ~90-character input; the Python port (which mirrors this pattern) takes ~50s on 98 characters. Java's own matcher prunes this shape quickly, so the JVM library is not practically affected, but the pattern is the source for the JS port. Rewrite the label as [alnum]+(\-+[alnum]+)*, which matches the same language with exactly one way to match per position. --- .../i18n/phonenumbers/PhoneNumberUtil.java | 4 +-- .../phonenumbers/PhoneNumberUtilTest.java | 36 +++++++++++++++++++ .../i18n/phonenumbers/phonenumberutil.js | 8 ++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java index a1d83b3070..30dfd9c74a 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java @@ -328,9 +328,9 @@ public class PhoneNumberUtil { // defined in RFC3966. private static final String ALPHANUM = VALID_ALPHA + DIGITS; private static final String RFC3966_DOMAINLABEL = - "[" + ALPHANUM + "]+((\\-)*[" + ALPHANUM + "])*"; + "[" + ALPHANUM + "]+(\\-+[" + ALPHANUM + "]+)*"; private static final String RFC3966_TOPLABEL = - "[" + VALID_ALPHA + "]+((\\-)*[" + ALPHANUM + "])*"; + "[" + VALID_ALPHA + "]+(\\-+[" + ALPHANUM + "]+)*"; private static final String RFC3966_DOMAINNAME = "^(" + RFC3966_DOMAINLABEL + "\\.)*" + RFC3966_TOPLABEL + "\\.?$"; static final Pattern RFC3966_DOMAINNAME_PATTERN = Pattern.compile(RFC3966_DOMAINNAME); diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java index 4da210ec1d..7b0f672add 100644 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import org.junit.Assert; import org.junit.function.ThrowingRunnable; import org.mockito.Mockito; @@ -2221,6 +2222,41 @@ public void testParseMaliciousInput() throws Exception { } } + public void testParseRFC3966PhoneContextDomainname() throws Exception { + // Domain phone-contexts keep parsing, including labels with single or repeated hyphens. + assertEquals(US_LOCAL_NUMBER, + phoneUtil.parse("tel:253-0000;phone-context=www.google.com", RegionCode.US)); + assertEquals(US_LOCAL_NUMBER, + phoneUtil.parse("tel:253-0000;phone-context=ex--ample.domain.com", RegionCode.US)); + + // A long ambiguous domain is rejected. The old label pattern ([alnum]+((\-)*[alnum])*) made + // backtracking engines in the JS and Python ports of this pattern spend minutes on this + // input; Java's own matcher prunes it, this pins the rejection and the accepted language. + StringBuilder evilContext = new StringBuilder(100); + for (int i = 0; i < 25; i++) { + evilContext.append("aa."); + } + evilContext.append("aa!"); + try { + phoneUtil.parse("tel:253-0000;phone-context=" + evilContext, RegionCode.US); + fail("This should not parse without throwing an exception."); + } catch (NumberParseException e) { + // Expected this exception. + } + + // The rewritten label matches alnum runs separated by one or more hyphens, but not labels + // with leading/trailing hyphens or empty labels. + Pattern domainname = PhoneNumberUtil.RFC3966_DOMAINNAME_PATTERN; + assertTrue(domainname.matcher("a.b.c").matches()); + assertTrue(domainname.matcher("ab-cd.ef").matches()); + assertTrue(domainname.matcher("a--b.c").matches()); + assertTrue(domainname.matcher("example.com.").matches()); + assertFalse(domainname.matcher("aa.bb-").matches()); + assertFalse(domainname.matcher("aa..bb").matches()); + assertFalse(domainname.matcher("-aa.bb").matches()); + assertFalse(domainname.matcher("a-.b").matches()); + } + public void testParseWithInternationalPrefixes() throws Exception { assertEquals(US_NUMBER, phoneUtil.parse("+1 (650) 253-0000", RegionCode.NZ)); assertEquals(INTERNATIONAL_TOLL_FREE, phoneUtil.parse("011 800 1234 5678", RegionCode.US)); diff --git a/javascript/i18n/phonenumbers/phonenumberutil.js b/javascript/i18n/phonenumbers/phonenumberutil.js index db90566a47..7cf61a9e18 100644 --- a/javascript/i18n/phonenumbers/phonenumberutil.js +++ b/javascript/i18n/phonenumbers/phonenumberutil.js @@ -784,8 +784,8 @@ i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ = * @private */ i18n.phonenumbers.PhoneNumberUtil.RFC3966_DOMAINLABEL_ = '[' - + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + ']+((\\-)*[' - + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + '])*'; + + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + ']+(\\-+[' + + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + ']+)*'; /** * @const @@ -793,8 +793,8 @@ i18n.phonenumbers.PhoneNumberUtil.RFC3966_DOMAINLABEL_ = '[' * @private */ i18n.phonenumbers.PhoneNumberUtil.RFC3966_TOPLABEL_ = '[' - + i18n.phonenumbers.PhoneNumberUtil.VALID_ALPHA_ + ']+((\\-)*[' - + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + '])*'; + + i18n.phonenumbers.PhoneNumberUtil.VALID_ALPHA_ + ']+(\\-+[' + + i18n.phonenumbers.PhoneNumberUtil.ALPHANUM_ + ']+)*'; /** * @const