19

要求 $input 是不安全的。'.php'。然后开课。我怎样才能使它安全,而不需要使用可以启动的类的白名单。

例 1.(错误代码)。

<?php

$input = $_GET['controller'];

require $input . '.php';

new $input;

?>
4

9 回答 9

13

免责声明

首先我应该说,在您的系统中定义静态路由在设计上是安全的,而这个答案,即使我已经努力缓解安全问题,在信任它的操作之前应该进行彻底的测试和理解。

基础知识

首先,使用手册中的正则表达式确保控制器包含有效的变量名;这消除了明显的错误条目:

$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()用来在实例化类之前输入检查。

自动加载

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

// 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();
    }
}

所有这些代码都假定您有适当的错误处理代码;对于实施建议,您可以查看此答案

于 2013-04-22T03:15:31.627 回答
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));
    
  • 检查文件是否存在为什么不存在 file_exists?见描述

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

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

    new $loadController;
    
于 2013-04-21T11:57:13.457 回答
1

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

在我看来,使用include而不是require然后捕获错误是最佳实践并且最容易实现。为了使其安全,您必须在允许包含的文件名中添加额外的部分。例如:“控制器”。现在,如果您有一个名为 的类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';

于 2013-04-26T07:32:23.410 回答
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();
}

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

于 2013-04-26T13:55:44.510 回答
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

于 2013-04-19T10:32:00.390 回答
0

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

所以是这样的:

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

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

如果您有子目录,则必须根据该目录修改此代码。顺便说一句,就性能而言,这不是最好的解决方案,尤其是在您有很多文件和很多子目录的情况下。此外,in_array()效率不是很高,所以如果你有大数组,你应该避免它。

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

于 2013-04-21T12:23:13.263 回答
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(...);
}
于 2013-04-21T12:24:46.707 回答
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是配置文件中定义的一个秘密词,用作所有类文件的后缀。因此,只会加载带有标记后缀的类文件。

于 2013-04-22T03:29:44.347 回答
-1

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

话虽如此,我只是将输入限制为仅字母(假设您的类名为MyClass.phpMyOtherClass.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');
}
于 2013-04-21T12:08:23.407 回答