PHP ACL Trying to make code safer

    I welcome the habrasociety.

    Imagine? that you are developing a product in which there is a system of modules. Modules can be written by third-party developers. Next, you load the modules into the system and run the code.
    In such a situation, the question often arises - how can I limit the possibilities of the launched code?

    We all remember stories with hidden miners that were added in the open library dependencies.

    How to protect your product from a module that will banally make a request to the database and upload the archive to some ftp server.

    If you are not Apple, Google, etc. and you don’t have a staff of moderators who will moderate downloadable modules, maybe a solution under the cut will make your life easier.

    I’ll immediately make a reservation that at the moment this post is only an attempt to probe the soil and collect the opinions of the community in order to understand whether it is worth digging further. The project is so far only a concept.

    There is a global security problem in IT related to the fact that PLs do not provide language-level capabilities to control privilege levels, as various operating systems do. To some extent, we can use these features, for example, change file permissions, allow prohibiting the opening of ports, and configure a firewall. But this is not always convenient. There is no way to split the code of your product into system and user (similar to kernel and userspace).

    The idea is to create some kind of ACL for the poor, until respected language developers introduce such features into the language itself.

    The basis is the excellent extension uopz . Thanks to the guys for the great work. Currently supported by PHP 7. (7.1 and 7.2).

    It allows us to override the built-in PHP functions and, very importantly, the class methods.
    Using this, we can redefine all dangerous functions (access to the FS, sockets, calls to exec, proc_open, etc.), replace them with our own, so that on the basis of this we can make a set of rules by which we allow / reject this action.

    I must say right away that functions like require, include, etc. there is no way to override these are not functions at all. But about this a little lower.

    This works as follows. Calls to dangerous functions are redirected to the function wrapper, which creates an object containing information about the call, then this object is passed to the rule ACL array. If at least one rule returns true, the action is allowed (passed to the native function). Otherwise, an error occurs.

    The initialization code is pretty simple. At the entry point we write.

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

    The config file for a project on a symphony can look like this:

     [
            '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());
                }
            ]
        ]
    ];

    In this configuration, an attempt to write / read / open a file from third-party code, even yours, will result in an error.

    Not many features are currently supported. If the comments do not reveal a clear lack of such an approach, it will not be difficult to outline the remaining functions.

    Concerning require and others like them. Unfortunately, it is impossible to defeat language constructions in this way. An evil hacker can still connect your config file and output it directly to the browser.

    But since this is a language construct , we cannot invoke it directly, by hiding the name in variables, etc.

    We cannot write something like:

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

    So we can go over the source and just find these dangerous calls and take action.

    Perhaps this functionality should be added to this project.

    The issue with the database can be solved in a similar way. It is possible for plugins to provide the ability to work only on behalf of a specific database user, barring through the ACL to get a link to a full-fledged connection to the database. For example, in the doctrine, we can create several EntityManagers and, through the ACL, prohibit third-party code from methods that allow it to get the default EntityManager with advanced rights.

    The source code can be viewed on github .

    I think I managed to convey the main idea. I hope in the comments to get your feedback.

    Also popular now: