PHP ACL. Попытка сделать код безопаснее

    Приветствую хабрасообщество.

    Представьте? что вы разрабатываете какой-то продукт в котором есть система модулей. Модули могут писать сторонние разработчики. Далее вы загружаете модули в систему и запускаете код.
    В такой ситуации часто возникает вопрос — как можно ограничить возможности запускаемого кода?

    Все мы помним истории со скрытыми майнерами, которые были добавлены в зависимости опенсорс библотек.

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

    Если вы не Apple, Google и т.д. и у вас нет штата своих модераторов которые будут модерировать загружаемые модули возможно решение под катом облегчит Вам жизнь.

    Сразу оговорюсь, что на данный момент этот пост лишь попытка прощупать почву и собрать мнения сообщества, дабы понять стоить ли копать дальше. Проект представляет из себя пока лишь концепт.

    В айти существует глобальная проблема безопасности, связанная с тем что ЯП не предоставляют возможностей на уровне языка контролировать уровни привилегий как это делают различные операционные системы. В какой-то мере мы можем использовать эти возможности, например менять права файлов, разрешать запрещать открытие портов, настраивать фаервол. Но это не всегда удобно. Нет возможности разделить код вашего продукта на системный и пользовательский (по аналогии с kernel и userspace).

    Идея сделать некую ACL для бедных, пока уважаемые разработчики языков не введут такие возможности в сам язык.

    За основу взято прекрасное расширение uopz. Спасибо ребятам за огромный труд. На данный момент поддерживается PHP 7. (7.1 и вроде 7.2)

    Оно позволяет нам переопределить встроенные PHP функции и что очень важно методы классов.
    Пользуясь этим, мы можем переопределить все опасные функции (доступ к ФС, сокеты, вызовы exec, proc_open и т.д.), заменить их своими, чтобы на базе этого сделать набор правил, по которым мы разрешаем / отклоняем данное действие.

    Сразу скажу что такие функции как require, include и т.д. нет возможности переопределить т.к. это не функции вовсе. Но про это чуть ниже.

    Это работает следубщим образом. Вызовы опасных функции перенаправляются в функции обертку, которая создает объект содержащий инфорацию о вызове, далее этот объект передается массиву ACL правил. Если хоть одно правило вернет true, действие разрешается (передается нативной функции). В противном случае возникает ошибка.

    Код инициализации довольно прост. В точке входа пишем.

    $acl = \PhpAcl\ACLComponent::getInstance();
    $acl->init(require_once __DIR__ . '/../app/config/acl.php');

    Файл конфига для проекта на симфони может иметь такой вид:

    <?php
    
    use PhpAcl\IOOperation;
    
    define('ROOT_DIR', realpath(__DIR__ . '/../../../code'));
    
    return [
    
        'io' => [
            'enabled' => true,
    
            'rules' => [
                // allow all from symfony
                function(IOOperation $operation){
                    return $operation->callerStartsWith(ROOT_DIR . '/vendor/symfony/symfony/src');
                },
                // allow doctrine to read annotations
                function(IOOperation $operation){
                    return
                        $operation->isReadOrOpenOperation() &&
                        $operation->callerStartsWith(ROOT_DIR . '/code/vendor/doctrine/annotations');
                },
                // allow writing to cache
                function(IOOperation $operation){
                    return
                        $operation->isWriteOrOpenOperation() &&
                        preg_match(sprintf('{^%s/var/cache/dev|prod/}', ROOT_DIR), $operation->getSrc());
                },
                // allow reading from cache
                function(IOOperation $operation){
                    return
                        $operation->isReadOrOpenOperation() &&
                        preg_match(sprintf('{^%s/var/cache/dev|prod/}', ROOT_DIR), $operation->getSrc());
                },
                // allow monolog read/write log files
                function(IOOperation $operation){
                    return
                        (
                            $operation->type === IOOperation::TYPE_WRITE ||
                            $operation->type === IOOperation::TYPE_READ ||
                            $operation->type === IOOperation::TYPE_OPEN
                        ) &&
                        $operation->callerStartsWith(ROOT_DIR. '/vendor/monolog/monolog/src') &&
                        preg_match(sprintf('{^%s/var/logs/dev|prod.log}', ROOT_DIR), $operation->getSrc());
                },
                // allow twig to read/write template files
                function(IOOperation $operation){
                    return
                        (
                            $operation->type === IOOperation::TYPE_WRITE ||
                            $operation->type === IOOperation::TYPE_READ ||
                            $operation->type === IOOperation::TYPE_OPEN
                        ) &&
                        $operation->callerStartsWith(ROOT_DIR . '/vendor/twig/twig') &&
                        preg_match('/\.twig$/', $operation->getSrc());
                }
            ]
        ]
    
    ];

    В такой конфигурации попытка записать/прочитать/открыть файл из стороннего кода, даже вашего, приведет к ошибке.

    На данный момент поддерживается не так много функций. Если в коментариях не выявится явного недостатка такого подхода, набросать остальные функции не составит труда.

    По поводу require и иже с ними. К сожалению победить таким образом конструкции языка невозможно. Злой хакер до сих пор может подключить ваш файл конфигов и вывести его прямо в браузер.

    Но так как это конструкция языка, мы не можем вызывать её не напрямую, через сокрытие названия в переменных и т.д.

    Мы не можем написать что-то вроде:

    $_req = 'require';
    $_req('/app/config/config.yml');

    Значит мы можем пробежаться по исходникам и просто найти данные опасные вызовы и принять меры.

    Возможно, такой функционал стоит добавить в этот проект.

    Вопрос с БД можно решить похожим способом. Можно плагинам предоставить возможность работать только от имени определенного пользователя БД, запретив через ACL получить ссылку на полноправный конект к базе. К примеру в доктрине мы можем создать несколько EntityManager и через ACL запретить стороннему коду методы, которые позволят ему получить дефолтный EntityManager с раширенными правами.

    Исходный код можно посмотреть на github.

    Я думаю, у меня получилось донести основную идею. Надеюсь, в комментариях получить ваши отзывы.
    Поделиться публикацией

    Комментарии 6

      0
      Прошу прощения за оффтоп, но за что вы так не любите слово «функция»?
        0
        Прочитал и понял что как-то на автомате оно работает. Поправил.
        0
        Это все, конечно, здорово. Идея мне нравится. Но какой оверхед от этого, не замеряли? Или это все пока что на стадии концепта?
          0
          Вы знаете особой разницы я не заметил. Если правил много можно кешировать результаты работы ACL слоя. Основной минус в том что нельзя давайть возможность отключать ацл после инициализации, иначе это может сделать кто угодно. Т.е. нельзя например сделать ACL OFF --> do many file work ---> ACL ENABLE
          0

          Из за таких как вы — все плохо!
          То что генерите идеи хорошо.
          ТОЛЬКО ПОЖАЛУЙСТА НЕ ИСПОЛЬЗУЙТЕ ИХ НИГДЕ!
          А то какакой нить стартапер прочтет и сделает очередного монстра.
          А так делать нельзя, точнее можно если понимаешь чётко что зачем и какой ценой

            0
            Было бы хорошо к такому гневному комментарию чуток обоснований.

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

          Самое читаемое