Создание класса на основе ввода пользователем
Небезопасно требовать $input. '.php'. Затем создать класс. Как я могу сделать это безопасным, без необходимости использовать белый список классов, которые могут быть проинформированы.
Пример 1. (неправильный код).
<?php
$input = $_GET['controller'];
require $input . '.php';
new $input;
?>
Ответы
Ответ 1
Отказ
Я должен начать с того, что определение статических маршрутов в вашей системе является безопасным по дизайну, тогда как этот ответ, даже если я прилагал усилия для смягчения проблем безопасности, должен быть тщательно протестирован и понят, прежде чем доверять его работе.
Основы
Сначала убедитесь, что контроллер содержит допустимое имя переменной, используя регулярное выражение, взятое из руководство; это уничтожает очевидные ошибочные записи:
$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 {}
Btw, чтобы избежать конфликтов имен, вы можете определить их внутри отдельного пространства имен.
И именно так вы будете применять такую иерархию:
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()
в этом случае вместо этого вы можете использовать автоматический загрузчик:
// 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();
}
}
Весь этот код предполагает, что у вас есть правильный код обработки ошибок; для рекомендаций по реализации вы можете посмотреть этот ответ.
Ответ 2
Несколько предложений:
- Поместите классы контроллера в свою собственную выделенную папку, содержащую ТОЛЬКО классы контроллера
-
Сделайте свой фильтр как можно более строгим, например.
/* 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));
-
Проверьте, существует ли файл Почему не файл_exists? см. ниже
/* 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;
Ответ 3
Большинство пользователей используют вариант 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 не являются. поэтому, если кто-то войдет в контроллер = HOME, то включение будет терпеть неудачу.
Вы можете предотвратить эту проблему, выполнив все файлы типа homeController.php с префиксом нижнего регистра. Затем вы можете использовать $filename = strtolower($input).'Controller.php';
Ответ 4
Рассмотрим использование spl_autoload_register()
. Это поможет вам сэкономить много усилий при проверке файлов/классов и т.д.
<?php
function autoloadClasses($class) {
if (file_exists('core/'.$class.'.php')) {
include 'core/'.$class . '.php';
}
}
spl_autoload_register('autoloadClasses');
?>
Затем сохраните имя файла dart.php
в основной папке (имя файла и имя класса должны быть одинаковыми)
Когда вы затем создаете объект:
new dart();
файл будет включен, если необходимо.
Дополнительная информация:
http://php.net/manual/en/function.spl-autoload-register.php
Ответ 5
Если у вас мало классов/файлов, вы можете получить все php файлы в папке, где хранятся классы, и проверить, является ли класс, который вы хотите включить/требовать, одним из них.
Так что-то вроде этого:
$classDir = '/path/to/classes';
$classList = glob($classDir.'/*.php');
$classAbsolutePath = $classDir.'/'.$_GET['class'];
if (in_array($classAbsolutePath, $classList)) {
require $classAbsolutePath;
}
Если у вас есть подкаталоги, вам необходимо изменить этот код в соответствии с этим. Кстати, это не лучшее решение, касающееся выступлений, особенно если у вас много файлов и много подкаталогов. Кроме того, in_array()
не очень эффективен, поэтому вам следует избегать его, если у вас большие массивы.
На мой взгляд, лучший способ сделать что-то вроде этого - иметь белый список. Вы можете автоматически генерировать его с помощью кода. Каждый раз, когда вы перестраиваете или развертываете свой проект, вы можете восстановить список, чтобы у вас всегда был действующий.
Ответ 6
Я бы предложил вам ввести специальный тег в разрешенные файлы. Затем перед включением файла прочитайте его как обычный текст и найдите тег. Только если тег присутствует, включите его. Тег может быть внутри комментария 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(...);
}
Ответ 7
Сначала добавьте эту функцию.
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
- секретное слово, определенное в файле конфигурации и используемое как суффикс для всех файлов классов. Таким образом, будет загружен только файл класса с суффиксом токена.
Ответ 8
Вы можете использовать spl_autoload_register()
function my_autoload($className) {
$phpFolders = array('models', 'controllers');
foreach($phpFolders as $folder) {
if(file_exists($folder . '/' . $className . '.php')) {
require_once $folder . '/' . $className . '.php';
}
}
}
spl_autoload_register('my_autoload');
$input = $_GET['controller'];
new $input();
Ответ 9
Что касается безопасности, нет ничего плохого в принятии идентификатора ресурсов от ввода, будь то изображение или какой-то код. Но неизбежно избегать какой-то авторизации, если ее можно ожидать (очевидно, это парадокс, чтобы иметь разрешение, но не иметь его). Поэтому, если вы настаиваете на отсутствии 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();
}
Предположим, что уже установлен один автозагрузчик.
Ответ 10
В каком экземпляре вы собираетесь разрешить пользователю создавать экземпляр контроллера через шаблон строки запроса, но не иметь представления о том, что они на самом деле пытаются создать? Звучит как рецепт катастрофы.
Говоря, что Id ограничивает ввод только письмами (предполагается, что ваши классы называются 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');
}