Недавно я узнал об атаках типа «отказ в обслуживании» с помощью регулярных выражений и решил искоренить так называемые «злые» шаблоны регулярных выражений везде, где я мог бы найти их в своей кодовой базе - или, по крайней мере, те, которые используются при вводе пользователем. Примеры, приведенные по ссылке выше на OWASP и в википедии , полезны, но они не очень хорошо объясняют проблему простым языком.
Описание злых регулярных выражений из Википедии :
- регулярное выражение применяет повторение («+», «*») к сложному подвыражению;
- для повторяющегося подвыражения существует совпадение, которое также является суффиксом другого действительного совпадения.
С примерами, опять же из Википедии :
(a+)+
([a-zA-Z]+)*
(a|aa)+
(a|a?)+
(.*a){x}
для x> 10
Это проблема, которой нет более простого объяснения? Я ищу что-то, что помогло бы избежать этой проблемы при написании регулярных выражений или найти их в существующей кодовой базе.
Ответы:
Почему плохие регулярные выражения - проблема?
Потому что компьютеры делают именно то, что вы им говорите, даже если это не то, что вы имели в виду или это совершенно необоснованно. Если вы попросите движок Regex доказать, что для некоторого заданного ввода существует или нет совпадение с заданным шаблоном, то движок попытается сделать это независимо от того, сколько различных комбинаций необходимо проверить.
Вот простой шаблон, вдохновленный первым примером в сообщении OP:
^((ab)*)+$
Учитывая ввод:
Механизм регулярных выражений пытается что-то вроде,
(abababababababababababab)
и совпадение обнаруживается с первой попытки.Но затем мы бросаем гаечный ключ:
Двигатель сначала попытается,
(abababababababababababab)
но из-за этого ничего не получитсяa
. Это приводит к катастрофическому разрыву скобок, потому что наш шаблон(ab)*
, демонстрируя добросовестность, выпустит один из своих захватов (он будет "возвращаться") и позволит внешнему шаблону повторить попытку. Для нашего движка регулярных выражений это выглядит примерно так:Количество возможных комбинаций экспоненциально масштабируется с длиной ввода, и, прежде чем вы это узнаете, механизм регулярных выражений съедает все ваши системные ресурсы, пытаясь решить эту проблему, пока, исчерпав все возможные комбинации терминов, он, наконец, не сдается и сообщает «Нет совпадения». Тем временем ваш сервер превратился в горящую кучу расплавленного металла.
Как распознать злые регулярные выражения
На самом деле это очень сложно. Я сам написал пару, хотя знаю, что это такое и как их избежать. Посмотрите, как Regex занимает удивительно много времени . Обертывание всего, что вы можете, в атомарной группе может помочь предотвратить проблему с возвратом. По сути, он говорит механизму регулярных выражений не пересматривать данное выражение - «заблокируйте все, что вы нашли с первой попытки». Обратите внимание, однако, что атомарные выражения не предотвращают возврат в пределах выражения, поэтому
^(?>((ab)*)+)$
все еще опасно, но^(?>(ab)*)+$
безопасно (оно будет соответствовать(abababababababababababab)
а затем откажется отдавать любой из совпадающих символов, тем самым предотвращая катастрофический возврат).К сожалению, после написания очень сложно сразу или быстро найти проблемное регулярное выражение. В конце концов, распознавание плохого регулярного выражения похоже на распознавание любого другого плохого кода - это требует много времени и опыта и / или одного катастрофического события.
Интересно, что с тех пор, как этот ответ был впервые написан, команда Техасского университета в Остине опубликовала статью, описывающую разработку инструмента, способного выполнять статический анализ регулярных выражений с явной целью поиска этих «злых» шаблонов. Этот инструмент был разработан для анализа Java-программ, но я подозреваю, что в ближайшие годы мы увидим больше инструментов, разработанных для анализа и обнаружения проблемных шаблонов в JavaScript и других языках, особенно с учетом того, что частота атак ReDoS продолжает расти .
источник
То, что вы называете «злым» регулярным выражением, - это регулярное выражение, которое демонстрирует катастрофический возврат . На связанной странице (которую я написал) подробно объясняется концепция. По сути, катастрофический возврат с возвратом происходит, когда регулярное выражение не может соответствовать, и различные перестановки одного и того же регулярного выражения могут найти частичное совпадение. Затем механизм регулярных выражений пробует все эти перестановки. Если вы хотите просмотреть свой код и проверить свои регулярные выражения, это 3 ключевые проблемы, на которые следует обратить внимание:
Альтернативы должны быть взаимоисключающими. Если несколько альтернатив могут соответствовать одному и тому же тексту, то движок попробует оба, если оставшаяся часть регулярного выражения не удалась. Если альтернативы находятся в повторяющейся группе, у вас катастрофический возврат. Классический пример -
(.|\s)*
сопоставление любого количества любого текста, когда в разновидности регулярного выражения нет режима «точка соответствует разрывам строк». Если это часть более длинного регулярного выражения, тогда строка темы с достаточно длинным пробелом (соответствует обоим.
и\s
) нарушит регулярное выражение. Исправление заключается в том,(.|\n)*
чтобы сделать альтернативы взаимоисключающими или даже более конкретными в отношении того, какие символы действительно разрешены, например,[\r\n\t\x20-\x7E]
для печатных таблиц ASCII, табуляции и разрывов строк.Последовательные количественные токены должны быть либо взаимоисключающими друг с другом, либо взаимоисключающими между собой. В противном случае оба могут соответствовать одному и тому же тексту, и все комбинации двух квантификаторов будут проверены, когда оставшаяся часть регулярного выражения не найдет совпадения. Классический пример -
a.*?b.*?c
сопоставить 3 объекта с «чем угодно» между ними. Еслиc
не удается сопоставить, первый.*?
будет расширяться символ за символом до конца строки или файла. Для каждого расширения второе.*?
будет расширять символ за символом, чтобы соответствовать оставшейся части строки или файла. Чтобы исправить это, нужно понять, что между ними не может быть ничего. Первый запуск должен остановиться в,b
а второй запуск должен остановиться вc
. С одиночными персонажамиa[^b]*+b[^c]*+c
это простое решение. Поскольку теперь мы остановились на разделителе, мы можем использовать притяжательные квантификаторы для дальнейшего повышения производительности.Группа, содержащая токен с квантификатором, не должна иметь собственного квантификатора, если только квантифицированный токен внутри группы не может быть сопоставлен только с чем-то еще, что является взаимоисключающим с ним. Это гарантирует, что меньшее количество итераций внешнего квантификатора с большим количеством итераций внутреннего квантификатора не может соответствовать тому же тексту, что и большее количество итераций внешнего квантификатора с меньшим количеством итераций внутреннего квантификатора. Это проблема, проиллюстрированная в ответе JDB.
Пока я писал свой ответ, я решил, что это заслуживает полной статьи на моем сайте . Это сейчас тоже онлайн.
источник
Я бы резюмировал это как «Повторение повторения». Первый пример, который вы указали, является хорошим, поскольку в нем говорится «буква a, один или несколько раз подряд. Это может снова произойти один или несколько раз подряд».
В этом случае следует обратить внимание на комбинацию квантификаторов, таких как * и +.
Несколько более тонкая вещь, на которую следует обратить внимание, - это третий и четвертый. Эти примеры содержат операцию ИЛИ, в которой обе стороны могут быть истинными. Это в сочетании с квантификатором выражения может привести к МНОГО потенциальных совпадений в зависимости от входной строки.
Подводя итог, TLDR-стиль:
Будьте осторожны при использовании квантификаторов в сочетании с другими операторами.
источник
Я на удивление встречал 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
истечении заданного тайм- что не приведет к использованию ресурсов, что приведет к отказу в обслуживании.Вы захотите поэкспериментировать со значением тайм-аута, чтобы убедиться, что оно подходит для вашего использования.
источник
Обнаружение злых регулярных выражений
Эмпирические правила
Плохие регулярные выражения всегда возникают из-за двусмысленности в соответствующей NFA, которую вы можете визуализировать с помощью таких инструментов, как regexper .
Вот несколько форм двусмысленности. Не используйте их в своих регулярных выражениях.
(a+)+
("высота звезды> 1"). Это может вызвать экспоненциальный взрыв. См. Подстакsafe-regex
.(a|a)+
. Это может вызвать экспоненциальный взрыв.\d+\d+
. Это может вызвать полиномиальный взрыв.Дополнительные ресурсы
Я написал эту статью о суперлинейных регулярных выражениях. Он включает множество ссылок на другие исследования, связанные с регулярными выражениями.
источник
Я бы сказал, что это связано с используемым механизмом регулярных выражений. Возможно, вам не всегда удастся избежать этих типов регулярных выражений, но если ваш механизм регулярных выражений построен правильно, это не проблема. См. Эту серию блогов для получения большого количества информации по теме движков регулярных выражений.
Обратите внимание на предостережение в конце статьи, поскольку возврат с возвратом является проблемой NP-Complete. В настоящее время нет возможности эффективно их обрабатывать, и вы можете запретить их вводить.
источник
a*a*
не использует обратные ссылки. Теперь движок регулярных выражений использует отслеживание с возвратом , что, возможно, вы имели в виду? В этом случае все современные движки используют откат. Вы можете легко отключить переход с возвратом(?>...)
, но это чаще всего не меняет значения вашего выражения (а в некоторых случаях его можно обойти).Я не думаю, что вы можете распознать такие регулярные выражения, по крайней мере, не все из них или нет, без строгого ограничения их выразительности. Если вас действительно волнуют ReDoS, я бы попытался изолировать их и убить их обработку таймаутом. Также возможно, что существуют реализации RegEx, которые позволяют ограничить их максимальное количество обратных запросов.
источник
a*
или\*
уязвимыми.a*
вообще не уязвим. Между тем,a{0,1000}a{0,1000}
катастрофическое регулярное выражение ждет своего часа. Дажеa?a?
при правильных условиях может иметь неприятные результаты.a*b*c*$
безопасно, ноa*b*[ac]*$
опасно, потому чтоa*
потенциально можно отказаться от символов,[ac]*
если ониb
отсутствуют, и начальное совпадение не удается (напримерaaaaaaaaaaaccccccccccd
).Я могу придумать несколько способов реализовать некоторые правила упрощения, запустив их на небольших тестовых входных данных или проанализировав структуру регулярного выражения.
(a+)+
можно сократить, используя какое-то правило для замены избыточных операторов просто(a+)
([a-zA-Z]+)*
также можно упростить с помощью нашего нового правила объединения избыточности, чтобы([a-zA-Z]*)
Компьютер может запускать тесты, выполняя небольшие подвыражения регулярного выражения для случайно сгенерированных последовательностей соответствующих символов или последовательностей символов и видя, в какие группы они все попадают. В первом случае компьютер похож: эй, регулярное выражение хочет а, так что давайте попробуем с
6aaaxaaq
. Затем он видит, что все а и только первая группа попадает в одну группу, и приходит к выводу, что независимо от того, сколько а ставится, это не имеет значения, поскольку+
получает все в группе. Второй - это как, эй, регулярному выражению нужна связка букв, поэтому давайте попробуем-fg0uj=
, и тогда он увидит, что снова каждая связка находится в одной группе, поэтому избавляется от+
в конце.Теперь нам нужно новое правило для обработки следующих: правило исключения-нерелевантных-параметров.
С
(a|aa)+
, компьютер смотрит на него , и как, нам нравится , что большой второй, но мы можем использовать это первый для заполнения в более пробелов, позволяет получить анс много АА , как мы можем, и посмотреть , если мы можем получить что - нибудь еще после того, как мы закончим. Он может запустить его с другой тестовой строкой, например `eaaa @ a ~ aa '. чтобы определить это.Вы можете защитить себя от
(a|a?)+
этого, заставив компьютер понять, что соответствующие строки -a?
это не те дроиды, которых мы ищем, потому что, поскольку они всегда могут совпадать где угодно, мы решаем, что нам не нравятся такие вещи(a?)+
, и выбрасываем их.Мы защищаемся от
(.*a){x}
этого, заставляя его понять, что персонажи, сопоставленные сa
, уже были бы схвачены.*
. Затем мы выбрасываем эту часть и используем другое правило для замены избыточных квантификаторов в(.*){x}
.Хотя реализация такой системы была бы очень сложной, это сложная проблема, и может потребоваться сложное решение. Вы также должны использовать методы, которые были предложены другими людьми, например, разрешить регулярному выражению только ограниченное количество ресурсов выполнения перед его уничтожением, если оно не завершится.
источник