Можно ли использовать пользовательский ввод для регулярных выражений Python?

Я хочу, чтобы мои пользователи использовали регулярные выражения для некоторых функций. Мне любопытно, какие последствия для ввода пользовательского ввода в re.compile(). Я предполагаю, что пользователю не существует возможности дать мне строку, которая позволила бы им выполнять произвольный код. Опасности, о которых я думал, следующие:

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

Решение 1. легко: уловить исключения. Я не уверен, есть ли хорошее решение для 2. Возможно, только ограничение длины регулярного выражения будет работать.

Есть ли что-нибудь еще, о чем мне нужно беспокоиться?

Ответы

Ответ 1

Я работал над программой, которая позволяет пользователям вводить собственное регулярное выражение, и вы правы - они могут (и делать) вводить регулярное выражение, которое может занять много времени, а иногда и дольше, чем время жизни юниверса. Что еще хуже, при обработке регулярного выражения Python содержит GIL, поэтому он не только повесит поток, в котором работает регулярное выражение, но и вся программа.

Ограничение длины регулярного выражения не будет работать, так как проблема возвращается. Например, соответствие регулярному выражению r"(\S+)+x" в строке длины N, которая не содержит "x", будет возвращать 2 ** N раз. В моей системе это занимает около секунды, чтобы соответствовать "a"*21, а время удваивается для каждого дополнительного символа, поэтому строка из 100 символов займет приблизительно 19167393131891000 лет (это оценка, я ее не приурочил).

Для получения дополнительной информации прочитайте книгу O'Reilly "Освоение регулярных выражений" - в ней есть пара глав о производительности.

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

Еще одна вещь, на которую мы смотрели, - это исправление модуля re, чтобы вызвать исключение, если оно слишком многократно отступает. Это возможно, но требует изменения источника Python C и перекомпиляции, поэтому он не переносится. Мы также представили патч для выпуска GIL при сопоставлении с строками python, но я не думаю, что он был принят в ядро ​​(python содержит только GIL, поскольку регулярное выражение может быть запущено в отношении изменяемых буферов).

Ответ 2

Для обычных пользователей гораздо проще дать им язык подмножества. Например, правила globing оболочки в fnmatch. Еще один пример - условия SQL LIKE.

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

Ответ 3

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

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

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

edit: Как отмечает Дэйв, по-видимому, блокировка глобального интерпретатора сохраняется во время сопоставления регулярных выражений, что сделает установку более сложной. Если это так, единственный вариант установки тайм-аута - запустить совпадение в отдельном процессе. Хотя он не совсем идеален, он выполним. Я полностью забыл о multiprocessing. Точка интереса этот раздел об общих объектах. Если вам действительно нужны жесткие ограничения, здесь можно перейти к отдельным процессам.

Ответ 4

Не нужно использовать compile(), за исключением случаев, когда вам нужно повторно использовать множество разных регулярных выражений. Модуль уже кэширует последние выражения.

Точка 2 (при исполнении) может быть очень сложной, если вы разрешаете пользователю вводить любое регулярное выражение. Вы можете создать сложное регулярное выражение с несколькими символами, например, знаменитым (x+x+)+y. Я думаю, что проблема еще не решена в общем виде. Обходной путь может запускать другой поток и контролировать его, если он превышает допустимое время, убить поток и вернуться с ошибкой.

Ответ 5

Я действительно не думаю, что можно выполнить код, просто передав его в re.compile. То, как я это понимаю, re.compile(или любая система регулярных выражений на любом языке) преобразует строку регулярного выражения в конечный автомат (DFA или NFA), и, несмотря на зловещее имя "компиляция", это не имеет никакого отношения к выполнению какого-либо кода.

Ответ 6

Технически вам не нужно использовать re.compile() для выполнения операции регулярного выражения в строке. На самом деле, метод компиляции часто может быть медленнее, если вы выполняете операцию только один раз, поскольку там связаны служебные данные, связанные с первоначальной компиляцией.

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