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