Hide Document Parts:
Images
   Digital Media
Forms
QR Code
Output Options:
Print Document
Download PDF
Article
Comments

Trusting user input in PHP’s chmod: decimal vs. octal

The PHP manual on chmod has a subtle little warning about providing a decimal number instead of an octal when calling the chmod function:

Mode is not automatically assumed to be an octal value, so to ensure the expected operation, you need to prefix mode with a zero (0).

So what does this warning really mean and more importantly what is a programmer to do when allowing users to input the mode themselves? Besides shouting out never allow the user to do this! and calling it case closed, I would like to show you what you can do to solve this problem and a legitimate reason why you might use this solution in your application.

For the Hive Learning Management System security is of the utmost importance to me. One of the many things I’ve done to insure that best security practices are being followed or at least implemented is to develop a secure function that not only checks and changes directory and file permissions to safe levels for access over the internet, it also adds permanent redirects that push users attempting direct access to directories they shouldn’t be in back to the sites root. This obviously is not the best security practice but if a server admin forgets to setup certain settings like disabling directory listing or mis-configures settings like an htacess file it can offer them a little bit of protection. Every time one of the redirects is triggered it will be logged and if to many redirects are being noticed, either based on time, amount, or both we can escalate a warning to system admins making sure they realize they need to review their security settings.

Well the secure function is not covered in this post the above example is my legitimate need for a function that can accept a user supplied permission level and check that its valid for use. Well my application handles making sure a set permission level is used at a minimum there may be times that stricter permissions are desired. Since I have the function already in place that can recursively handle locking the file system down I just expose parameters that allow you to provide your own needed levels. As of the time of writing this I do not allow changing ownership but that could be added in the future. If you would like to see what the code looked like for the secure function as I tested and built it, have a look at the next section, otherwise skip ahead.

My Test Code

This is the last working version of my test code before it was cleaned up and put into an actual function. The $test variable is hard coded and can be changed to any valid permission level in decimal or octal. The code would then correct any mistakes caused by using a decimal or a mis-typed octal and then this value is checked to match what is in the last if statement; of course you need to update the if statement every time you change $test.

$test = 0755;

if(isOctal($test)){
    echo 'GIVEN OCTAL of ';
    $test = sprintf('%o', $test);
    echo $test.'<br>';
    if(chmodInRange($test)){
        echo 'SAFE<br>';
    } else {
        echo 'NOT SAFE<br>';
    }
} else {
    echo 'NOT GIVEN OCTAL<br>';
    if(strlen($test)==4){
        $test = '0'.$test;
        echo 'T1: '.$test.'<br>';
    } elseif(strlen($test)<4){
        if($test[0]==0){
            $test = '0'.sprintf('%o', $test);
            echo 'T2: '.$test.'<br>';
        } else {
            $test = '0'.$test;
            echo 'T3: '.$test.'<br>';
        }
    }
    if(chmodInRange($test)){
        echo 'SAFE<br>';
    } else {
        echo 'NOT SAFE<br>';
    }
}

if(forceOct($test)==0755){
    echo 'TRUE';
} else {
    echo 'FALSE';
}

Back to my Point

Being able to validate and enforce what a user can provide to PHP’s built in chmod function is extremely important. On one hand user error can cause terrible accidents as pmichaud pointed out in a comment on the PHP manual, paraphrased below:

/** Correct usage of chmod. */
chmod("file", 01777);

/** 
* Incorrect use of chmod! 
* This revokes all permissions from the owner!
*/
chmod("file", 1777); // The compiler sees: chmod("file",01023);

And on the other hand malicious users can try injecting malformed octals to either wreak havoc on your file system or break into it. To solve both issues I first created some helper functions:

/** Convert a perceived octal to a decimal and then back to check if it really is an octal. */
function isOctal($x) {
    return decoct(octdec($x)) == $x;
}
/** Catch any fake octal chmod permissions. */
function chmodInRange($mod){
    /** Make sure all indexes are set or bail. */
    if(!isset($mod[0]) || !isset($mod[1]) || !isset($mod[2]) || !isset($mod[3])){ return false; }
    /** Make sure everything is in range or bail. */
    if($mod[0]<0 || $mod[0]>7){ return false; }
    if($mod[1]<1 || $mod[0]>7){ return false; } // Owner must have permissions!
    if($mod[2]<0 || $mod[0]>7){ return false; }
    if($mod[3]<0 || $mod[0]>7){ return false; }
    return true;
}

The first function uses PHP’s built in decoct and octdec to tell me if I really have a valid octal on my hands, if so I’ll need to tackle checking it a little differently than a decimal or string. As you’ll see in a minute I end up turning whatever I’m given into a string because its easier and safer to check the users input that way. The second function actually takes advantage of this and picks apart a permission string (the mode) making sure three important things are happening:

  1. I have a full mode with all 4 parts not just 3.
  2. Each part is within the valid range.
  3. Your not accidentally (trying?) locking out the file owner.

Before I call this function I make sure to add on the leading zero that may be missing from the octal value and then convert it to its decimal value which if converted from a valid octal will be in the range of 0100 to 7777. Putting this all together now, the two functions above and my test code, I finally get my forceOctal function that will attempt to convert any number your supply it to a valid octal for the chmod function. If it can’t or if it detected an invalid mode you’ll get NULL back:

/**
* Make sure a user supplied mode (permission level) is valid.
* @author Christopher Keers <cdkeers@caboodle.tech>
* @param {number} permission The mode you wish to use for a chmod call.
* @return {octal|null} The permission in a valid octal format or null if invalid permission.
*/
function forceOctal($permission){
    /** If the permission looks like an octal handle it differently. */
    if(isOctal($permission)){
        $permission = sprintf('%o', $permission);
        /** Any octal can make it in here so make sure it's safe and valid. */
        if(!chmodInRange($permission)){ return null; }
    } else {
        /** See if the user provided a decimal thinking it was octal. */
        if(strlen($permission)==4){
            $permission = '0'.$permission;
        } elseif(strlen($permission)<4){
            /** Make sure they gave us an octal. */
            if($permission[0]==0){
                /** This was a correct octal. */
                $permission = '0'.sprintf('%o', $permission);
            } else {
                /** We turned the decimal to octal. */
                $permission = '0'.$permission;
            }
        }
        /** Any octal can make it in here so make sure it's safe and valid. */
        if(!chmodInRange($permission)){ return null; }
    }
    /** We checked the octal as a string turn it back into a true octal. */
    $mode = 0;
    switch($permission[0]){
        case '1': $mode += 01000; break;
        case '2': $mode += 02000; break;
        case '3': $mode += 03000; break;
        case '4': $mode += 04000; break;
        case '5': $mode += 05000; break;
        case '6': $mode += 06000; break;
        case '7': $mode += 07000; break;
    }
    switch($permission[1]){
        case '1': $mode += 0100; break;
        case '2': $mode += 0200; break;
        case '3': $mode += 0300; break;
        case '4': $mode += 0400; break;
        case '5': $mode += 0500; break;
        case '6': $mode += 0600; break;
        case '7': $mode += 0700; break;
    }
    switch($permission[2]){
        case '1': $mode += 010; break;
        case '2': $mode += 020; break;
        case '3': $mode += 030; break;
        case '4': $mode += 040; break;
        case '5': $mode += 050; break;
        case '6': $mode += 060; break;
        case '7': $mode += 070; break;
    }
    switch($permission[3]){
        case '1': $mode += 01; break;
        case '2': $mode += 02; break;
        case '3': $mode += 03; break;
        case '4': $mode += 04; break;
        case '5': $mode += 05; break;
        case '6': $mode += 06; break;
        case '7': $mode += 07; break;
    }
    return($mode);
}
Comments are currently disabled. Please check back later.
Close
Categories