简体   繁体   中英

Securing paths in PHP

I'm writing some PHP which takes some paths to different content directories, and uses these to include various parts of pages later. I'm trying to ensure that the paths are as they seem, and none of them break the rules of the application. Which are,

  1. PRIVATEDIR (defined relative to DOCUMENT_ROOT ) must be above DOCUMENT_ROOT .
  2. CONTENTDIR (defined relative to PRIVATEDIR ) must lie below PRIVATEDIR and must not go back in to DOCUMENT_ROOT .
  3. The remaining *DIRS 's (defined relative to CONTENTDIR ) must lie below CONTENTDIR

I'm setting some defaults in a singleton controller class, and then the user passes in an array of the paths they want to override to this classes constructor. I then want to sanity check them to ensure they abide by the above rules. Heres how I've started to go about it...

EDIT: Please note my use of error_reporting in the code below, and then don't do it yourself! I misunderstood how that command works. If you're wondering why, see stealthyninja's and Col. Shrapnel's remarks in the comments (and thanks to them for pointing this out to me).

private $opts = array( // defaults
   'PRIVATEDIR'   => '..',        // relative to document root
   'CONTENTDIR'   => 'content',   // relative to private dir
   ...
   ...
);

private function __construct($options) { //$options is the user defined options
    error_reporting(0);
    if(is_array($options)) {
        $this->opts = array_merge($this->opts, $options);
    }

    if($this->opts['STATUS']==='debug') {
        error_reporting(E_ALL | E_NOTICE | E_STRICT);
    }

    $this->opts['PUBLICDIR']  = realpath($_SERVER['DOCUMENT_ROOT'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['PRIVATEDIR'] = realpath($this->opts['PUBLICDIR']
                                        .$this->opts['PRIVATEDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['CONTENTDIR'] = realpath($this->opts['PRIVATEDIR']
                                        .$this->opts['CONTENTDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['CACHEDIR']   = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['CACHEDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['ERRORDIR']   = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['ERRORDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['TEMPLATEDIR' = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['TEMPLATEDIR'])
                                        .DIRECTORY_SEPARATOR;

   ...
   ...
   ...


    // then here I have to check that PRIVATEDIR is above PUBLICDIR
    // and that all the rest remain within private dir and don't drop 
    // down into (or below) PUBLICDIR again. And die with an error if
    // they don't conform.
}

The thing is this seems like a lot of work to do, especially as it must be run, every time a page is accessed, before I can do anything else (eg check for a cached version of the page I'm serving), when the paths are essentially static.

Part of me is thinking that the maintainer (currently me) of the site should be aware of what paths they are supplying and should check themselves that they conform to the rules. But the (I think) more sensible side of me is saying, no, I should be equally responsible for checking that the paths conform, since accidents do happen (especially as the site grows/changes/gets a new maintainer...) and if they can be caught, in a security-critical environment, there is no excuse not to catch them.

So, I'm pretty much decided that I DO want to check these paths, the question is how good is my method? It feels like a suboptimal solution. How would you improve it or what alternatives would you suggest? Is there anyway of getting round the problem of having to do it every time a page loads, and somehow just do it when the configuration changes? (That would be ideal, but I don't imagine trivial).

Thanks.

How about using strpos to find a substring? It's not the most elegant solution, but if you just want some basic protection for someone who already is given a lot of power on the site. Here is how I would do it:

// PRIVATEDIR already includes PUBLICDIR at this point
if(strpos($this->opts['PRIVATEDIR'],$this->opts['PUBLICDIR']) !== 0) {
die('ERROR: '.$this->opts['PRIVATEDIR'].' must be located in .'$this->opts['PUBLICDIR']);
}

Note the triple not equals, that makes sure that PUBLICDIR occurs at position 0 of the PRIVATEDIR string and not it simply being not found or somewhere else in the string. Also you should check that they aren't the same (PUBLICDIR != PRIVATEDIR) since you could set it to "./test/../" and that would return you to just ".".

I would just leave it to the site admin to decide where the directories should be, and provide the above as convenient defaults. One might want to use a common error or temp directory for all sites on the server, or put the private dir inside the public one and defend it via .htaccess (bad practice, but might be necessary on some cheap webhosts).

Also, what is the point of array_merging in the constructor? If the $opts array cannot get a value before that point, then it is confusing to one reading the code and an error when E_STRICT is enabled; if it has a default value or something, then changing it like the way you do is misleading (if someone sees $opts = array('TEMPLATEDIR'=>'/templates') in your class definition and then finds a reference to $config->$opts['TEMPLATEDIR'] somewhere in the code, he will expect it to be /templates ); you should either use full paths in the options in the first place, or hold the full paths in a different variable.

If the objective is to ensure that the code stays in the right directories to look for files, why do you use realpath() without checking that your still within the constrained directory? Or are you not concerned about any of these paths containing '../'?

I would infer that at least CACHEDIR is writeable - but is within the document root - this is not a good idea from a security viewpoint.

Also, you are re-writing existing variables, rather than constructing new variables from template values - even though you're doing this in the constructor, this is a rather messy approach for values which really should be final as soon as they are declared.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM