Як я можу розпізнати злий регулярний вираз?


85

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

Опис злих регулярних виразів з Вікіпедії :

  • регулярний вираз застосовує повторення ("+", "*") до складного підвиразу;
  • для повторного підвираження існує збіг, який також є суфіксом іншого дійсного збігу.

З прикладами, знову з Вікіпедії :

  • (a+)+
  • ([a-zA-Z]+)*
  • (a|aa)+
  • (a|a?)+
  • (.*a){x} при x> 10

Це проблема, яка просто не має більш простого пояснення? Я шукаю щось, що полегшило б уникнути цієї проблеми під час написання регулярних виразів або знайти їх у існуючій кодовій базі.


7
Ще одне посилання на цю тему це одна: regular-expressions.info/catastrophic.html
Daniel Hilgarth

1
Ось інструмент для проведення статичного аналізу регулярних виразів для виявлення підозр на проблеми ReDoS
tripleee,

Здається, що посилання, надане @tripleee, має непрацююче посилання на інструмент RXXR. Ось дзеркало GitHub: github.com/ConradIrwin/rxxr2
Майк Хілл

3
Крім того, для тих, хто цікавиться, схоже, що автори оригінального інструменту RXXR замінили його на RXXR2. Їх нова сторінка розміщена тут і наразі має робоче посилання на джерело RXXR2
Майк Хілл

Відповіді:


77

Чому злі регулярні вирази є проблемою?

Оскільки комп’ютери роблять саме те, що ви наказали їм робити, навіть якщо це не те, що ви мали на увазі, або зовсім нерозумно. Якщо ви попросите механізм регулярних виразів довести, що для певного даного вводу існує або не відповідає даному шаблону, тоді движок спробує це зробити, незалежно від того, скільки різних комбінацій потрібно перевірити.

Ось простий зразок, натхненний першим прикладом у дописі ОП:

^((ab)*)+$

Враховуючи введені дані:

абабабабабабабабабабабаб

Двигун регулярного виразу намагається щось подібне (abababababababababababab) і з першої спроби знайдено відповідність.

Але тоді ми кидаємо гайковий ключ:

abababababababababababab a

Спочатку двигун спробує, (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 у програмах, що використовують регулярні вирази
Валентин Вюстхольц, Освальдо Оливо, Марійн Й.Х. Хейле та Ісіль Ділліг, Техаський
університет в Остіні


Це дуже гарна відповідь при описі / чому / приклад регулярного виразу займає багато часу, але я шукаю кілька правил, які людина може узагальнити, щоб допомогти розпізнати проблему регулярного виразу.
Mike Partridge

4
Знання "чому" - це найважливіший крок до уникнення написання "злого" регулярного виразу. На жаль, після написання фактично дуже важко негайно або швидко знайти проблему регулярного виразу. Якщо ви хочете загальний виправлення, атомарне групування, як правило, є найкращим способом, але це може мати значний вплив на шаблони, з якими буде відповідати регулярний вираз. Зрештою, розпізнавання поганого регулярного виразу подібне до регулярного виразу будь-якого іншого поганого коду - для цього потрібен великий досвід, багато часу та / або одна катастрофічна подія.
JDB все ще пам'ятає Моніку

Ось чому я віддаю перевагу двигунам регулярних виразів, які не підтримують зворотне відстеження без потреби користувача. IE lex / flex.
Спенсер Ратбун,

@MikePartridge - це загальна проблема класичної теорії ІТ: вирішити, чи буде якийсь код нескінченно крутитися чи зупинятись, є проблемою, що повна NP. З регулярними виразами ви, мабуть, можете здогадатися / зловити деякі з них, шукаючи певні шаблони / правила, але якщо ви не зробите важкого аналізу, повного NP, ви ніколи не вловите їх усіх. Деякі варіанти: 1) ніколи не дозволяйте користувачеві вводити регулярний вираз на ваш сервер. 2) налаштуйте механізм регулярних виразів для завершення обчислення досить рано (але протестуйте свій дійсний регулярний вираз у своєму коді, як і раніше, навіть із суворими обмеженнями). 3) запустити код регулярного виразу в потоці з низьким пріоритетом з обмеженнями процесора / пам’яті.
Ped7g

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

Я б підсумував це як "Повторення повторення". Перший приклад, який ви перелічили, є хорошим, оскільки в ньому вказано "букву а, один або кілька разів поспіль. Це може повторитися один або кілька разів поспіль".

У цьому випадку слід шукати комбінацію кванторів, таких як * і +.

Дещо більш тонка річ, на яку слід звернути увагу, - це третя та четверта. Ці приклади містять операцію АБО, в якій обидві сторони можуть бути істинними. Це в поєднанні з квантором виразу може призвести до МНОГО потенційних збігів, залежно від вхідного рядка.

Підводячи підсумок, у стилі TLDR:

Будьте обережні, як квантори використовуються в поєднанні з іншими операторами.


3
В даний час ця відповідь найближча до того, що я шукаю: емпіричне правило розпізнавання регулярного виразу, яке може спричинити катастрофічні зворотне відстеження.
Mike Partridge

1
Те, що ви залишили поза увагою, і що, здається, є важливою частиною проблеми, - це захоплення груп.
Mike Partridge

@MikePartridge Це теж. Я намагався звести це якомога більше, тому є й інші речі, які можуть спричинити те саме, наприклад, захоплення груп.
Ярмунд,

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");
}

Цей регулярний вираз вразливий до відмови в обслуговуванні і без тайм-ауту буде обертатися і з'їдати ресурси. З таймаутом він викине aRegexMatchTimeoutException після заданого тайм-ауту і не призведе до використання ресурсу, що призведе до стану відмови в обслуговуванні.

Вам доведеться поекспериментувати зі значенням часу очікування, щоб переконатися, що воно працює для вашого використання.


7

Виявлення злих регулярних термінів

  1. Спробуйте RegexStaticAnalysis Nicolaas Weideman .
  2. Спробуйте мій вугільно-регулярний детектор у стилі ансамблю, який має CLI для інструменту Вейдемана та інших.

Емпіричні правила

Злі регулярні вирази завжди обумовлені двозначністю у відповідному NFA, який ви можете візуалізувати за допомогою таких інструментів, як регулярний вираз .

Ось деякі форми двозначності. Не використовуйте їх у своїх регулярних виразах.

  1. Вкладені квантори на зразок (a+)+(він же "висота зірки> 1"). Це може спричинити експоненціальний вибух. Див. Підстікsafe-regex Інструмент .
  2. Кількісне перекриття диз’юнкцій типу (a|a)+ . Це може спричинити експоненціальний вибух.
  3. Уникайте кількісно перекриваються суміжностей, таких як \d+\d+. Це може спричинити вибух поліномів.

Додаткові ресурси

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


4

Я б сказав, що це пов'язано із використовуваним двигуном регулярних виразів. Можливо, вам не завжди вдається уникнути таких типів регулярних виразів, але якщо ваш механізм регулярних виразів побудований правильно, то це менше проблем. Дивіться цю серію блогів для отримання великої кількості інформації на тему механізмів регулярних виразів.

Зверніть увагу на застереження внизу статті, оскільки зворотне відстеження є проблемою, що заповнює NP. Наразі неможливо ефективно їх обробити, і ви можете заборонити їх у своєму введенні.


a*a*не використовує зворотні посилання. Тепер движок регулярних виразів використовує зворотне відстеження , що, мабуть, ви мали на увазі? У цьому випадку всі сучасні двигуни використовують зворотне відстеження. Ви можете легко відключити зворотне відстеження за допомогою (?>...), але це частіше тоді не змінить значення вашого виразу (а в деяких випадках його можна обійти).
JDB все ще пам'ятає Моніку

@ Cyborgx37 ох! Я мав на увазі зворотне відстеження. Виправлено.
Спенсер Ратбун,

У цьому випадку двигун або використовує зворотне відстеження, або він цього не робить. Практично немає способу обмежити зворотне відстеження, обмежуючи введення.
JDB все ще пам'ятає Моніку

2
@JDB: "усі сучасні двигуни використовують зворотне відстеження". - Можливо, це було правдою в 2013 році, але вже не .
Кевін

@Kevin - впевнений. ти виграв.
JDB все ще пам'ятає Моніку

3

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


2
Я думаю, ви нерозумієте питання. Коли я читав це, OP буквально запитує, як він може розпізнати злий регулярний вираз, а не як він може написати програму для цього. Наприклад, "Я написав цей регулярний вираз, але як я можу зрозуміти, чи це може бути злом?"
ruahh

Е-е, ти можеш мати рацію. Тоді я можу лише порекомендувати статтю про катастрофічне зворотне відстеження, до якої @DanielHilgarth вже посилався в коментарях.
Бергі,

2
@ 0x90: Тому що я не вважаю, наприклад, a*або \*"вразливим".
ruahh

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}.

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


1
"бути подібним", розпізнавати, що щось "хоче", "пробувати" здогадки, "бачити" і робити висновки ("реалізувати", "визначати") - нетривіальні проблеми, які важко реалізувати алгоритмічно для комп'ютерів ... А приклади тестування - це ні на що покладатися, швидше вам знадобиться якесь підтвердження.
Бергі,

@Bergi Я мав на увазі під тестовими прикладами те, що ви берете крихітний фрагмент повного регулярного виразу і запускаєте його проти тестового рядка як простий спосіб визначити, як він поводиться. Звичайно, ви тестуєте лише ті фрагменти, які ви вже вивчили і вже знаєте, що не робите дивних речей у тестових випадках.
AJMansfield
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.