Back to InputFilter class

Method isSafeFile

public static bool
isSafeFile
(mixed $file, mixed $options = array())
Checks an uploaded for suspicious naming and potential PHP contents which could indicate a hacking attempt.
Parameters
  • array $file An uploaded file descriptor
  • array $options The scanner options (see the code for details)
Returns
  • bool True of the file is safe
Since
  • 3.4
Class: InputFilter
Project: Joomla

Method isSafeFile - Source code

/**
 * Checks an uploaded for suspicious naming and potential PHP contents which could indicate a hacking attempt.
 *
 * The options you can define are:
 * null_byte                   Prevent files with a null byte in their name (buffer overflow attack)
 * forbidden_extensions        Do not allow these strings anywhere in the file's extension
 * php_tag_in_content          Do not allow `<?php` tag in content
 * phar_stub_in_content        Do not allow the `__HALT_COMPILER()` phar stub in content
 * shorttag_in_content         Do not allow short tag `<?` in content
 * shorttag_extensions         Which file extensions to scan for short tags in content
 * fobidden_ext_in_content     Do not allow forbidden_extensions anywhere in content
 * php_ext_content_extensions  Which file extensions to scan for .php in content
 *
 * This code is an adaptation and improvement of Admin Tools' UploadShield feature,
 * relicensed and contributed by its author.
 *
 * @param   array  $file     An uploaded file descriptor
 * @param   array  $options  The scanner options (see the code for details)
 *
 * @return  boolean  True of the file is safe
 *
 * @since   3.4
 */
public static function isSafeFile($file, $options = array())
{
    $defaultOptions = array(
        // Null byte in file name
        'null_byte' => true,
        // Forbidden string in extension (e.g. php matched .php, .xxx.php, .php.xxx and so on)
        'forbidden_extensions' => self::FORBIDDEN_FILE_EXTENSIONS,
        // <?php tag in file contents
        'php_tag_in_content' => true,
        // <? tag in file contents
        'shorttag_in_content' => true,
        // __HALT_COMPILER()
        'phar_stub_in_content' => true,
        // Which file extensions to scan for short tags
        'shorttag_extensions' => array('inc', 'phps', 'class', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8', 'txt', 'dat', 'tpl', 'tmpl'),
        // Forbidden extensions anywhere in the content
        'fobidden_ext_in_content' => true,
        // Which file extensions to scan for .php in the content
        'php_ext_content_extensions' => array('zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa'),
    );
    $options = array_merge($defaultOptions, $options);
    // Make sure we can scan nested file descriptors
    $descriptors = $file;
    if (isset($file['name']) && isset($file['tmp_name'])) {
        $descriptors = static::decodeFileData(array($file['name'], $file['type'], $file['tmp_name'], $file['error'], $file['size']));
    }
    // Handle non-nested descriptors (single files)
    if (isset($descriptors['name'])) {
        $descriptors = array($descriptors);
    }
    // Scan all descriptors detected
    foreach ($descriptors as $fileDescriptor) {
        if (!isset($fileDescriptor['name'])) {
            // This is a nested descriptor. We have to recurse.
            if (!static::isSafeFile($fileDescriptor, $options)) {
                return false;
            }
            continue;
        }
        $tempNames = $fileDescriptor['tmp_name'];
        $intendedNames = $fileDescriptor['name'];
        if (!\is_array($tempNames)) {
            $tempNames = array($tempNames);
        }
        if (!\is_array($intendedNames)) {
            $intendedNames = array($intendedNames);
        }
        $len = \count($tempNames);
        for ($i = 0; $i < $len; $i++) {
            $tempName = array_shift($tempNames);
            $intendedName = array_shift($intendedNames);
            // 1. Null byte check
            if ($options['null_byte']) {
                if (strstr($intendedName, "\x00")) {
                    return false;
                }
            }
            // 2. PHP-in-extension check (.php, .php.xxx[.yyy[.zzz[...]]], .xxx[.yyy[.zzz[...]]].php)
            if (!empty($options['forbidden_extensions'])) {
                $explodedName = explode('.', $intendedName);
                $explodedName = array_reverse($explodedName);
                array_pop($explodedName);
                $explodedName = array_map('strtolower', $explodedName);
                /*
                 * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
                 * be set, i.e. they should have unique values.
                 */
                foreach ($options['forbidden_extensions'] as $ext) {
                    if (\in_array($ext, $explodedName)) {
                        return false;
                    }
                }
            }
            // 3. File contents scanner (PHP tag in file contents)
            if ($options['php_tag_in_content'] || $options['shorttag_in_content'] || $options['phar_stub_in_content'] || $options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions'])) {
                $fp = strlen($tempName) ? @fopen($tempName, 'r') : false;
                if ($fp !== false) {
                    $data = '';
                    while (!feof($fp)) {
                        $data .= @fread($fp, 131072);
                        if ($options['php_tag_in_content'] && stripos($data, '<?php') !== false) {
                            return false;
                        }
                        if ($options['phar_stub_in_content'] && stripos($data, '__HALT_COMPILER()') !== false) {
                            return false;
                        }
                        if ($options['shorttag_in_content']) {
                            $suspiciousExtensions = $options['shorttag_extensions'];
                            if (empty($suspiciousExtensions)) {
                                $suspiciousExtensions = array('inc', 'phps', 'class', 'php3', 'php4', 'txt', 'dat', 'tpl', 'tmpl');
                            }
                            /*
                             * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
                             * be set, i.e. they should have unique values.
                             */
                            $collide = false;
                            foreach ($suspiciousExtensions as $ext) {
                                if (\in_array($ext, $explodedName)) {
                                    $collide = true;
                                    break;
                                }
                            }
                            if ($collide) {
                                // These are suspicious text files which may have the short tag (<?) in them
                                if (strstr($data, '<?')) {
                                    return false;
                                }
                            }
                        }
                        if ($options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions'])) {
                            $suspiciousExtensions = $options['php_ext_content_extensions'];
                            if (empty($suspiciousExtensions)) {
                                $suspiciousExtensions = array('zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa');
                            }
                            /*
                             * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
                             * be set, i.e. they should have unique values.
                             */
                            $collide = false;
                            foreach ($suspiciousExtensions as $ext) {
                                if (\in_array($ext, $explodedName)) {
                                    $collide = true;
                                    break;
                                }
                            }
                            if ($collide) {
                                /*
                                 * These are suspicious text files which may have an executable
                                 * file extension in them
                                 */
                                foreach ($options['forbidden_extensions'] as $ext) {
                                    if (strstr($data, '.' . $ext)) {
                                        return false;
                                    }
                                }
                            }
                        }
                        /*
                         * This makes sure that we don't accidentally skip a <?php tag if it's across
                         * a read boundary, even on multibyte strings
                         */
                        $data = substr($data, -10);
                    }
                    fclose($fp);
                }
            }
        }
    }
    return true;
}