securityreport.html // visit securityreport.html (where your Tiki is) // // // Related script: doc/devtools/prefreport.php // if (isset($_SERVER['REQUEST_METHOD'])) { die; } // Add the imported libraries located in lib/ $thirdpartyLibs = [ '\./lib.*', /* as per NKO 4:18 19-MAY-09 */ /* jb 110715 Tiki 7.1 - so everything in lib is protected by the .htaccess file, right? */ ]; /* The following need to be added as features FIX LATER (FIXED < 7.1?) ./tiki-login_openid.php The following do actually have features, but the fix check checker needs to be changed to accept access->check_permissions() so that also that it loads tikisetup.php ./tiki-orphan_pages.php ./tiki-plugins.php ./tiki-switch_perspective.php */ $safePaths = [ /* Not in build */ '\./doc/devtools/.*', '\./db/local.php', '\./db/virtuals.inc', /* The following are DELIBERATELY PUBLIC. */ '\./tiki-cookie-jar.php', '\./tiki-error_simple.php', '\./tiki-information.php', '\./tiki-install.php', // does its own check '\./tiki-live_support_chat_frame.php', '\./tiki-login_scr.php', '\./tiki-channel.php', // does its own checks /* This file is just comments */ './about.php', /* The following need to be refactored to a lib */ '\./tiki-testGD.php', /* vendor and vendor_bundled dirs, not tiki files*/ '\./vendor_bundled/*', '\./vendor/*', ]; if (! file_exists('tiki-setup.php')) { die("Please run this script from tiki root.\n"); } include_once('lib/setup/twversion.class.php'); $TWV = new TWVersion(); if (! $TWV->version) { die("Could not find version information.\n"); } $ver = explode('.', $TWV->version); $major = (count($ver) >= 1) ? $ver[0] : '?'; $minor = (count($ver) >= 2) ? $ver[1] : '?'; $revision = (count($ver) >= 3) ? $ver[2] : '?'; /** * @param $filename * @return bool|string */ function get_content($filename) { static $last, $content; if ($filename == $last) { return $content; } $content = file_get_contents($last = $filename); return $content; } /** * @param $featureNameIndex * @return string */ function feature_pattern(&$featureNameIndex) // {{{ { global $major, $minor, $revision; $featureName = "((feature_\w+)|lang_use_db|allowRegister|validateUsers|cachepages)"; $q = "[\"']"; if ($major == 1 && $minor == 9) { $featureNameIndex = [2, 7]; $tl = '\\$tikilib->get_preference'; return "/(\\\${$featureName}\s*(!=|==)=?\s*$q(y|n)[\"'])|($tl\s*\(\s*$q{$featureName}$q\s*(,\s*{$q}n?$q)?\s*\)\s*(==|!=)=?\s*$q(y|n)$q)/"; } elseif (($major == 1 && $minor == 10) || $major >= 2) { $featureNameIndex = 1; return "/\\\$prefs\s*\[$q(\w+)$q\]\s*(!=|==)=?\s*$q(y|n)$q/"; } } // }}} /** * @param $permissionNameIndex * @return string */ function permission_pattern(&$permissionNameIndex) // {{{ { global $major, $minor, $revision; $permissionNameIndex = 1; return "/\\$(tiki_p_\w+)\s*(!=|==)=?\s*[\"'](y|n)[\"']/"; } // }}} /** * @return string */ function includeonly_pattern() // {{{ { return "/strpos\s*\(\s*\\\$_SERVER\s*\[\s*[\"']SCRIPT_NAME[\"']\s*\]\s*,\s*basename\s*\(\s*__FILE__\s*\)\s*\)\s*!==\s*(false|FALSE)/"; } // }}} /** * @return string */ function includeonly_pattern3() // {{{ { return "/basename\s*\(\s*\\\$_SERVER\s*\[\s*[\"']SCRIPT_NAME[\"']\s*\]\s*\)\s*===?\s*basename\s*\(\s*__FILE__\s*\)\s*\)/"; } // }}} /** * @return string */ function includeonly_pattern2() // {{{ { return "/\\\$access\s*->\s*check_script\s*\(\s*\\\$_SERVER\s*\[\s*[\"']SCRIPT_NAME[\"']\s*\]\s*,\s*basename\s*\(\s*__FILE__\s*\)\s*\)/s"; } // }}} /** * @return string */ function noweb_pattern() // {{{ { return "/if\s*\(\s*isset\s*\(\s*\\\$_SERVER\[\s*[\"']REQUEST_METHOD[\"']\]\s*\)\s*\)\s*die/"; } // }}} /** * @return string */ function tikisetup_pattern() // {{{ { return "/(require(_once)?|include(_once)?)\s*\(?\s*['\"]tiki-setup.php['\"]/"; } // }}} /** * @param $folder * @param $files */ function scanfiles($folder, &$files) // {{{ { global $filesHash; $handle = opendir($folder); if (! $handle) { printf("Could not open folder: %s\n", $folder); return; } while (false !== $file = readdir($handle)) { // Skip self and parent if ($file[0] == '.' || $file[0] == '..') { continue; } $path = "$folder/$file"; if (is_dir($path)) { scanfiles($path, $files); } else { $analysis = analyse_file_path($path); $files[] = $analysis; $filesHash[$path] = $analysis; } } } // }}} // TODO This is an inefficient function, but more flexible than in_array /** * @param $path * @param $regex_possibles * @return bool */ function regex_match($path, $regex_possibles) { foreach ($regex_possibles as $possible) { // print "Matching $path against $possible\n"; if (preg_match('%' . $possible . '%', $path)) { //print "Matches $possible\n\n"; print "\n"; return true; } } return false; } /** * @param $path * @return array */ function analyse_file_path($path) // {{{ { global $thirdpartyLibs, $safePaths; $type = 'unknown'; $name = basename($path); if (strpos($name, '.') !== false) { $extension = substr($name, strrpos($name, '.') + 1); } else { $extension = false; } if (strpos($path, '/CVS/') !== false) { $type = 'cvs'; } elseif (strpos($path, './temp/templates_c/') === 0) { $type = 'cache'; } elseif (regex_match($path, $safePaths)) { $type = 'safe'; } elseif ($extension == 'php' || $extension == 'inc') { if ($name == 'index.php') { $type = 'blocker'; } elseif ($name == 'language.php') { $type = 'lang'; } elseif (strpos($path, './lib/wiki-plugins') === 0) { $type = 'wikiplugin'; } elseif (strpos($path, './lib/') === 0) { if (regex_match($path, $thirdpartyLibs)) { $type = '3dparty'; } else { $type = 'lib'; } } elseif (strpos($path, './tiki-') === 0) { $type = 'public'; } elseif (strpos($path, './modules/') === 0) { $type = 'module'; } else { $type = "include"; } } elseif (in_array($extension, ['txt', 'png', 'jpg', 'html', 'css', 'sql', 'gif', 'afm', 'js'])) { $type = 'static'; } elseif (strpos($path, './doc/devtools/') === 0) { $type = 'script'; } elseif (strpos($path, './files/') === 0) { $type = 'user'; } elseif ($extension == 'sh') { $type = 'system'; } elseif (strpos($path, '_htaccess') !== false) { $type = 'system'; } elseif (in_array(basename($path), ['INSTALL', 'README'])) { $type = 'doc'; } elseif ($extension == 'tpl') { $type = 'template'; } return [ 'filename' => basename($path), 'path' => $path, 'type' => $type, 'extension' => $extension, 'features' => [], 'permissions' => [], 'includeonce' => false, 'noweb' => false, 'tikisetup' => false, 'unsafeextract' => false, ]; } // }}} /** * @param $file * @return array */ function perform_feature_check(&$file) // {{{ { global $features; $index = []; $feature_pattern = feature_pattern($index); $index = (array)$index; $path = $file['path']; preg_match_all($feature_pattern, get_content($path), $parts); $featuresInFile = []; foreach ($index as $i) { $featuresInFile = array_merge($features, $parts[$i]); } $featuresInFile = array_merge($featuresInFile, access_check_call($path, 'check_feature')); $featuresInFile = array_unique($featuresInFile); $file['features'] = $featuresInFile; // var_dump($featuresInFile); /* This data structure seems to be typical, and very confusing. An array of 3, with the zeroth element being a named element whose value is an array of one element. other elements being named, not numbered 1array(3) { 2 ["feature_directory"]=> 3 array(1) { 4 [0]=> 5 string(28) "./tiki-directory_ranking.php" 6 } 7 [0]=> 8 string(18) "feature_html_pages" 9 [1]=> 10 string(21) "feature_theme_control" 11} */ /* // store, for each feature, which files are involved foreach ($featuresInFile as $feature) { if (is_string($feature)) { if (preg_match('/feature/', $feature)) { // SMELL sure to be a better way to do this. //print "Listing as feature $feature\n"; $featuresListed = (array) $features[$feature]; array_push($featuresListed, $path); $features[$feature] = $featuresListed; } // TODO SMELL: this regex should not be necessary, it should only contain features at this point. // SMELL: it will also miss some vital elements. } } */ return $featuresInFile; } // }}} /** * @param $file */ function perform_permission_check(&$file) // {{{ { $index = 0; $permission_pattern = permission_pattern($index); preg_match_all($permission_pattern, get_content($file['path']), $parts); $permissions = array_unique( array_merge( access_check_call($file['path'], 'check_permission'), permission_check_accessors($file['path']), $parts[$index] ) ); $file['permissions'] = $permissions; } // }}} /** * @param $file */ function perform_includeonly_check(&$file) // {{{ { $index = 0; $pattern = includeonly_pattern($index); preg_match_all($pattern, get_content($file['path']), $parts); $pattern = includeonly_pattern2($index); preg_match_all($pattern, get_content($file['path']), $parts2); $pattern = includeonly_pattern3($index); preg_match_all($pattern, get_content($file['path']), $parts3); $file['includeonly'] = count($parts[0]) > 0 || count($parts2[0]) > 0 || count($parts3[0]) > 0; } // }}} /** * @param $file */ function perform_noweb_check(&$file) // {{{ { $index = 0; $pattern = noweb_pattern($index); preg_match_all($pattern, get_content($file['path']), $parts); $file['noweb'] = count($parts[0]) > 0; } // }}} /** * @param $file */ function perform_tikisetup_check(&$file) // {{{ { $index = 0; $pattern = tikisetup_pattern($index); preg_match_all($pattern, get_content($file['path']), $parts); $file['tikisetup'] = count($parts[0]) > 0; } // }}} /** * @param $file */ function perform_extract_skip_check(&$file) // {{{ { $pattern = "/extract\s*\([^\)]+\)/"; preg_match_all($pattern, get_content($file['path']), $parts); foreach ($parts[0] as $extract) { if (strpos($extract, 'EXTR_SKIP') === false) { $file['unsafeextract'] = true; } } } // }}} /** * @param $file * @param $type * @return array */ function access_check_call($file, $type) // {{{ { $content = get_content($file); $tokens = token_get_all($content); $checks = []; foreach ($tokens as $key => $token) { if (is_array($token)) { if ($token[0] == T_VARIABLE && $token[1] == '$access') { if ( $tokens[$key + 1][0] == T_OBJECT_OPERATOR && $tokens[$key + 2][0] == T_STRING && $tokens[$key + 2][1] == $type ) { $checks = array_merge($checks, access_checks($tokens, $key + 2)); } } } } return $checks; } // }}} /** * @param $tokens * @param $from * @return array */ function access_checks($tokens, $from) // {{{ { $end = count($tokens); $features = []; for ($i = $from; $end > $i; ++$i) { $token = $tokens[$i]; if (is_string($token) && $token == ';') { break; } if (is_array($token) && $token[0] == T_CONSTANT_ENCAPSED_STRING) { $features[] = trim($token[1], "\"'"); } } return $features; } // }}} /** * @param $file * @return array */ function permission_check_accessors($file) // {{{ { $tokens = token_get_all(get_content($file)); $perms = []; foreach ($tokens as $key => $token) { if (is_array($token) && ($token[0] == T_IF || $token[0] == T_ELSEIF)) { $subset = tokenizer_get_subset($tokens, $key); $perms = array_merge($perms, permission_check_condition($subset)); } } return $perms; } // }}} /** * @param $tokens * @param $from * @return array */ function tokenizer_get_subset($tokens, $from) // {{{ { $out = []; $started = false; $count = 0; $end = count($tokens); for ($i = $from; $end > $i && (! $started || $count > 0); ++$i) { $t = $tokens[$i]; if (is_string($t)) { if ($t == '(') { $started = true; $count++; } elseif ($t == ')') { $count--; } } $out[] = $t; } return $out; } // }}} /** * @param $tokens * @return array */ function permission_check_condition($tokens) // {{{ { $permissions = []; foreach ($tokens as $i => $t) { if ($t[0] == T_VARIABLE) { if ('perms' == substr($t[1], -5)) { if ($tokens[$i + 1][0] == T_OBJECT_OPERATOR && $tokens[$i + 2][0] == T_STRING) { $perm = $tokens[$i + 2][1]; if ('tiki_p_' != substr($perm, 0, 7)) { $perm = 'tiki_p_' . $perm; } $permissions[] = $perm; } } } } return $permissions; } // }}} /* Build Files structures */ // a hash of filenames, each element is a hash of attributes of that file $filesHash = []; // a hash of features, each element is a hash of filenames that use that feature $features = []; // note: the files[0..N] is intended to be replaced by the above hash. $files = []; // build these two files structures scanfiles('.', $files); error_reporting(E_ALL); /* Iterate each file, and perform checks */ $unsafe = []; foreach ($files as $key => $dummy) { $file = &$files[$key]; switch ($file['type']) { case 'wikiplugin': perform_extract_skip_check($file); if ($file['unsafeextract']) { $unsafe[] = $file; } break; case 'public': case 'include': case 'script': case 'module': case 'lib': case '3rdparty': perform_feature_check($file); perform_permission_check($file); perform_includeonly_check($file); perform_noweb_check($file); perform_tikisetup_check($file); if ( ! $file['noweb'] && ! $file['includeonly'] && ! count($file['features']) && ! count($file['permissions']) ) { $unsafe[] = $file; } break; } } /** * @param $a * @param $b * @return int */ function sort_cb($a, $b) { return strcmp($a['path'], $b['path']); } usort($files, 'sort_cb'); usort($unsafe, 'sort_cb'); ?>
Tiki Version:
Audit Date:
To be safe, files must have either an include only check, block web access, have a feature check or have a permission check.
| File | Include only check | Not web accessible | Includes tiki-setup | Unsafe extract | Permissions checked | Features checked |
|---|---|---|---|---|---|---|