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

85

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

Описание злых регулярных выражений из Википедии :

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

С примерами, опять же из Википедии :

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

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

Майк Партридж
источник
7
Еще одна ссылка на эту тему: regular-expressions.info/catastrophic.html
Дэниел Хилгарт,
1
Вот инструмент для выполнения статического анализа регулярных выражений для обнаружения предполагаемых проблем ReDoS
Tripleee
Ссылка, предоставленная @tripleee, похоже, содержит неработающую ссылку на инструмент RXXR. Вот зеркало GitHub: github.com/ConradIrwin/rxxr2
Майк Хилл,
3
Кроме того, для тех, кому интересно, похоже, что авторы оригинального инструмента RXXR заменили его на RXXR2. Их новая страница размещена здесь и в настоящее время имеет рабочую ссылку на источник RXXR2
Майк Хилл,

Ответы:

77

Почему плохие регулярные выражения - проблема?

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

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

^((ab)*)+$

Учитывая ввод:

ababababababababababababab

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

Но затем мы бросаем гаечный ключ:

abababababababababababab а

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

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

Количество возможных комбинаций экспоненциально масштабируется с длиной ввода, и, прежде чем вы это узнаете, механизм регулярных выражений съедает все ваши системные ресурсы, пытаясь решить эту проблему, пока, исчерпав все возможные комбинации терминов, он, наконец, не сдается и сообщает «Нет совпадения». Тем временем ваш сервер превратился в горящую кучу расплавленного металла.

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

На самом деле это очень сложно. Я сам написал пару, хотя знаю, что это такое и как их избежать. Посмотрите, как Regex занимает удивительно много времени . Обертывание всего, что вы можете, в атомарной группе может помочь предотвратить проблему с возвратом. По сути, он говорит механизму регулярных выражений не пересматривать данное выражение - «заблокируйте все, что вы нашли с первой попытки». Обратите внимание, однако, что атомарные выражения не предотвращают возврат в пределах выражения, поэтому ^(?>((ab)*)+)$все еще опасно, но ^(?>(ab)*)+$безопасно (оно будет соответствовать(abababababababababababab) а затем откажется отдавать любой из совпадающих символов, тем самым предотвращая катастрофический возврат).

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


Интересно, что с тех пор, как этот ответ был впервые написан, команда Техасского университета в Остине опубликовала статью, описывающую разработку инструмента, способного выполнять статический анализ регулярных выражений с явной целью поиска этих «злых» шаблонов. Этот инструмент был разработан для анализа Java-программ, но я подозреваю, что в ближайшие годы мы увидим больше инструментов, разработанных для анализа и обнаружения проблемных шаблонов в JavaScript и других языках, особенно с учетом того, что частота атак ReDoS продолжает расти .

Статическое обнаружение уязвимостей DoS в программах, использующих регулярные выражения
Валентин Вюстхольц, Освальдо Оливо, Марин Дж. Хойле и Исил Диллиг
Техасский университет в Остине

JDB до сих пор помнит Монику
источник
Это очень хороший ответ для описания / почему / пример регулярного выражения занимает много времени, но я ищу несколько правил, которые можно усвоить, чтобы помочь распознать проблемное регулярное выражение.
Майк Партридж,
4
Знание «почему» - самый важный шаг к тому, чтобы избежать написания «злого» регулярного выражения. К сожалению, после написания очень сложно сразу или быстро найти проблемное регулярное выражение. Если вам нужно общее исправление, атомарная группировка обычно является лучшим способом, но это может оказать значительное влияние на шаблоны, которые будут соответствовать регулярному выражению. В конце концов, распознавание плохого регулярного выражения похоже на регулярное выражение любого другого плохого кода - для этого требуется много опыта, много времени и / или одно катастрофическое событие.
JDB все еще помнит Монику
Вот почему я предпочитаю механизмы регулярных выражений, которые не поддерживают обратный поиск без принуждения пользователя. IE lex / flex.
Спенсер Ратбун
@MikePartridge - это обычная проблема классической теории ИТ: решить, будет ли какой-то код зацикливаться бесконечно или останавливаться, является NP-полной проблемой. С регулярными выражениями вы, вероятно, можете угадать / поймать некоторые из них, выполнив поиск определенных шаблонов / правил, но если вы не проведете тяжелый NP-полный анализ, вы никогда не поймаете их все. Некоторые варианты: 1) никогда не позволять пользователю вводить регулярное выражение на ваш сервер. 2) сконфигурируйте механизм регулярных выражений, чтобы завершить расчет достаточно рано (но проверьте, что ваше действительное регулярное выражение в вашем коде все еще работает, даже с жесткими ограничениями). 3) запустите код регулярного выражения в потоке с низким приоритетом с ограничениями cpu / mem.
Ped7g 09
1
@MikePartridge - недавно наткнулся на статью о некоторых новых инструментах, которые разрабатываются для статического обнаружения проблемных регулярных выражений. Интересный материал ... Думаю, за ним стоит следить.
JDB все еще помнит Монику
13

То, что вы называете «злым» регулярным выражением, - это регулярное выражение, которое демонстрирует катастрофический возврат . На связанной странице (которую я написал) подробно объясняется концепция. По сути, катастрофический возврат с возвратом происходит, когда регулярное выражение не может соответствовать, и различные перестановки одного и того же регулярного выражения могут найти частичное совпадение. Затем механизм регулярных выражений пробует все эти перестановки. Если вы хотите просмотреть свой код и проверить свои регулярные выражения, это 3 ключевые проблемы, на которые следует обратить внимание:

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

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

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

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

Ян Гойвертс
источник
10

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

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

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

Подводя итог, TLDR-стиль:

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

Jarmund
источник
3
В настоящее время этот ответ ближе всего к тому, что я ищу: эмпирическое правило для распознавания регулярного выражения, которое может вызвать катастрофический возврат.
Майк Партридж,
1
Что вы упустили и что, кажется, является важной частью проблемы, - это захват групп.
Майк Партридж,
@MikePartridge Это тоже. Я попытался максимально упростить это, поэтому есть и другие вещи, которые могут вызывать то же самое, например, захват групп.
Jarmund
7

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

Например, в C # я могу создать регулярное выражение с 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 истечении заданного тайм- что не приведет к использованию ресурсов, что приведет к отказу в обслуживании.

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

Кейси
источник
7

Обнаружение злых регулярных выражений

  1. Попробуйте RegexStaticAnalysis Николааса Вайдемана проект .
  2. Попробуйте мой детектор vuln-regex в стиле ансамбля, в котором есть интерфейс командной строки для инструмента Weideman и других.

Эмпирические правила

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

Вот несколько форм двусмысленности. Не используйте их в своих регулярных выражениях.

  1. Вложенные квантификаторы, например (a+)+("высота звезды> 1"). Это может вызвать экспоненциальный взрыв. См. Подстакsafe-regex .
  2. Количественные перекрывающиеся дизъюнкции, такие как (a|a)+ . Это может вызвать экспоненциальный взрыв.
  3. Избегайте количественной оценки перекрывающихся смежностей, таких как \d+\d+. Это может вызвать полиномиальный взрыв.

Дополнительные ресурсы

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

Джеймс Дэвис
источник
4

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

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

Спенсер Рэтбун
источник
a*a*не использует обратные ссылки. Теперь движок регулярных выражений использует отслеживание с возвратом , что, возможно, вы имели в виду? В этом случае все современные движки используют откат. Вы можете легко отключить переход с возвратом (?>...), но это чаще всего не меняет значения вашего выражения (а в некоторых случаях его можно обойти).
JDB все еще помнит Монику
@ Cyborgx37 упс! Я действительно имел в виду возврат. Исправлена.
Спенсер Ратбун
В этом случае движок либо использует откат, либо нет. Фактически нет способа ограничить отслеживание с возвратом путем ограничения ввода.
JDB все еще помнит Монику
2
@JDB: «все современные движки используют возврат». - Может, в 2013 году так было, но сейчас нет .
Кевин
@Kevin - конечно. ты победил.
JDB все еще помнит Монику
3

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

Берги
источник
2
Я думаю, вы неправильно поняли вопрос. Когда я это читал, OP буквально спрашивает, как он может распознать злое регулярное выражение, а не как он может написать программу для этого. Например: «Я написал это регулярное выражение, но как я могу определить, может ли оно быть злом?»
ruakh
Вы могли быть правы. Тогда я могу только порекомендовать статью о катастрофическом обратном отслеживании, на которую @DanielHilgarth уже ссылался в комментариях.
Берги
2
@ 0x90: Потому что я не считаю, например, a*или \*уязвимыми.
ruakh
1
@ 0x90 a*вообще не уязвим. Между тем, a{0,1000}a{0,1000}катастрофическое регулярное выражение ждет своего часа. Даже a?a?при правильных условиях может иметь неприятные результаты.
JDB все еще помнит Монику
2
@ 0x90 - Катастрофический возврат с возвратом представляет опасность, когда у вас есть два выражения, одно из которых идентично другому или является его подмножеством, где длина выражения является переменной и где они расположены так, что можно передать один или несколько символов другое через возврат. Например, a*b*c*$безопасно, но a*b*[ac]*$опасно, потому что a*потенциально можно отказаться от символов, [ac]*если они bотсутствуют, и начальное совпадение не удается (например aaaaaaaaaaaccccccccccd).
JDB все еще помнит Монику
0

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

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

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

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

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

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

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

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

AJMansfield
источник
1
«быть похожим на», распознавать, чего «хочет», «пытаться» догадываться, «видеть» и делать выводы («осознавать», «определять») - это нетривиальные задачи, которые сложно реализовать алгоритмически для компьютеров ... И примеры тестирования - это не на что полагаться, вам скорее понадобится какое-то доказательство.
Берги
@Bergi Под тестовыми примерами я имел в виду то, что вы берете крошечный кусок полного регулярного выражения и запускаете его с тестовой строкой, как простой способ определить, как оно себя ведет. Конечно, вы тестируете только те фрагменты, которые вы изучили и уже знаете, что не делают странных вещей в тестовых примерах.
AJMansfield