You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

736 lines
19 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
// analyse_file_path groups files by type, e.g. library, etc.
// Usage:
// From Tiki root, run:
// php doc/devtools/securitycheck.php > 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 "<!-- Found $path in " . join(",", $regex_possibles) . "-->\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');
?>
<html>
<head><title>Security Static Checker Output</title></head>
<body>
<p>Tiki Version: <?php echo "$major.$minor.$revision" ?></p>
<p>Audit Date: <?php echo date('Y-m-d H:i:s') ?></p>
<h1>Potentially unsafe files</h1>
<p>
To be safe, files must have either an include only check, block web access, have a feature check or have a
permission check. </p>
<ol>
<?php foreach ($unsafe as $unsafeUrlAndFile) :
$pathname = $unsafeUrlAndFile['path'];
$url = substr($unsafeUrlAndFile['path'], 2);
$fileRecord = $filesHash[$pathname];
$fileType = $fileRecord['type'];
?>
<li>
<?php echo $fileType; ?>
<a href="<?php echo htmlentities($url) ?>"><?php echo htmlentities($pathname) ?></a>
</li>
<?php endforeach; ?>
</ol>
<h1>All files</h1>
<table border="1">
<thead>
<tr style="font-size:x-small">
<th>File</th>
<th>Include only check</th>
<th>Not web accessible</th>
<th>Includes tiki-setup</th>
<th>Unsafe extract</th>
<th>Permissions checked</th>
<th>Features checked</th>
</tr>
</thead>
<tbody>
<?php
foreach ($files as $file) {
if (in_array($file['type'], ['script', 'module', 'include', 'public', 'lib', '3rdparty', 'wikiplugin'])) : ?>
<tr>
<td><a href="<?php echo htmlentities(substr($file['path'], 2)) ?>"><?php echo htmlentities($file['path']) ?></a></td>
<td>
<?php
if (isset($file['includeonly']) && $file['includeonly']) {
echo 'X';
}
?>
</td>
<td>
<?php
if ($file['noweb']) {
echo 'X';
}
?>
</td>
<td>
<?php
if ($file['tikisetup']) {
echo 'X';
}
?>
</td>
<td>
<?php
if ($file['unsafeextract']) {
echo 'X';
}
?>
</td>
<td>
<?php foreach ($file['permissions'] as $perm) : ?>
<div><?php echo $perm ?></div>
<?php endforeach; ?>
</td>
<td>
<?php foreach ($file['features'] as $feature) : ?>
<div><?php echo $feature ?></div>
<?php endforeach; ?>
</td>
</tr>
<?php endif;
};
?>
</tbody>
</table>
<?php
foreach ($features as $featureKey => $featureValue) {
print "$featureKey :\n";
foreach ($featureValue as $file) {
print "<li>$file</li>";
}
print "<br/><br/>\n";
} ?>
</body>
</html>
<!-- If you see this in your terminal window it's because you didn't read the usage. See the start of the file. -->