根据用户输入创建一个类

时间:2013-04-19 10:02:46

标签: php

要求$输入是不安全的。 '.PHP'。然后开始上课。如何使其安全,无需使用可以被提及的类的白名单。

Ex 1.(错误代码)。

<?php

$input = $_GET['controller'];

require $input . '.php';

new $input;

?>

9 个答案:

答案 0 :(得分:13)

<强>声明

我首先要说的是,在您的系统中定义静态路由是安全的,但这个答案,即使我已经努力减轻安全问题,也应该在信任其操作之前进行全面测试和理解。

基础知识

首先,使用从the manual获取的正则表达式确保控制器包含有效的变量名称;这清除了明显错误的条目:

$controller = filter_input(INPUT_GET, FILTER_VALIDATE_REGEXP, [
    'options' => [
        'regexp' => '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/',
        'flags' => FILTER_NULL_ON_FAILURE,
    ]
]);

if ($controller !== null) {
    // load and use controller
    require_once("$controller.php");
    $c = new $controller();
}

实施等级

这很好用,但如果有人试图加载内部类呢?它可能会使应用程序失败。

您可以引入一个所有控制器必须扩展或实现的抽象基类或接口:

abstract class Controller {}

// e.g. controller for '?controller=admin'
class Admin extends Controller {}

顺便说一句,为避免名称冲突,您可以在单独的命名空间中定义这些内容。

这就是你如何强制执行这样的层次结构:

if ($controller !== null) {
    // load and use controller
    require_once("$controller.php");
    if (is_subclass_of($controller, 'Controller')) {
        $c = new $controller();
    }
}

我在实例化类之前使用is_subclass_of()来键入check。

自动加载

在这种情况下,您可以使用自动加载器代替使用require_once()

// register our custom auto loader
spl_autoload_register(function($class) {
    $file = "$class.php"; // admin -> admin.class.php
    if (file_exists($file)) {
        require_once $file; // this can be changed
    }
});

这也是您可以规范化类名称的地方,以便它更好地映射到文件名,以及强制执行自定义命名空间,例如: "App\\$class.php"

这会将代码减少一行,但会使加载更加灵活:

if ($controller !== null) {
    // check hierarchy (this will attempt auto loading)
    if (class_exists($controller) && is_subclass_of($controller, 'Controller')) {
        $c = new $controller();
    }
}

所有这些代码都假定您有适当的错误处理代码;有关实施建议,您可以查看this answer

答案 1 :(得分:3)

一些建议:

  • 将您的控制器类放在其自己的专用文件夹中,该文件夹仅包含控制器类
  • 尽可能严格地使用过滤器,例如

    /* is $_GET['controller'] set? */
    if (!isset($_GET['controller'])) {
        // load error or default controller???
    }
    
    $loadController = $_GET['controller'];
    
    /* replace any characters NOT matching a-z or _ (whitelist approach), case insensitive */
    $loadController = preg_replace('/[^a-z_]+/i', '', $loadController);
    
    /* verify var is not empty now :) */
    if (!$loadController) {
        // load error or default controller???
    }
    
    /* if your classes are named in a certain fashion, eg. "Classname", format the incoming text to match ** NEVER TRUST USER INPUT ** */
    $loadController = ucfirst(strtolower($loadController));
    
  • 检查文件是否存在Why not file_exists? see desc

    /* avoiding using file_exists as it also matches folders... */
    if (!is_file($myControllerClassesPath.$loadController.'.php')) {
        // load error or default controller???
    }
    
  • 然后需要该文件,并验证该类本身是否存在

    require($myControllerClassesPath.$loadController.'.php');
    
    /* of course, this assumes filename === classname, adjust accordingly */
    if (!class_exists($loadController)) {
        // load error or default controller???
    }
    
  • 然后当然是X的新实例

    new $loadController;
    

答案 2 :(得分:1)

大多数用户使用auto_load的变体代替包含,以使其更安全。但是提供的示例没有提到他们使用它的方式,auto_load只是一个奇特的包含。不是手动包含文件然后调用类,而是自动包含该文件。这没有任何安全优势,因为仍然可以调用任何可用的类。

在我的选择中使用include代替require,然后捕获错误是最佳做法,也是最容易实现的。为了确保安全,您必须在允许包含的文件名中添加额外的部分。 EG:“控制器”。现在,如果您有一个名为Home的类,则调用homeController.php文件。这样我们只能要求以'Controller.php'结尾的文件。

作为额外的预防措施,我在输入中添加了basename()以阻止Windows系统上的网络访问

<?php
//EG GET ?controller=home
$input = isset($_GET['controller']) ? $_GET['controller'] : "";
if (empty($input))
  die('No controller');

$input = basename($input);
$filename = $input.'Controller.php';

//since only valid files can be included, you dont need to check for valid chars or anything. Just make sure that only your controller files end with 'Controller.php'
//use the @ to hide the warning when the file does not exist
if ((@include $filename) !== 1)
  die('Unknown controller');

//no error, so we included a valid controller and now we can call it.
$controller = new $input();
?>

请记住,如果您在非Windows服务器上运行,则您的文件名区分大小写,而您的PHP类则不区分大小写。所以,如果有人进入controller = HOME,则包含将失败。

您可以通过使用小写类前缀来生成所有类似'homeController.php'的文件来防止此问题。然后,您可以使用$filename = strtolower($input).'Controller.php';

答案 3 :(得分:0)

考虑使用spl_autoload_register()。这样可以省去验证文件/类等的大量工作。

<?php
function autoloadClasses($class) {
    if (file_exists('core/'.$class.'.php')) {
        include 'core/'.$class . '.php';
    }
}
spl_autoload_register('autoloadClasses');

?>

然后在core-folder中保存文件名dart.php(文件名和类名必须相同)

然后创建对象时: new dart();该文件将在需要时包含在内。

有关此内容的更多信息: http://php.net/manual/en/function.spl-autoload-register.php

答案 4 :(得分:0)

如果您的类/文件很少,您可以在存储类的文件夹中获取所有php文件,并检查您要包含/要求的类是否是其中之一。

这样的事情:

$classDir = '/path/to/classes';
$classList = glob($classDir.'/*.php');
$classAbsolutePath = $classDir.'/'.$_GET['class'];

if (in_array($classAbsolutePath, $classList)) {
    require $classAbsolutePath;
}

如果您有子目录,则必须根据该目录修改此代码。顺便说一句,这不是关于性能的最佳解决方案,特别是如果你有很多文件和很多子目录。另外,in_array()效率不高,所以如果你有大数组,你应该避免它。

在我看来,做这样的事情的最好方法是拥有白名单。您可以通过代码自动生成它。每次重建或部署项目时,都可以重新生成列表,以便始终拥有有效的列表。

答案 5 :(得分:0)

我建议你在允许的文件中引入特殊标记。然后在包含文件之前,将其作为纯文本阅读并查找标记。只有标签存在时,才包含它。标记可以在允许文件开头的PHP注释内。

$class = $_GET['class'];
if (preg_match('/^[a-zA-Z]+$/', $class))
{
    $file = $class.".php";
    if (is_file($file)) {
    {
        $content = file_get_contents($file);
        if (strpos($content, "THECLASSMAGIC") !== false)
        {
            require($file);
        }
    }
    else
    {
        die(...);
    }
}
else
{
    die(...);
}

答案 6 :(得分:0)

首先添加此功能。

function __autoload ( $class ) {
     $path = "../path/to/class/dir/" . $class . TOKEN . ".php";
     if ( file_exists ($path) ) {
          require_once ( $path );
     } else {
          // class not found.
     }
}

然后只需访问类,

$class = new input();

它将检查文件"../path/to/class/dir/input_secretToken.php"是否存在,并自动包含它。

此处TOKEN是配置文件中定义的密码,用作所有类文件的后缀。因此,只会加载带有标记后缀的类文件。

答案 7 :(得分:0)

就安全性而言,从输入接受资源的标识符没有任何问题,无论是图像还是某些代码。但是,如果预期会有某种授权,那就不可避免地要避免某种授权(显然,获得授权是一种悖论,但却没有授权)。因此,如果您坚持不使用ACL(或称之为“白名单”),我必须说您想要的是不可能的。

另一方面,如果你可以接受ACL,那么剩下的就是直截了当。您需要做的就是将控制器视为资源并将用户分组为角色(最后一部分是可选的)。然后指定哪个角色或用户可以访问哪个控制器。以下是使用Zend Framework完成的操作。

$acl = new Zend_Acl();

$acl->addRole(new Zend_Acl_Role('guest'))
    ->addRole(new Zend_Acl_Role('member'))
    ->addRole(new Zend_Acl_Role('admin'));

$parents = array('guest', 'member', 'admin');
$acl->addRole(new Zend_Acl_Role('someUser'), $parents);

$acl->add(new Zend_Acl_Resource('someController'));

$acl->deny('guest', 'someController');
$acl->allow('member', 'someController');

然后,当某些请求到达时,您可以像下面这样质疑其授权:

if ($acl->isAllowed('currentUser', $_GET['controller'])) {
    $ctrlClass = $_GET['controller'];
    $controller = new $ctrlClass();
}

假设已经设置了一些自动加载器。

答案 8 :(得分:-1)

在什么情况下,您将允许用户通过查询字符串参数实例化控制器,但不知道他们实际尝试实例化的内容?听起来像灾难的食谱。

这样说,我只是将输入限制为字母(假设您的类名为 MyClass.php MyOtherClass.php 等)并锁定到特定目录

<?php

$className = $_GET['file'];
$dir = '/path/to/classes/';
$file = $dir . $className . '.php';

if (preg_match('/^[a-zA-Z]+$/', $className) && is_file($file)) {
    require($file);
    $class = new $className;
}
else {
    die('Class not found');
}