19

It's not safe to require $input . '.php'. To then institate a class. How can I make it secure, without needing to use say a whitelist of classes that can be inistiated.

Ex 1. (bad code).

<?php

$input = $_GET['controller'];

require $input . '.php';

new $input;

?>
John
  • 2,900
  • 8
  • 36
  • 65
  • block special characters like ../ , hard code ext to php , hard code folder like controllers ,class_exists, – JOE LEE Apr 19 '13 at 10:12
  • 2
    WHY do yo need to include a file in this manner? – bestprogrammerintheworld Apr 19 '13 at 10:12
  • 1
    +1 for clear/good question – bestprogrammerintheworld Apr 19 '13 at 10:16
  • 4
    Try to avoid this approach. You may take a look at [this](http://www.madirish.net/397) and [this](http://php.robm.me.uk/#toc-IncludingFiles). – The Alpha Apr 21 '13 at 17:43
  • I can understand that you don't want to whitelist the valid classes. But this seems to be the easiest and still most secure solution. I think simple `switch` statement might be Okay and allows you to avoid the risk on including any arbitrary class by mistake. I understand that this is not what you are asking but still suggest to consider this option. – Victor Smirnov Apr 22 '13 at 07:15
  • 2
    1) use autoloader, 2) don't use require/include at all, that should be safe. The reason I think so it that autoloader will try to load your file and since it will fail given the classname is not proper, the non-secure files will not be included at all. Hope this makes sense – Amit Kriplani Apr 23 '13 at 10:36
  • 2
    @bestprogrammerintheworld So one can claim to be employing OOP, MVC, design patterns, and other so-called "best practices" on a web site that's essentially just a bunch of files? – cleong Apr 25 '13 at 11:51
  • What do you really mean by secure? In which manner? I think you've got a lot of answers, but you haven't accepted any yet? – bestprogrammerintheworld Apr 27 '13 at 12:51

9 Answers9

13

Disclaimer

I should start by saying that defining static routes in your system is secure by design whereas this answer, even though I've made efforts to mitigate security issues, should be thoroughly tested and understood before trusting its operation.

The basics

First, make sure the controller contains a valid variable name using a regular expression as taken from the manual; this weeds out obvious erroneous entries:

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

Enforcing hierarchy

This works well, but what if someone tries to load an internal class instead? It might horribly fail the application.

You could introduce an abstract base class or interface that all your controllers must extend or implement:

abstract class Controller {}

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

Btw, to avoid name conflicts, you could define these inside a separate namespace.

And this is how you would enforce such a hierarchy:

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

I'm using is_subclass_of() to type check before instantiating the class.

Auto loading

Instead of using a require_once() in this case, you could use an auto loader instead:

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

This is also the place where you can normalize the class name, so that it maps better to a file name, as well as enforcing a custom namespace, e.g. "App\\$class.php".

This reduces the code by one line, but making the loading more flexible:

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

All this code assumes you have proper error handling code in place; for implementation suggestions you can look at this answer.

Community
  • 1
  • 1
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
3

A couple of suggestions:

  • Put your controller classes in its own dedicated folder, containing ONLY controller classes
  • Make your filter as strict as possible eg.

    /* 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));
    
  • Check if the file exists 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???
    }
    
  • Then require the file, and verify that the class itself exists

    require($myControllerClassesPath.$loadController.'.php');
    
    /* of course, this assumes filename === classname, adjust accordingly */
    if (!class_exists($loadController)) {
        // load error or default controller???
    }
    
  • Then of course, new instance of X

    new $loadController;
    
Jason
  • 2,035
  • 11
  • 13
1

Most anwsers are using a variation of auto_load instead of include to make it more secure. But the provided examples fail to mention that the way they use it, auto_load is just a fancy include. Instead of including the file manually and then calling the class, the file is included automatically. This offers no security advantage as one could still call any available class.

In my opion using include instead of require and then catching the error is the best practise and easiest to implement. To make it secure you must add an extra part to the filenames you allow to be included. EG: "Controller". Now if you have a class called Home, then you call the file homeController.php. This way we can only require files that end with 'Controller.php'.

Just as an extra precaution I added basename() to the input to prevent network access on windows systems

<?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();
?>

keep in mind that if you run on a none Windows server, your filenames are case-sensitive while your PHP classes are not. so if somebody would enter controller=HOME then the include would fail.

You could prevent this issue by makeing all files like 'homeController.php' with a lower case class prefix. Then you can use $filename = strtolower($input).'Controller.php';

Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72
0

Consider using spl_autoload_register(). This would you save a lot of effort in validating files/classes etc.

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

?>

Then save a file name dart.php in core-folder (filename and classname has to be the same)

When you then create an object: new dart(); the file will be included when needed.

More information about this: http://php.net/manual/en/function.spl-autoload-register.php

BlitZ
  • 12,038
  • 3
  • 49
  • 68
bestprogrammerintheworld
  • 5,417
  • 7
  • 43
  • 72
  • 2
    Please say WHY downvote? I can't see any problem with above. If someone can give me feedback I would really appreciate it. I'm not really the best programmer in the world, it's just an alias ;-) – bestprogrammerintheworld Apr 22 '13 at 17:34
  • I saw now that I had some typos. Was that the reason for downvote? – bestprogrammerintheworld Apr 22 '13 at 17:40
  • **@bestprogrammerintheworld**, I'm curious too, because my answer was downvoted with same manner. – BlitZ Apr 23 '13 at 03:06
  • @CORRUPT - I seriusly think that someone has a lot of reputation points they want to get rid of... I took a look at your answer as well, and I couldn't see any problem with it. Everyone who answered has downvotes.. – bestprogrammerintheworld Apr 23 '13 at 06:11
  • not all of them. It's inconstuctive behaviour of somebody. Still, I don't care. Just curious **"for what?!!11ONE!"** – BlitZ Apr 23 '13 at 06:14
  • @CORRUPT - ah ALMOST all of them. I don't get it either. But I guess life moves on ;-) – bestprogrammerintheworld Apr 23 '13 at 06:20
  • Yeah. This is on their conscious. Doesn't matter. – BlitZ Apr 23 '13 at 06:23
  • 1
    I didnt downvote your answer but I guess the problem is that it provides no security at all over `require` and that is exactly what was asked. What if i have a `Secure` class that I dont want people to access? If I provice secure as input, then the autoloader will check for a file called secure in the core folder and since it exists, it runs. This solution makes it possible to call ANY class available and thus it is no solution at all. Giving wrong security advise deserves downvotes. – Hugo Delsing Apr 26 '13 at 07:41
  • @HugoDelsing - ahhh good point! Thank you for a relevant answer!! :-) – bestprogrammerintheworld Apr 27 '13 at 12:41
0

If you have few classes/files, you can get all php files within the folder where classes are stored and check if the class you want to include/require is one of them.

So something like this:

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

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

If you have sub directories you have to modify this code according to that. By the way, this is not the best solution, regarding performances, especially if you have a lot of files and a lot of subdirectories. Also, in_array() is not very efficient so you should avoid it if you have big arrays.

In my opinion the best way to do something like this is to have a whitelist. You can generate it automatically via code. Every time you rebuild or deploy your project you can regenerate the list so that you will always have a valid one.

ulentini
  • 2,413
  • 1
  • 14
  • 26
  • Having "white" list? Then you end up having class map like `array('ClassName' => '/Path/To/That/Class.php')`. What if you have 25-60 classes? Then you need 25-50 items accordingly + iterate over white list per HTTP request. This is extremely inefficient. PSR-0 standard exists to address this problem. – Yang Apr 21 '13 at 12:57
  • @metal_fan PSR-0 is a standard, not an implementation. If you implement PSR-0 the wrong way you still could have security issues, since class names comes from user input. Also, a class map **doesn't** require any iteration, that's why it's more efficient than an algorithm with iterations. That said, I was just suggesting a solution, not *the* solution. – ulentini Apr 21 '13 at 13:57
  • What's the point of giving an answer that you don't think is the best solution? – bestprogrammerintheworld Apr 22 '13 at 17:33
  • @bestprogrammerintheworld Actually, I gave two different solutions. The first should be used in case of very few classes/controllers. The second if you have a lot of classes. – ulentini Apr 23 '13 at 06:26
  • @Uby - Ok! :-) Everyone is different but if I asked this question I would prefer an example of the best solution (with code) instead of the solution you supplied. – bestprogrammerintheworld Apr 23 '13 at 06:29
  • 1
    @bestprogrammerintheworld The *best solution* for which case? There's no mention about how many classes are involved, that's why I provided two different solutions. Also, writing code for class map generation without knowing anything about the project is pretty useless for the OP. – ulentini Apr 23 '13 at 06:37
  • @Uby - Ok, That's so true! :-) – bestprogrammerintheworld Apr 23 '13 at 06:38
0

I would suggest you to introduce special tag into allowed files. Then before you include the file, read it as plain text and look for the tag. Only if the tag is present, include it. The tag can be inside PHP comment at the beginning of allowed files.

$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(...);
}
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
0

First add this function.

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

Then simply access class,

$class = new input();

It will check if file "../path/to/class/dir/input_secretToken.php" exists, and include it automatically.

Here TOKEN is a secret word defined in config file, and used as suffix to all class files. So, only class file with token suffix will load.

user1995997
  • 581
  • 2
  • 8
  • 19
0

As far as security goes, there's nothing wrong with accepting resources' identifier from input, whether it's an image or some code. But it is inevitable to avoid some sort of authorization if one is expected (obviously it's a paradox to have authorization but not to have one). So if you insist on not having an ACL (or 'white list' as you call it) I have to say that what you want is not possible.

On the other hand, if you can come to terms with ACL, then the rest is straightforward. All you need to do is to see your controllers as resources and group your users into roles (this last part is optional). Then specify which role or user can access which controller. Here's how it's done using 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');

Then when some requests is arrived you can question its authorization simply like this:

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

Assuming that some autoloader is already set.

Mehran
  • 15,593
  • 27
  • 122
  • 221
-1

In what instance are you going to allow a user to instantiate a controller via a query string paramater, but not have an idea of what they’re actually trying to instantiate? Sounds like a recipe for disaster.

Saying that, I’d just restrict input to letters only (assuming your classes are named MyClass.php, MyOtherClass.php etc) and locked to a particular directory.

<?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');
}
Martin Bean
  • 38,379
  • 25
  • 128
  • 201
  • If people are going to down-vote my answer, at least have the decency to say _why_. @Ninsuo If you’re suggesting it’s possible to traverse directories with my above code, you’ll notice I’m using `preg_match()` to check if the supplied query string parameters is letters only. – Martin Bean Apr 26 '13 at 08:41
  • Didn't downvoted at least. Looks ok if this file isn't in the same directory as classes. – Alain Tiemblo Apr 26 '13 at 08:58