Подтвердить что ты не робот

Как я могу распознать злое регулярное выражение?

Недавно я узнал об атаках "Регулярное выражение" Отказ в обслуживании " и решил исправить так называемые" злые "шаблоны регулярных выражений, где бы я мог их найти в моей кодовой базе - или, по крайней мере, те, которые используются при вводе пользователя. Примеры, приведенные в ссылке OWASP выше и wikipedia, полезны, но они не делают отличная работа по простому объяснению проблемы.

Описание злых регулярных выражений, wikipedia:

  • регулярное выражение применяет повторение ( "+", "*" ) к сложному подвыражению;
  • для повторного подвыражения, существует соответствие, которое также является суффиксом другого допустимого соответствия.

С примерами снова из wikipedia:

  • (a+)+
  • ([a-zA-Z]+)*
  • (a|aa)+
  • (a|a?)+
  • (.*a){x} для x > 10

Это проблема, которая просто не имеет более простого объяснения? Я ищу что-то, что облегчило бы избежать этой проблемы при написании регулярных выражений или найти их в существующей кодовой базе.

4b9b3361

Ответ 1

Почему возникают проблемы с злым регулярным выражением?

Потому что компьютеры делают именно то, что вы им говорите, даже если это не то, что вы имели в виду, или совершенно необоснованно. Если вы попросите механизм Regex доказать, что для некоторого заданного ввода существует или не соответствует данному шаблону, тогда движок будет пытаться сделать это независимо от того, сколько различных комбинаций должно быть проверено.

Вот простой шаблон, вдохновленный первым примером в сообщении OP:

^((ab)*)+$

С учетом ввода:

abababababababababababab

Механизм regex пытается найти что-то вроде (abababababababababababab), и совпадение найдено с первой попытки. Это как просить биолога доказать, что лошадей не существует. Биолог может быстро представить доказательства того, что они делают, проблема решена.

Но тогда мы бросаем ключ обезьяны в:

abababababababababababab a

Сначала программа попробует (abababababababababababab), но из-за этого добавляется a. Это похоже на то, чтобы попросить биолога доказать, что единорогов не существует, что намного сложнее. Биолог должен искать все возможные места, где может существовать единорог, и может только объявить, что они не существуют, если все местоположения оказались пустыми. Для нашего регулярного выражения это выглядит примерно так:

(abababababababababababab) - Nope
(ababababababababababab)(ab) - Nope
(abababababababababab)(abab) - Nope
(abababababababababab)(ab)(ab) - Nope
(ababababababababab)(ababab) - Nope
(ababababababababab)(abab)(ab) - Nope
(ababababababababab)(ab)(abab) - Nope
(ababababababababab)(ab)(ab)(ab) - Nope
(abababababababab)(abababab) - Nope
(abababababababab)(ababab)(ab) - Nope
(abababababababab)(abab)(abab) - Nope
(abababababababab)(abab)(ab)(ab) - Nope
(abababababababab)(ab)(ababab) - Nope
(abababababababab)(ab)(abab)(ab) - Nope
(abababababababab)(ab)(ab)(abab) - Nope
(abababababababab)(ab)(ab)(ab)(ab) - Nope
(ababababababab)(ababababab) - Nope
(ababababababab)(abababab)(ab) - Nope
(ababababababab)(ababab)(abab) - Nope
(ababababababab)(ababab)(ab)(ab) - Nope
(ababababababab)(abab)(abab)(ab) - Nope
(ababababababab)(abab)(ab)(abab) - Nope
(ababababababab)(abab)(ab)(ab)(ab) - Nope
(ababababababab)(ab)(abababab) - Nope
(ababababababab)(ab)(ababab)(ab) - Nope
(ababababababab)(ab)(abab)(abab) - Nope
(ababababababab)(ab)(abab)(ab)(ab) - Nope
(ababababababab)(ab)(ab)(ababab) - Nope
(ababababababab)(ab)(ab)(abab)(ab) - Nope
(ababababababab)(ab)(ab)(ab)(abab) - Nope
(ababababababab)(ab)(ab)(ab)(ab)(ab) - Nope
                              ...
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abababab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab)(ab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(abab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab)(ab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab) - Nope
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab) - Нет

Количество возможных комбинаций масштабируется по экспоненте с длиной ввода, и, прежде чем вы это знаете, механизм регулярных выражений поедает все ваши системные ресурсы, пытаясь решить эту проблему, пока, исчерпав все возможные комбинации терминов, наконец, отказывается и сообщает "Нет совпадения". Тем временем ваш сервер превратился в горящую кучу расплавленного металла. (Интересно, что в основном это работает с паролем brute-forcer, так как он находится в тот же класс проблем.)

Как определить злые регулярные выражения

Это на самом деле очень сложно. Я написал пару себя, хотя я знаю, что это такое, и вообще как их избежать. См. Regex, на удивление долгое время. Обтекание всего, что можно, в атомной группе, может помочь предотвратить проблему обратного отслеживания. Он в основном говорит движку регулярных выражений не пересматривать данное выражение - "блокировать все, что вы соответствовали при первой попытке". Обратите внимание, однако, что атомарные выражения не препятствуют возврату в выражении, поэтому (?>.*.*) по-прежнему опасен, но (?>.*)(?>.*) безопасен.

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

Ответ 2

Я бы подвел итог как "Повторение повторения". Первый пример, который вы указали, является хорошим, поскольку он указывает "письмо a, один или несколько раз подряд, это может повториться один или несколько раз подряд".

В этом случае искать комбинацию кванторов, таких как * и +.

Несколько более тонкая вещь, на которую нужно обратить внимание, - это третья и четвертая. Эти примеры содержат операцию ИЛИ, в которой обе стороны могут быть истинными. Это в сочетании с квантификатором выражения может привести к LOT возможных совпадений в зависимости от входной строки.

Подводя итог, TL;DR-стиль:

Будьте осторожны, как кванторы используются в сочетании с другими операторами.

Ответ 3

Я неожиданно наткнулся на ReDOS, несколько раз выполняя обзоры исходного кода. Одна вещь, которую я бы рекомендовал, - использовать таймаут с любым движком регулярного выражения, который вы используете.

Например, в С# я могу создать регулярное выражение с атрибутом TimeSpan.

string pattern = @"^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$";
Regex regexTags = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1.0));
try
{
    string noTags = regexTags.Replace(description, "");
    System.Console.WriteLine(noTags);
} 
catch (RegexMatchTimeoutException ex)
{
    System.Console.WriteLine("RegEx match timeout");
}

Это регулярное выражение уязвимо для отказа в обслуживании и без тайм-аута будет вращаться и потреблять ресурсы. С таймаутом он выдает RegexMatchTimeoutException после заданного таймаута и не приведет к тому, что использование ресурса приведет к условию отказа в обслуживании.

Вы хотите поэкспериментировать со значением таймаута, чтобы убедиться, что он работает для вашего использования.

Ответ 4

То, что вы называете "злым" регулярным выражением, представляет собой регулярное выражение, которое показывает катастрофическое обратное отслеживание. Связанная страница (которую я написал) подробно объясняет эту концепцию. В принципе, катастрофическое обратное отслеживание происходит, когда регулярное выражение не подходит, и разные перестановки одного и того же регулярного выражения могут найти частичное совпадение. Затем двигатель regex пытается выполнить все эти перестановки. Если вы хотите просмотреть свой код и проверить свои регулярные выражения, то это три ключевых вопроса, на которые нужно обратить внимание:

  • Альтернативы должны быть взаимоисключающими. Если несколько альтернатив могут совпадать с одним и тем же текстом, тогда двигатель попытается выполнить оба действия, если остальная часть регулярного выражения завершится с ошибкой. Если альтернативы находятся в группе, которая повторяется, у вас есть катастрофическое отступление. Классическим примером является (.|\s)*, чтобы соответствовать любому количеству любого текста, когда аромат регулярных выражений не имеет режима "Точечная совпадение строк". Если это часть более длинного регулярного выражения, тогда предметная строка с достаточно длинным пробелом (совпадающая как с ., так и с \s) приведет к разрыву регулярного выражения. Исправление состоит в том, чтобы использовать (.|\n)*, чтобы сделать альтернативы взаимоисключающими или даже более конкретными, какие символы действительно разрешены, например [\r\n\t\x20-\x7E] для ASCII -печатков, вкладок и разрывов строк.

  • Количественные токены, которые в последовательности должны либо быть взаимоисключающими друг с другом, либо быть взаимоисключающими, что происходит между ними. В противном случае оба могут совпадать с одним и тем же текстом, и все комбинации двух кванторов будут проверяться, если остальная часть регулярного выражения не будет соответствовать. Классическим примером является a.*?b.*?c, чтобы соответствовать 3 вещам с "чем-либо" между ними. Если c невозможно сопоставить, первый .*? будет расширять символ символом до конца строки или файла. Для каждого расширения второй .*? будет расширять символ по символу, чтобы соответствовать оставшейся части строки или файла. Исправление состоит в том, чтобы понять, что у вас не может быть "ничего" между ними. Первый запуск должен останавливаться на b, а второй запуск должен останавливаться на c. С одиночными символами a[^b]*+b[^c]*+c является простым решением. Поскольку теперь мы останавливаемся на разделителе, мы можем использовать притяжательные квантификаторы для дальнейшего повышения производительности.

  • Группа, содержащая токен с квантификатором, не должна иметь собственный квантификатор, если квантованный токен внутри группы не может быть сопоставлен только с чем-то другим, который является взаимоисключающим с ним. Это гарантирует, что нет никакого способа, чтобы меньшее количество итераций внешнего квантификатора с большим количеством итераций внутреннего квантора могло соответствовать тому же тексту, что и больше итераций внешнего квантификатора с меньшим количеством итераций внутреннего квантора. Это проблема, проиллюстрированная в ответе JDB.

Пока я писал свой ответ, я решил, что это заслужило полную статью на моем веб-сайте. Это тоже сейчас.

Ответ 5

Я не думаю, что вы можете распознавать такие регулярные выражения, по крайней мере, не все из них или нет, без ограничения их выразительности. Если вы действительно заботитесь о ReDoS, я бы попробовал их изолировать и убить их обработку таймаутом. Возможно также, что существуют реализации RegEx, которые позволяют ограничить их максимальную величину возврата.

Ответ 6

Я бы сказал, что это связано с используемым движком regex. Вы не всегда можете избежать этих типов регулярных выражений, но если ваш механизм регулярных выражений построен правильно, то это не проблема. См. эту серию блога для получения большой информации по теме двигателей регулярных выражений.

Обратите внимание на оговорку в нижней части статьи, в которой обратная трассировка является проблемой NP-Complete. В настоящее время нет возможности эффективно обрабатывать их, и вы можете запретить их при вводе.

Ответ 7

Есть несколько способов, я могу думать о том, что вы можете реализовать некоторые правила упрощения, выполнив их на небольших тестовых входах или проанализировав структуру регулярных выражений.

  • (a+)+ можно уменьшить, используя какое-то правило для замены избыточных операторов только на (a+)
  • ([a-zA-Z]+)* также может быть упрощено с нашим новым правилом объединения избыточности на ([a-zA-Z]*)

Компьютер может запускать тесты, запуская небольшие подвыражения регулярного выражения для случайных последовательностей соответствующих символов или последовательностей символов и видя, в каких группах они все входят. Для первого компьютера компьютер похож, эй, регулярное выражение хочет a, поэтому давайте попробуем его с помощью 6aaaxaaq. Затем он видит, что все а, и только первая группа, попадают в одну группу, и заключают, что независимо от того, сколько из них ставится, это не имеет значения, поскольку + получает все в группе. Второй, похоже, эй, регулярное выражение требует кучу букв, поэтому давайте попробуем его с помощью -fg0uj=, а затем он увидит, что снова каждый пучок находится в одной группе, поэтому он избавляется от + в конец.

Теперь нам нужно новое правило для обработки следующих: правило исключения-нерелевантных опций.

  • С помощью (a|aa)+ компьютер обращает на это внимание и, похоже, нам нравится этот большой второй, но мы можем использовать этот первый, чтобы заполнить больше пробелов, давайте получим aa many aa, как мы можем, и посмотрим, сможем ли мы получить что-нибудь еще после того, как мы закончим. Он может запускать его против другой тестовой строки, например `eaaa @a ~ aa. ' чтобы определить это.

  • Вы можете защитить себя от (a|a?)+, убедившись, что строки, соответствующие a?, не являются дроидами, которые мы ищем, потому что, поскольку они всегда могут соответствовать друг другу, мы решаем, t как вещи, как (a?)+, и выбросьте его.

  • Мы защищаем от (.*a){x}, понимая, что символы, сопоставленные a, уже были бы захвачены .*. Затем мы выбрасываем эту часть и используем другое правило для замены избыточных кванторов в (.*){x}.

В то время как реализация такой системы была бы очень сложной, это сложная проблема, и может потребоваться сложное решение. Вы также должны использовать методы, созданные другими людьми, например, только разрешая регулярному выражению ограниченное количество ресурсов выполнения, прежде чем убить его, если он не завершится.

Ответ 8

Мне известно о двух инструментах с открытым исходным кодом, которые пытаются ответить на этот вопрос.

rxxr2 - это академический проект. safe-refex - проект с открытым исходным кодом.

Ни один из этих инструментов не является точным, и каждый идентифицирует действительно уязвимые регулярные выражения, другие - нет. Я рассказываю об этом в своей недавней статье .