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

    Зачем нужно


    Различная фильтрация есть везде. Например, файрволл 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 ( перевод на хабре: habrahabr.ru/post/221937 )
    tav.espians.com/a-challenge-to-break-python-security.html

    Right Way


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

    Evalidate


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

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


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

    На сайте книжного магазина размещаем текстовую строчку поиска (значение передаем в переменную src — здесь они hardcoded чтобы веб-приложение не городить, но их вполне безопасно брать из запроса пользователя), и пользователи могут искать книги по любым доступым критериям в любой комбинации. Вместо отдельных кнопок, чтобы показать «книги, которых нет в наличии» «дешевые книги» «дорогие книги», «Книги авторов умерших до второй мировой, проживавших в Австралии либо любой из стран Африки, которые (книги) у нас в наличии больше чем в 10 экземплярах, и стоят дешевле чем $1 за 100 страниц книги» — просто одно текстовое поле.

    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 минут мне не пришло в голову, как сделать что-то ужасное, используя только эти операции. Но может быть вам придет? Или, может стоит добавить в список еще какие-то операции, которые сделают дефолтную конфигурацию более гибкой (позволят использовать более богатый язык выражений), и при этом не создадут уязвимостей? Есть идеи?

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 8

      –11
      ACL для доступа к объектам не решение? Или в мире WEB/LINUX принято давать доступ ко всему подряд без проверки, кто куда хочет???
      Зачем ВООБЩЕ ПАРСИТЬ на плохой код в запросе??? Искренне не понял проблемы, подскажите, please.
        +2
        Судя по тому, что я тоже не понял суть возражения, видимо я не совсем четко в статье это прописал. Область применения — везде, где «по фичам» подходит eval(), но «по безопасности» он не подходит. Часть возможных примеров приведены в статье (хоть и использовать питон для фильтрации трафика — слишком медленно, тем не менее) — в файрволе, в почтовой программе, в бухгалтерской, в любом веб-приложении с базой данных, даже в СУБД. В том числе — и для определения админом правил доступа (для всех из группы такой-то, а так же из группы другой-то только по четвергам, если обращаются к какому-то конкретному ресурсу) — это же тоже по сути фильтрация запросов на доступ по какому-то сложному критерию.

        К примеру, на хабре нет возможности найти все посты в определенных хабах с определенными тегами за указанный период. Как это реализовать? SQL строчку из мира брать — опасно очень. Сделать свой advanced search? Можно, но во-первых сложно (для каждого критерия надо формочку/поле), во-вторых, все равно это не будет гибче текстового выражения — критерии будут либо объединены по И либо по ИЛИ, либо придется вообще какой-то невероятной сложности страничку делать чтобы реализовать выражение вроде: post.date>20100101 and («security» in post.tags or «python» in post.tags)
          0
          Спасибо, теперь всё встало на место! :-)
        0
        А ссылочку на исходники не подскажете?
          0
          там все в одном __init__.py, можно pip'ом взять и посмотреть. ну или на битбакете: bitbucket.org/yaroslaff/evalidate
          +2
          success, result = evalidate.safeeval(src,book)
          

          Вот этот код очень сильно попахивает C-подобными языками. В питоне принято использовать исключения для таких вещей, которые вы используете внутри evalidate.safeeval, но почему-то стесняетесь выбрасывать наружу. Было бы неплохо сделать что-то вроде такого:
          try:
              evalidate.safeeval(src, book)
          except evalidate.ValidationError:
              pass  # отображение ошибок, записи в логи, etc
          


          Кроме того, стоит натравить pep8 и pep257 на исходники, чтобы привести код к общепринятым style guide.
            0
            Да, согласен. Но нет — не стесняюсь, сам главный метод — evalidate.evalidate — он «публичный», на rtfd описан, и примеры с ним там есть, просто эта статья — он как «реклама», просто чтобы кратко показать что умеет модуль и зачем он нужен, поэтому тут пример покороче и попонятнее, ближе к самому eval() по синтаксису. А try/except на мой взгляд сильно загромождают код и производят впечатления «сложности». То есть, для продакшна — уместны, а для короткого примера — мне кажутся оверкиллом.

            Насчет pep — хороший совет, спасибо, не знал о них. Сейчас погоняю-посмотрю.

        Only users with full accounts can post comments. Log in, please.