要求 $input 是不安全的。'.php'。然后开课。我怎样才能使它安全,而不需要使用可以启动的类的白名单。
例 1.(错误代码)。
<?php
$input = $_GET['controller'];
require $input . '.php';
new $input;
?>
要求 $input 是不安全的。'.php'。然后开课。我怎样才能使它安全,而不需要使用可以启动的类的白名单。
例 1.(错误代码)。
<?php
$input = $_GET['controller'];
require $input . '.php';
new $input;
?>
免责声明
首先我应该说,在您的系统中定义静态路由在设计上是安全的,而这个答案,即使我已经努力缓解安全问题,在信任它的操作之前应该进行彻底的测试和理解。
基础知识
首先,使用手册中的正则表达式确保控制器包含有效的变量名;这消除了明显的错误条目:
$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();
}
}
所有这些代码都假定您有适当的错误处理代码;对于实施建议,您可以查看此答案。
几个建议:
使您的过滤器尽可能严格,例如。
/* 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;
大多数 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';
就安全性而言,从输入中接受资源标识符没有任何问题,无论是图像还是某些代码。但是,如果需要某种授权,则不可避免地要避免某种授权(显然,有授权但没有授权是一个悖论)。因此,如果您坚持没有 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();
}
假设已经设置了一些自动加载器。
考虑使用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
如果您的类/文件很少,您可以获取存储类的文件夹中的所有 php 文件,并检查您想要包含/需要的类是否是其中之一。
所以是这样的:
$classDir = '/path/to/classes';
$classList = glob($classDir.'/*.php');
$classAbsolutePath = $classDir.'/'.$_GET['class'];
if (in_array($classAbsolutePath, $classList)) {
require $classAbsolutePath;
}
如果您有子目录,则必须根据该目录修改此代码。顺便说一句,就性能而言,这不是最好的解决方案,尤其是在您有很多文件和很多子目录的情况下。此外,in_array()
效率不是很高,所以如果你有大数组,你应该避免它。
在我看来,做这样的事情的最好方法是有一个白名单。您可以通过代码自动生成它。每次重建或部署项目时,您都可以重新生成列表,以便始终拥有有效的列表。
我建议您在允许的文件中引入特殊标签。然后在包含文件之前,将其作为纯文本读取并查找标签。仅当标签存在时,才包含它。该标签可以在允许文件开头的 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(...);
}
首先添加这个功能。
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
是配置文件中定义的一个秘密词,用作所有类文件的后缀。因此,只会加载带有标记后缀的类文件。
在什么情况下,您将允许用户通过查询字符串参数实例化控制器,但不知道他们实际尝试实例化的是什么?听起来像是灾难的秘诀。
话虽如此,我只是将输入限制为仅字母(假设您的类名为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');
}