如何安全地使用PHP Runkit_Sandbox

问题描述 投票:5回答:3

我正在构建一个教学工具Web应用程序,允许用户将php类作为文本提交,然后应用程序将运行它们。我认为Runkit_Sandbox是这项工作的工具,但是the docs没有提供关于使用哪种配置的建议。

是否有已建立的应禁用的功能列表?还是上课?我打算将所有其他配置设置为尽可能限制(例如关闭url fopen),但我甚至不能100%确定它们是什么。任何建议都非常感谢。

php security
3个回答
2
投票

该功能的主要部分是沙箱(Runkit_Sandbox类)。使用它们,您可以在隔离的环境中运行PHP代码。每个沙箱都可以配置自己的PHP安全选项,例如safe_mode,safe_mode_gid,safe_mode_include_dir,open_basedir,allow_url_fopen,disable_functions,disable_classes。

此外,每个沙箱都可以为Runkit的INI设置提供单独的值:拥有全局变量并禁止覆盖内置函数。

沙箱可以加载PHP文件(通过include(),include_once(),require()和require_once()),调用函数内部,执行任意PHP代码并打印包含的变量值。此外,您可以指定捕获和处理沙箱输出的函数。

在沙箱中,您可以创建一个反沙箱类Runkit_Sandbox_Parent的对象,该对象将沙箱与其父环境连接起来。防沙箱的功能与沙箱的功能非常相似,但出于安全原因,应在沙箱创建期间明确启用与外部环境的每种类型的通信。


2
投票

我正在构建一个教学工具Web应用程序,让用户提交php类

如果您正在构建应用程序,那么您不会对您的环境进行太多控制。这意味着您的解决方案必须基于PHP(这使得Runkit具有吸引力),因为应用程序可能托管在任何地方,可能在某些地方您无法安装以下任何解决方案。您仍然只能提供那些提供Runkit或安装它的可能性的ISP,但它们中的ISP可以安装chroot jail或Web服务器的第二个副本。

但是从其他评论来看,我认为你正在构建一个安装。也就是说,你可以按照自己的意愿做一整台机器(真实的或虚拟的)。这使其他,恕我直言更有效,方法可能:

Two web servers (easier)

安装仅在本地主机上侦听的第二个Web服务器,具有降低的权限(例如,作为用户nobody运行,对其自己的Web根没有写入权限)。在那里安装一个强化的PHP实例。要做到这一点,从old rules with list of functions to disable开始,然后检查HOWTOs和一些指针here。最后一个针对您的客户更有针对性,但可能有用(并且知道您已经考虑过安全性可能会减少规避尝试)。

您甚至可以在辅助PHP上安装XDebug,并使用例如PHPUnit的代码覆盖工具可以生成有用的信息。

现在,您可以通过从主Web安装写入/var/www-secure/website-user123/htdocs来部署代码,该安装可以写入/var/www-secure,并通过restart命令在辅助Web服务器上运行system("sudo...")。您可以通过curl向托管的Web应用程序提供“真实生活”命令。

Linux允许使用apparmor / SELinuxuserid-based firewall rules进一步加强。这意味着无论托管应用程序做什么,它都无法在外部进行通信,接收除您之外的其他命令,或在Web根目录之外执行任何操作 - 您可以在其中阅读任何内容并进行检查,例如:通过tripwire

您甚至可以启用危险功能(但由apparmor / iptables阻止),然后检查日志以查看是否已触发防御。不是真的推荐。你确实想要检查日志(并且可能在运行未知类后运行对系统进行tripwire检查),但是,如果有人成功推翻了PHP.INI中的第一层防御并被apparmor粉碎了。

Chroot jail

这是hanshenrik的答案,如果您通过CLI运行,那么它很有吸引力。根据您的设置和类需要做什么,它可能比其他两种替代方案更好(仍然需要防火墙/ apparmor,或者至少它可以从中受益),或者功能较弱。

Virtual machine (more secure)

如上所述,但这次“第二次安装”在VM内完全隔离。你可以用Docker做到这一点,但它不会那么安全;还是,check this out。您可以使用FTP在VM内部发送代码(PHP具有此命令)。此设置允许更好地与主安装隔离。它不如其他两个解决方案灵活,因为您应该为每个用户使用一个VM,并且将VM重置为中性更昂贵。运行VM更昂贵。另一方面,它可以更彻底(即你可以更容易地重新部署整个事物)并且限制粉碎攻击是不可能的,因为流氓类最多可以成功占用虚拟CPU。


1
投票

I think Runkit_Sandbox is the tool for this job - 我没有。假设您在Unix系统上运行,我可以建议改为chroot jail吗?

mkdir /jail /jail/bin /jail/lib /jail/lib64 /jail/usr /jail/etc /jail/etc/alternatives
chmod -R 0711 /jail
chown -R root:root /jail
mount -o bind,ro /bin /jail/bin
mount -o bind,ro /lib /jail/lib
mount -o bind,ro /lib64 /jail/lib64
mount -o bind,ro /usr /jail/usr
mount -o bind,ro /etc/alternatives /jail/etc/alternatives

我假设您的代码最初由非特权用户接收和处理,我们称之为www-data,如果是这样,您可以使用sudo允许www-data使用sudo运行特定命令,为此,添加

www-data ALL = (root) NOPASSWD: /usr/bin/php /jail/jailexecutor.php

到/ etc / sudoers

这将允许www-data运行特定命令sudo /usr/bin/php /jail/jailexecutor.php

现在对于jailexecutor.php,它从STDIN获取源代码,用php chrooted到/ jail作为用户nobody执行它,并回显代码生成的STDOUT和STDERR,如果运行时间超过5秒则终止它,

<?php
declare(strict_types = 1);
const MAX_RUNTIME_SECONDS = 5;
if (posix_geteuid () !== 0) {
    fprintf ( STDERR, "this script must run as root (only root can chroot)" );
    die ();
}
$code = stream_get_contents ( STDIN );
if (! is_string ( $code )) {
    throw new \RuntimeException ( 'failed to read the code from stdin! (stream_get_contents failed)' );
}
$file = tempnam ( __DIR__, "unsafe" );
if (! is_string ( $file )) {
    throw new \RuntimeException ( 'tempnam failed!' );
}
register_shutdown_function ( function () use (&$file) {
    if (! unlink ( $file )) {
        throw new \RuntimeException ( 'failed to clean up the file! (unlink failed!?)' );
    }
} );
if (strlen ( $code ) !== file_put_contents ( $file, $code )) {
    throw new \RuntimeException ( 'failed to write the code to disk! (out of diskspace?)' );
}
if (! chmod ( $file, 0444 )) {
    throw new \RuntimeException ( 'failed to chmod!' );
}
$starttime = microtime ( true );
$unused = [ ];
$ph = proc_open ( 'chroot --userspec=nobody /jail /usr/bin/php ' . escapeshellarg ( basename ( $file ) ), $unused, $unused );
$terminated = false;
while ( ($status = proc_get_status ( $ph )) ['running'] ) {
    usleep ( 100 * 1000 ); // 100 ms
    if (! $terminated && microtime ( true ) - $starttime > MAX_RUNTIME_SECONDS) {
        $terminated = true;
        echo 'max runtime reached (' . MAX_RUNTIME_SECONDS . ' seconds), terminating...';
        pkilltree ( ( int ) ($status ['pid']) );
        // proc_terminate ( $ph, SIGKILL );
    }
}
echo "\nexit status: " . $status ['exitcode'];
proc_close ( $ph );
function pkilltree(int $pid) {
    system ( "kill -s STOP " . $pid ); // stop it first, so it can't make any more children
    $children = shell_exec ( 'pgrep -P ' . $pid );
    if (is_string ( $children )) {
        $children = trim ( $children );
    }
    if (! empty ( $children )) {
        $children = array_filter ( array_map ( 'trim', explode ( "\n", $children ) ), function ($in) {
            return false !== filter_var ( $in, FILTER_VALIDATE_INT ); // shouldn't be necessary, but just to be safe..
        } );
        foreach ( $children as $child ) {
            pkilltree ( ( int ) $child );
        }
    }
    system ( "kill -s KILL " . $pid );
}

现在可以从www-data安全地执行PHP代码,如下所示:

<?php
declare(strict_types = 1);
header ( "content-type: text/plain;charset=utf8" );
$unsafeCode = ( string ) ($_POST ['code'] ?? '');
$pipes = [ ];
$proc = proc_open ( "sudo /usr/bin/php /jail/jailexecutor.php", array (
        0 => array (
                "pipe",
                "rb" 
        ),
        1 => array (
                "pipe",
                "wb" 
        ),
        2 => array (
                "pipe",
                "wb" 
        ) 
), $pipes );
fwrite ( $pipes [0], $unsafeCode );
fclose ( $pipes [0] );
while ( ($status = proc_get_status ( $proc )) ['running'] ) {
    usleep ( 100 * 1000 ); // 100 ms
    echo stream_get_contents ( $pipes [2] );
    echo stream_get_contents ( $pipes [1] );
}
// var_dump($status);
echo stream_get_contents ( $pipes [2] ); // just to be safe, it's technically possible that we dont get any cpu time between proc_open, the child finishes, and proc_get_status.. just extremely unlikely.
echo stream_get_contents ( $pipes [1] );
proc_close ( $proc );

并且为了快速测试,curl -d code='<?php echo rand()."it works!";' url(您甚至可以毫无后顾之忧地添加system("rm -rfv --no-preserve-root /");

  • 在debian 9延伸测试
© www.soinside.com 2019 - 2024. All rights reserved.