python

evalidate: безопасная обработка пользовательских выражений

  • среда, 21 января 2015 г. в 02:10:49
http://habrahabr.ru/post/248117/

Зачем нужно


Различная фильтрация есть везде. Например, файрволл netfilter (iptables) имеет свой синтаксис для описания пакетов. В файле .htaccess апача свой язык, как определять, кому давать доступ к каталогу, кому нет. В СУБД свой очень мощный язык (SQL WHERE ...) для фильтрации записей. В почтовых программах (thunderbird, gmail) — свой интерфейс описания фильтров, в соответствии с которыми письма будут раскидываться по папкам.

И везде — свой велосипед.

Для бухгалтерской программы вам может быть удобно позволить пользователю выбрать, кому будет повышена зарплата (все женщины, а так же мужчины возрастом от 25 до 32 лет, либо же до 50 лет если у мужчины имя Вася). И каждому подходящему повысить по пользовательскому выражению ( + 2000 рублей + 20% от прежней зарплаты + по 1000 рублей за каждый год стажа)

Для интернет-магазина (или его админки) — найти все ноутбуки, с памятью от 4 до 8 Gb, которых на складе более 3 штук, но не Acer, или даже Acer, если стоят меньше 30 000 рублей.

Конечно, можно присобачить свою сложную систему фильтров и критериев, сделать для них веб-интерфейс, но проще было бы все сделать в пару строк?

src="(RAM>=4 and RAM<=8 and stock>3 and not brand=='Acer') or (brand=='Acer' and price<30000)"
success, result = evalidate.safeeval(src,notebook)



Хочется и колется


Очевидный способ добавить любую логику в программу — через eval(). Решение самое простое, самое гибкое, но есть большие подводные камни — безопасность. Что если пользовательское выражение будет делать os.system('rm -rf /')?

Пример, как можно «завалить» питон через eval():
stackoverflow.com/questions/13066594/is-there-a-way-to-secure-strings-for-pythons-eval
nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
tav.espians.com/a-challenge-to-break-python-security.html

Right Way


Часто в советах рекомендуется «правильный путь» — использовать сам питон, чтобы он парсил код из текстовой формы в AST дерево, а потом уже самостоятельно парсить это дерево, отделяя зерна от козлищ. Но как? И тут выходит на арену главная проблема веломаркетинга — пока найдешь подходящий велосипед, или хотя бы хороший чертеж… проще самому велосипед изобрести.

Evalidate


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

Ставим пипом
pip install evalidate


Простой пример:

import evalidate

depot = [
    {
        'book': 'Sirens of Titan',
        'price': 12,
        'stock': 4
    },
    {
        'book': 'Gone Girl',
        'price': 9.8,
        'stock': 0
    },
    {
        'book': 'Choke',
        'price': 14,
        'stock': 2
    },
    {
        'book': 'Pulp',
        'price': 7.45,
        'stock': 4
    }
]

#src='stock==0' # books out of stock
src='stock>0 and price>8' # expensive book available for sale

for book in depot:
    success, result = evalidate.safeeval(src,book)

    if success:
        if result:
            print book
    else:
        print "ERR:", result



В данном случае, в src у нас «пользовательский» код, который может быть каким-то плохим. В примере два варианта «хорошего» кода, первый показывает книги, которых у нас нет на складе, второй — дорогие книги, которые в наличии. Если попробовать подсунуть плохой код (просто который не парсится, код с обращением к переменным, которых у нас нет в контексте, код, который использует неразрешенные операции, например Call (вызов функции)), то success будет False, и программа сообщит об ошибке (Но не упадет, и не исполнит плохой код).

Как альтернатива — можно через evalidate.evalidate() получить AST-дерево, которое генерируется через ast.parse (либо exception, если код не парсится или содержит неразрешенные операции), и затем уже его скомпилировать и исполнить через eval().
node = evalidate.evalidate(src)
code = compile(node,'<usercode>','eval')
result = eval(code,{},data)


Ну и еще посмотреть в код модуля (благо он простой), и сделать свой велосипед :-)

Обращение к сообществу


В evalidate по умолчанию включен свой набор «безопасных» (?) питоновских операций. Просто, по моему личному мнению — они безопасны. Это значит, что в течение 15 минут мне не пришло в голову, как сделать что-то ужасное, используя только эти операции. Но может быть вам придет? Или, может стоит добавить в список еще какие-то операции, которые сделают дефолтную конфигурацию более гибкой (позволят использовать более богатый язык выражений), и при этом не создадут уязвимостей? Есть идеи?