Другое

Почему регулярное выражение PHP некорректно работает с длинными доменными именами

Узнайте причины катастрофического возврата в регулярных выражениях PHP при проверке доменных имен с длинными TLD и эффективные решения проблемы.

Почему регулярное выражение в PHP некорректно работает с доменными именами, содержащими 19 и более символов в домене верхнего уровня? Я написал регулярное выражение для проверки доменного имени: $reg = "#^(([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)*[a-zA-Z0-9]{2,}$#"; preg_match($reg, $domain, $res);. Регулярное выражение корректно работает для доменных имен с доменом верхнего уровня до 18 символов, но при добавлении 19-го символа возвращает false. Примеры: 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.1.rusreasreasreasreasssreasreasreasreasss.reasreasreasreasss (true), rusreasreasreasreasssreasreasreasreasss.reasreasreasreasss (true), reasreasreasreasss (true), но reasreasreasreassss (false). В чем причина такого поведения регулярного выражения и как это исправить?

Ваше регулярное выражение сталкивается с проблемой катастрофического возврата (catastrophic backtracking) при работе с длинными доменными именами. Это происходит из-за вложенных квантификаторов в вашем шаблоне, которые создают экспоненциальное количество возможных комбинаций для проверки.

Содержание

Анализ проблемы

Ваше регулярное выражение:

php
$reg = "#^(([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)*[a-zA-Z0-9]{2,}$#";

Имеет структуру, которая вызывает проблемы при обработке длинных строк:

  • Вложенные квантификаторы (([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)*
  • Каждый * означает “ноль или больше раз”
  • При обработке длинных доменов возникает комбинаторный взрыв

Причина некорректной работы

Как видно из документации PHP, PHP имеет ограничения на возврат (backtrack limit) и рекурсию для PCRE:

When you use preg_match() for security purpose or huge data processing, maybe you should make consideration for backtrack_limit and recursion_limit.

Ваше регулярное выражение сталкивается с проблемой:

  1. Катастрофический возврат - при анализе длинных доменов движок регулярных выражений пытается все возможные комбинации
  2. Превышение лимита - по умолчанию pcre.backtrack_limit равен 100000, и при обработке 19+ символьных доменов этот лимит превышается
  3. “Fail silently” - PHP возвращает false вместо исключения, когда достигает лимита

Ваш пример reasreasreasreassss (16 символов) возвращает false, потому что при обработке:

  • Движок пытается все возможные комбинации символов
  • Каждая позиция создает новые ветви для проверки
  • Комбинаторика растет экспоненциально

Решение проблемы

1. Упрощение регулярного выражения

Замените сложный шаблон на более эффективный:

php
$reg = "#^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$#";

Этот шаблон:

  • Использует некаптурные группы (?:...)
  • Ограничивает длину каждого поддомена 63 символами
  • Избегает вложенных квантификеров

2. Разделение на части

Разделите проверку на более простые шаги:

php
function validateDomain($domain) {
    // Проверка основной структуры
    if (!preg_match('/^[a-zA-Z0-9.-]+$/', $domain)) {
        return false;
    }
    
    // Проверка длинны домена
    if (strlen($domain) > 253) {
        return false;
    }
    
    // Проверка TLD отдельно
    $parts = explode('.', $domain);
    $tld = end($parts);
    
    if (strlen($tld) < 2 || strlen($tld) > 63) {
        return false;
    }
    
    return true;
}

3. Использование функций PHP

Для более надежной проверки используйте встроенные функции:

php
function isValidDomain($domain) {
    $domain = idn_to_ascii($domain);
    return filter_var($domain, FILTER_VALIDATE_DOMAIN) !== false;
}

Альтернативные подходы

1. Использование DNS-проверки

php
function domainExists($domain) {
    // Проверка существования домена через DNS
    return checkdnsrr($domain, 'A') || checkdnsrr($domain, 'MX');
}

2. Использование специализированных библиотек

Рассмотрите использование библиотеки:

php
// Использование Respect/Validation
use Respect\Validation\Validator as v;

$validator = v::domain()->noWhitespace();
$result = $validator->validate($domain);

3. Разбор URL с помощью PHP

php
function parseDomain($url) {
    $parsed = parse_url($url);
    if (isset($parsed['host'])) {
        return $parsed['host'];
    }
    return false;
}

Оптимизация производительности

1. Увеличение лимитов

Если вам необходимо работать с очень длинными доменами:

php
// Установка лимитов перед проверкой
ini_set('pcre.backtrack_limit', 1000000);
ini_set('pcre.recursion_limit', 1000000);

2. Кеширование результатов

php
$domainCache = [];

function validateDomainCached($domain) {
    global $domainCache;
    
    if (isset($domainCache[$domain])) {
        return $domainCache[$domain];
    }
    
    $result = isValidDomain($domain);
    $domainCache[$domain] = $result;
    
    return $result;
}

3. Использование более быстрых функций

Для простых проверок используйте strpos() или explode() вместо сложных регулярных выражений:

php
function hasValidTld($domain) {
    $parts = explode('.', $domain);
    $tld = end($parts);
    return strlen($tld) >= 2 && strlen($tld) <= 63;
}

Источники

  1. PHP: preg_match - Manual
  2. PHP backtrack limit explanation
  3. Catastrophic backtracking in regex
  4. Domain name validation best practices

Заключение

Проблема с вашим регулярным выражением заключается в катастрофическом возврате при обработке длинных доменных имен. Для решения проблемы:

  1. Упростите регулярное выражение, избегая вложенных квантификаторов
  2. Используйте встроенные функции PHP для валидации доменов
  3. Разделите сложную проверку на несколько простых шагов
  4. Рассмотрите специализированные библиотеки для доменной валидации
  5. При необходимости увеличьте лимиты PCRE, но помните о производительности

Оптимальным решением будет использование filter_var() с FILTER_VALIDATE_DOMAIN для надежной и производительной проверки доменных имен.

Авторы
Проверено модерацией
Модерация