0, 'WARN' => 0, 'FAIL' => 0]; $FINDINGS = []; function add_result(string $level, string $title, string $detail = ''): void { global $SCORE, $FINDINGS; if (!isset($SCORE[$level])) { $level = 'WARN'; } $SCORE[$level]++; $FINDINGS[] = [$level, $title, $detail]; } function line(string $label, $value = null): void { if ($value === null) { echo $label . PHP_EOL; return; } if (is_bool($value)) { $value = $value ? 'YES' : 'NO'; } elseif (is_array($value) || is_object($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } echo str_pad($label, 46) . ': ' . $value . PHP_EOL; } function section(string $title): void { echo PHP_EOL . "--- {$title} ---" . PHP_EOL; } function bytes_from_ini(?string $val): ?int { if ($val === null || $val === '') return null; $val = trim($val); if ($val === '-1') return -1; $last = strtolower(substr($val, -1)); $num = (float)$val; return match($last) { 'g' => (int)($num * 1024 * 1024 * 1024), 'm' => (int)($num * 1024 * 1024), 'k' => (int)($num * 1024), default => (int)$num, }; } function with_error_capture(callable $fn): array { $error = null; set_error_handler(function($severity, $message) use (&$error) { $error = $message; return true; }); try { $result = $fn(); restore_error_handler(); return ['result' => $result, 'error' => $error]; } catch (Throwable $e) { restore_error_handler(); return ['result' => null, 'error' => $e->getMessage()]; } } function try_read_file(string $path): array { return with_error_capture(function() use ($path) { $data = @file_get_contents($path); if ($data === false) return false; return 'len=' . strlen($data); }) + ['path' => $path]; } function try_scandir_path(string $path): array { return with_error_capture(function() use ($path) { $data = @scandir($path); if ($data === false) return false; return array_slice($data, 0, 15); }) + ['path' => $path]; } function try_socket(string $target, int $port, float $timeout = 1.2): array { $errno = 0; $errstr = ''; $fp = @fsockopen($target, $port, $errno, $errstr, $timeout); $ok = is_resource($fp); if ($ok) fclose($fp); return [ 'target' => $target, 'port' => $port, 'connected' => $ok, 'errno' => $errno, 'errstr' => $errstr, ]; } function try_unix_socket(string $path, float $timeout = 1.0): array { $errno = 0; $errstr = ''; $fp = @stream_socket_client('unix://' . $path, $errno, $errstr, $timeout); $ok = is_resource($fp); if ($ok) fclose($fp); return [ 'path' => $path, 'connected' => $ok, 'errno' => $errno, 'errstr' => $errstr, ]; } echo "=== SHARED HOSTING SAFE AUDIT v2.1 ===" . PHP_EOL; echo "Time: " . date('c') . PHP_EOL; $docRoot = realpath($_SERVER['DOCUMENT_ROOT'] ?? getcwd()) ?: getcwd(); $scriptFile = $_SERVER['SCRIPT_FILENAME'] ?? __FILE__; $baseTmp = $docRoot . '/.audit_tmp_' . getmypid() . '_' . mt_rand(1000, 9999); @mkdir($baseTmp, 0700, true); section('Runtime identity'); line('PHP version', PHP_VERSION); line('SAPI', PHP_SAPI); line('OS', PHP_OS_FAMILY); line('Document root', $docRoot); line('Script filename', $scriptFile); line('Current dir', getcwd()); line('Loaded php.ini', php_ini_loaded_file() ?: 'none'); line('Additional .ini files', php_ini_scanned_files() ?: 'none'); line('Hostname', php_uname('n')); line('Server software', $_SERVER['SERVER_SOFTWARE'] ?? 'n/a'); line('Server addr', $_SERVER['SERVER_ADDR'] ?? 'n/a'); line('Server port', $_SERVER['SERVER_PORT'] ?? 'n/a'); line('Remote addr', $_SERVER['REMOTE_ADDR'] ?? 'n/a'); section('PHP limits and config'); $iniKeys = [ 'memory_limit', 'max_execution_time', 'max_input_time', 'max_input_vars', 'post_max_size', 'upload_max_filesize', 'open_basedir', 'disable_functions', 'disable_classes', 'user_ini.filename', 'user_ini.cache_ttl', 'file_uploads', 'allow_url_fopen', 'allow_url_include', 'session.save_path', 'upload_tmp_dir', 'sys_temp_dir', 'display_errors', 'log_errors', 'expose_php', 'mail.add_x_header', 'mysqli.default_socket', 'pdo_mysql.default_socket', ]; foreach ($iniKeys as $k) { line($k, ini_get($k)); } section('Dangerous functions present'); $dangerFns = [ 'exec','shell_exec','system','passthru','proc_open','popen', 'pcntl_exec','pcntl_fork','putenv','mail','symlink','link', 'stream_socket_client','fsockopen' ]; foreach ($dangerFns as $fn) { line("function_exists($fn)", function_exists($fn)); } section('Loaded extensions'); $exts = ['mysqli','pdo_mysql','redis','ftp','curl','openssl','pcntl','posix','sockets','imap','intl']; foreach ($exts as $ext) { line("extension_loaded($ext)", extension_loaded($ext)); } section('Filesystem isolation'); $fsTests = [ $docRoot, $docRoot . '/../', $docRoot . '/../../', '/var/www', '/var/www/vhosts', '/etc/passwd', '/etc/hosts', '/proc/self/environ', '/proc/meminfo', '/tmp', '/var/tmp', '/run', '/run/php', '/run/mysqld', '/dev/shm', '/var/spool/postfix', ]; foreach ($fsTests as $path) { $isDir = @is_dir($path); $result = $isDir ? try_scandir_path($path) : try_read_file($path); line($path, $result); } section('Path traversal / realpath checks'); $traversalTests = [ $docRoot . '/../www', $docRoot . '/../tmp', $docRoot . '/../session', $docRoot . '/../../../../etc/passwd', $docRoot . '/../other-vhost/www', ]; foreach ($traversalTests as $path) { line("realpath($path)", @realpath($path) ?: 'false'); line("read($path)", try_read_file($path)); } section('Allowed path write tests'); $writeFile = $baseTmp . '/write-test.txt'; $res = with_error_capture(function() use ($writeFile) { return file_put_contents($writeFile, "audit\n"); }); line('Write file in docroot tmp', ['path' => $writeFile] + $res); line('File exists after write', file_exists($writeFile)); line('File readable after write', is_readable($writeFile)); line('File writable after write', is_writable($writeFile)); $renameTo = $baseTmp . '/write-test-renamed.txt'; $res = with_error_capture(function() use ($writeFile, $renameTo) { return @rename($writeFile, $renameTo); }); line('Rename inside allowed path', ['from' => $writeFile, 'to' => $renameTo] + $res); $copyToSession = dirname($docRoot) . '/session/audit-copy.txt'; $res = with_error_capture(function() use ($renameTo, $copyToSession) { return @copy($renameTo, $copyToSession); }); line('Copy from docroot to session dir', ['to' => $copyToSession] + $res); section('Symlink / hardlink inside allowed path'); $src = $baseTmp . '/src.txt'; @file_put_contents($src, 'x'); $symlinkTarget = $baseTmp . '/sym.txt'; $hardlinkTarget = $baseTmp . '/hard.txt'; $symlinkRes = with_error_capture(fn() => @symlink($src, $symlinkTarget)); $hardlinkRes = with_error_capture(fn() => @link($src, $hardlinkTarget)); line('Symlink allowed path', $symlinkRes); line('Hardlink allowed path', $hardlinkRes); line('Symlink exists', is_link($symlinkTarget)); line('Hardlink exists', file_exists($hardlinkTarget)); section('Runtime override attempts'); $overrideTests = [ 'memory_limit' => '2048M', 'max_execution_time' => '600', 'upload_max_filesize' => '2048M', 'post_max_size' => '2048M', 'open_basedir' => '/', ]; $overrideResults = []; foreach ($overrideTests as $key => $value) { $before = ini_get($key); $ret = @ini_set($key, $value); $after = ini_get($key); $overrideResults[$key] = [ 'before' => $before, 'return' => $ret, 'after' => $after, 'changed' => ($before !== $after), ]; line("ini_set($key)", $overrideResults[$key]); } section('Execution capability tests'); $execResults = []; if (function_exists('exec')) { $execResults['exec'] = with_error_capture(function() { $out = []; $rc = 0; @exec('id 2>&1', $out, $rc); return ['rc' => $rc, 'out' => implode("\n", array_slice($out, 0, 5))]; }); line('exec("id")', $execResults['exec']); } if (function_exists('shell_exec')) { $execResults['shell_exec'] = with_error_capture(function() { $out = @shell_exec('whoami 2>&1'); return $out === null ? null : trim($out); }); line('shell_exec("whoami")', $execResults['shell_exec']); } if (function_exists('system')) { $execResults['system'] = with_error_capture(function() { ob_start(); $rc = 0; @system('pwd 2>&1', $rc); $out = ob_get_clean(); return ['rc' => $rc, 'out' => trim((string)$out)]; }); line('system("pwd")', $execResults['system']); } if (function_exists('proc_open')) { $execResults['proc_open'] = with_error_capture(function() { $desc = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $proc = @proc_open('id', $desc, $pipes); if (!is_resource($proc)) return 'proc_open failed'; fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); $code = proc_close($proc); return ['rc' => $code, 'stdout' => trim($stdout), 'stderr' => trim($stderr)]; }); line('proc_open("id")', $execResults['proc_open']); } if (function_exists('popen')) { $execResults['popen'] = with_error_capture(function() { $h = @popen('id 2>&1', 'r'); if (!is_resource($h)) return 'popen failed'; $out = stream_get_contents($h); $rc = pclose($h); return ['rc' => $rc, 'out' => trim((string)$out)]; }); line('popen("id")', $execResults['popen']); } section('Fork capability'); line('extension_loaded(pcntl)', extension_loaded('pcntl')); line('function_exists(pcntl_fork)', function_exists('pcntl_fork')); line('Active fork test', 'SKIPPED in web SAPI for safety'); section('Controlled memory probe'); $memoryLimit = bytes_from_ini(ini_get('memory_limit')); $chunks = []; $allocated = 0; $chunkSize = 4 * 1024 * 1024; $target = ($memoryLimit !== null && $memoryLimit > 0) ? (int)($memoryLimit * 0.70) : 64 * 1024 * 1024; $oom = false; $oomMsg = null; try { while ($allocated + $chunkSize <= $target) { $chunks[] = str_repeat('A', $chunkSize); $allocated += $chunkSize; } } catch (Throwable $e) { $oom = true; $oomMsg = $e->getMessage(); } line('memory_limit parsed', $memoryLimit); line('allocated safely', $allocated); line('oom caught', $oom); line('oom message', $oomMsg ?: 'none'); unset($chunks); section('Controlled CPU / timeout probe'); $start = microtime(true); $iterations = 0; $limitSeconds = max(1, (int)ini_get('max_execution_time')); $softBudget = min(3, max(1, $limitSeconds - 1)); while ((microtime(true) - $start) < $softBudget) { hash('sha256', random_bytes(256), false); $iterations++; } line('elapsed', round(microtime(true) - $start, 3) . 's'); line('iterations', $iterations); line('soft budget', $softBudget . 's'); section('set_time_limit test'); $setTimeLimitRes = with_error_capture(function() { return @set_time_limit(600); }); line('set_time_limit(600)', $setTimeLimitRes); line('max_execution_time after set_time_limit', ini_get('max_execution_time')); section('Network reachability'); $netTargets = [ ['127.0.0.1', 25], ['127.0.0.1', 3306], ['127.0.0.1', 6379], ['127.0.0.1', 11211], ['127.0.0.1', 7080], ['127.0.0.1', 80], ['127.0.0.1', 443], ]; $netResults = []; foreach ($netTargets as [$host, $port]) { $netResults["$host:$port"] = try_socket($host, $port); line("$host:$port", $netResults["$host:$port"]); } section('Unix socket reachability'); $unixCandidates = [ '/run/mysqld/mysqld.sock', '/var/run/mysqld/mysqld.sock', '/tmp/mysql.sock', '/run/redis/redis-server.sock', '/var/run/redis/redis.sock', '/tmp/redis.sock', '/usr/local/lsws/admin/tmp/admin.sock', ]; $unixResults = []; foreach ($unixCandidates as $sock) { $unixResults[$sock] = try_unix_socket($sock); line($sock, $unixResults[$sock]); } section('Stream wrappers'); $wrappers = stream_get_wrappers(); sort($wrappers); line('wrappers', $wrappers); $wrapperReads = [ 'php://memory', 'php://temp', 'data://text/plain;base64,SGVsbG8=', ]; foreach ($wrapperReads as $wrapper) { line("read $wrapper", with_error_capture(function() use ($wrapper) { $d = @file_get_contents($wrapper); if ($d === false) return false; return 'len=' . strlen($d) . ' data=' . substr($d, 0, 40); })); } section('Include wrapper tests'); $includeFile = $baseTmp . '/include-test.php'; file_put_contents($includeFile, " true, 'time' => time()];"); $includeLocal = with_error_capture(function() use ($includeFile) { return include $includeFile; }); $includeData = with_error_capture(function() { return @include 'data://text/plain;base64,PD9waHAgcmV0dXJuIFsiZGF0YSI9PnRydWVdOw=='; }); line('include local file', $includeLocal); line('include data:// wrapper', $includeData); section('Environment leakage'); $envKeys = ['HOME','USER','LOGNAME','PATH','TMPDIR','TEMP','HOSTNAME']; $envOut = []; foreach ($envKeys as $k) { $envOut[$k] = getenv($k); } line('getenv selected', $envOut); line('_ENV count', is_array($_ENV) ? count($_ENV) : 'n/a'); line('_SERVER selected', [ 'PATH' => $_SERVER['PATH'] ?? null, 'USER' => $_SERVER['USER'] ?? null, 'HOME' => $_SERVER['HOME'] ?? null, ]); section('Self-request capability'); $selfUrl = null; if (!empty($_SERVER['HTTP_HOST'])) { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $selfUrl = $scheme . '://' . $_SERVER['HTTP_HOST'] . ($_SERVER['REQUEST_URI'] ?? '/'); } line('self url', $selfUrl ?: 'n/a'); if ($selfUrl && function_exists('file_get_contents')) { line('self file_get_contents', with_error_capture(function() use ($selfUrl) { $ctx = stream_context_create(['http' => ['timeout' => 2]]); $data = @file_get_contents($selfUrl, false, $ctx); if ($data === false) return false; return 'len=' . strlen($data); })); } section('Session basics'); $sessionRes = with_error_capture(function() { if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); } $_SESSION['audit_test'] = 'ok'; return session_id(); }); line('session.save_path', ini_get('session.save_path')); line('session_start()', $sessionRes); line('session file expected', ini_get('session.save_path') . '/sess_' . session_id()); section('mail() basics'); line('function_exists(mail)', function_exists('mail')); line('sendmail_path', ini_get('sendmail_path')); line('mail() active send test', 'SKIPPED by design'); section('.user.ini verification hint'); $userIniFile = $docRoot . '/.user.ini.audit-test'; $userIniContent = << .user.ini, wait cache_ttl, reload script, compare values.'); section('Assessment'); $openBasedir = (string)ini_get('open_basedir'); $disableFunctions = (string)ini_get('disable_functions'); $userIni = (string)ini_get('user_ini.filename'); $allowUrlInclude = (string)ini_get('allow_url_include'); if ($openBasedir !== '') { add_result('PASS', 'open_basedir is set', $openBasedir); } else { add_result('FAIL', 'open_basedir is empty'); } if (!empty($overrideResults['memory_limit']['changed'])) { add_result('FAIL', 'memory_limit can be changed via ini_set()', json_encode($overrideResults['memory_limit'])); } else { add_result('PASS', 'memory_limit is not changeable via ini_set()'); } if (!empty($overrideResults['max_execution_time']['changed'])) { add_result('FAIL', 'max_execution_time can be changed via ini_set()', json_encode($overrideResults['max_execution_time'])); } else { add_result('PASS', 'max_execution_time is not changeable via ini_set()'); } if (!empty($overrideResults['upload_max_filesize']['changed'])) { add_result('FAIL', 'upload_max_filesize can be changed via ini_set()', json_encode($overrideResults['upload_max_filesize'])); } else { add_result('PASS', 'upload_max_filesize resisted ini_set()'); } if (!empty($overrideResults['post_max_size']['changed'])) { add_result('FAIL', 'post_max_size can be changed via ini_set()', json_encode($overrideResults['post_max_size'])); } else { add_result('PASS', 'post_max_size resisted ini_set()'); } if (!empty($overrideResults['open_basedir']['changed'])) { add_result('FAIL', 'open_basedir can be changed via ini_set()', json_encode($overrideResults['open_basedir'])); } else { add_result('PASS', 'open_basedir resisted ini_set()'); } $dangerousAvailable = []; foreach (['exec','shell_exec','system','passthru','proc_open','popen'] as $fn) { if (function_exists($fn) && !str_contains($disableFunctions, $fn)) { $dangerousAvailable[] = $fn; } } if ($dangerousAvailable) { add_result('FAIL', 'Command execution functions are available', implode(', ', $dangerousAvailable)); } else { add_result('PASS', 'Command execution functions are blocked'); } if (extension_loaded('pcntl')) { add_result('FAIL', 'pcntl extension is loaded', 'Not recommended for shared hosting web SAPI'); } else { add_result('PASS', 'pcntl extension is not loaded'); } if ($userIni === '' || strtolower($userIni) === 'none') { add_result('PASS', '.user.ini appears disabled'); } else { add_result('WARN', '.user.ini appears enabled', $userIni); } if ($allowUrlInclude === '' || $allowUrlInclude === '0') { add_result('PASS', 'allow_url_include is off'); } else { add_result('FAIL', 'allow_url_include is on'); } if (is_link($symlinkTarget)) { add_result('WARN', 'Symlink creation works inside allowed path', $symlinkTarget); } else { add_result('PASS', 'Symlink creation did not work'); } if (file_exists($hardlinkTarget)) { add_result('WARN', 'Hardlink creation works inside allowed path', $hardlinkTarget); } else { add_result('PASS', 'Hardlink creation did not work'); } $localhostSensitiveOpen = []; foreach (['127.0.0.1:25','127.0.0.1:3306','127.0.0.1:6379','127.0.0.1:7080'] as $k) { if (!empty($netResults[$k]['connected'])) { $localhostSensitiveOpen[] = $k; } } if ($localhostSensitiveOpen) { add_result('WARN', 'Sensitive localhost TCP ports reachable', implode(', ', $localhostSensitiveOpen)); } else { add_result('PASS', 'Sensitive localhost TCP ports not reachable'); } $reachableUnix = []; foreach ($unixResults as $sock => $res) { if (!empty($res['connected'])) { $reachableUnix[] = $sock; } } if ($reachableUnix) { add_result('WARN', 'Sensitive UNIX sockets reachable', implode(', ', $reachableUnix)); } else { add_result('PASS', 'Sensitive UNIX sockets not reachable'); } section('Score'); line('PASS', $SCORE['PASS']); line('WARN', $SCORE['WARN']); line('FAIL', $SCORE['FAIL']); section('Findings'); foreach ($FINDINGS as [$level, $title, $detail]) { echo '[' . $level . '] ' . $title; if ($detail !== '') { echo ' :: ' . $detail; } echo PHP_EOL; } section('Cleanup'); $cleanupFiles = [ $renameTo, $copyToSession, $src, $symlinkTarget, $hardlinkTarget, $includeFile, $userIniFile, ]; foreach ($cleanupFiles as $f) { if (is_link($f) || file_exists($f)) { @unlink($f); } } @rmdir($baseTmp); echo PHP_EOL . "Done." . PHP_EOL;