<?php
// SECURE: File management with proper validation and safe operations
session_start();
// CSRF token generation and validation
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
class SecureFileManager {
private const ALLOWED_OPERATIONS = ['view', 'search', 'info'];
private const SAFE_DIRECTORY = '/secure/uploads';
private const MAX_FILE_SIZE = 1024 * 1024; // 1MB
private const ALLOWED_EXTENSIONS = ['.txt', '.csv', '.json', '.xml', '.log'];
public function handleRequest(): void {
try {
// CSRF protection
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->validateCSRFToken();
}
$operation = $this->getValidatedOperation();
$result = $this->executeOperation($operation);
$this->sendResponse($result);
} catch (Exception $e) {
error_log('File operation error: ' . $e->getMessage());
$this->sendErrorResponse('Operation failed');
}
}
private function validateCSRFToken(): void {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
throw new Exception('CSRF token validation failed');
}
}
private function getValidatedOperation(): string {
$operation = $_REQUEST['operation'] ?? '';
if (!in_array($operation, self::ALLOWED_OPERATIONS, true)) {
throw new Exception('Operation not allowed');
}
return $operation;
}
private function executeOperation(string $operation): array {
switch ($operation) {
case 'view':
return $this->viewFile();
case 'search':
return $this->searchFiles();
case 'info':
return $this->getFileInfo();
default:
throw new Exception('Unknown operation');
}
}
// SECURE: Native PHP file viewing
private function viewFile(): array {
$filename = $_REQUEST['filename'] ?? '';
$filePath = $this->validateAndResolvePath($filename);
// Check file size limit
if (filesize($filePath) > self::MAX_FILE_SIZE) {
throw new Exception('File too large to display');
}
// SECURE: Native PHP file reading
$content = file_get_contents($filePath);
if ($content === false) {
throw new Exception('Failed to read file');
}
// Validate content is text
if (!mb_check_encoding($content, 'UTF-8')) {
throw new Exception('File contains binary data');
}
return [
'operation' => 'view',
'filename' => basename($filePath),
'content' => htmlspecialchars($content, ENT_QUOTES, 'UTF-8'),
'size' => filesize($filePath)
];
}
// SECURE: Native PHP directory search
private function searchFiles(): array {
$pattern = $_REQUEST['pattern'] ?? '';
// Validate search pattern
if (!$this->isValidSearchPattern($pattern)) {
throw new Exception('Invalid search pattern');
}
$results = [];
$iterator = new DirectoryIterator(self::SAFE_DIRECTORY);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$filename = $file->getFilename();
// Use fnmatch for safe pattern matching
if (fnmatch($pattern, $filename)) {
$results[] = [
'name' => $filename,
'size' => $file->getSize(),
'modified' => date('Y-m-d H:i:s', $file->getMTime())
];
}
}
return [
'operation' => 'search',
'pattern' => htmlspecialchars($pattern),
'results' => $results,
'count' => count($results)
];
}
// SECURE: Native PHP file information
private function getFileInfo(): array {
$filename = $_REQUEST['filename'] ?? '';
$filePath = $this->validateAndResolvePath($filename);
$stat = stat($filePath);
$pathInfo = pathinfo($filePath);
return [
'operation' => 'info',
'filename' => $pathInfo['basename'],
'extension' => $pathInfo['extension'] ?? '',
'size' => $stat['size'],
'size_human' => $this->formatBytes($stat['size']),
'modified' => date('Y-m-d H:i:s', $stat['mtime']),
'permissions' => substr(sprintf('%o', fileperms($filePath)), -4),
'mime_type' => mime_content_type($filePath) ?: 'unknown',
'is_readable' => is_readable($filePath),
'is_writable' => is_writable($filePath)
];
}
private function validateAndResolvePath(string $filename): string {
// Validate filename format
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
throw new Exception('Invalid filename format');
}
if (strlen($filename) > 255) {
throw new Exception('Filename too long');
}
// Check file extension
$extension = '.' . strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
throw new Exception('File extension not allowed');
}
// Resolve full path
$fullPath = realpath(self::SAFE_DIRECTORY . '/' . $filename);
// Ensure file is within safe directory
if (!$fullPath || !str_starts_with($fullPath, realpath(self::SAFE_DIRECTORY))) {
throw new Exception('File not in allowed directory');
}
// Check file exists and is readable
if (!is_file($fullPath) || !is_readable($fullPath)) {
throw new Exception('File not found or not readable');
}
return $fullPath;
}
private function isValidSearchPattern(string $pattern): bool {
// Allow only alphanumeric, wildcards, dots, hyphens, underscores
return preg_match('/^[a-zA-Z0-9*?._-]+$/', $pattern) &&
strlen($pattern) > 0 &&
strlen($pattern) <= 50;
}
private function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
private function sendResponse(array $data): void {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'data' => $data]);
}
private function sendErrorResponse(string $message): void {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['success' => false, 'error' => $message]);
}
}
// Handle AJAX requests
if (isset($_REQUEST['ajax'])) {
$manager = new SecureFileManager();
$manager->handleRequest();
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Secure File Manager</title>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.form-group { margin: 10px 0; }
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 4px; }
.error { background: #ffebee; color: #c62828; }
pre { background: #f8f8f8; padding: 10px; overflow-x: auto; }
button { padding: 8px 16px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; }
input[type="text"] { padding: 6px; border: 1px solid #ddd; border-radius: 4px; }
</style>
</head>
<body>
<h1>Secure File Manager</h1>
<div class="form-group">
<h2>View File</h2>
<input type="text" id="viewFilename" placeholder="Enter filename (e.g., document.txt)">
<button onclick="performOperation('view')">View File</button>
</div>
<div class="form-group">
<h2>Search Files</h2>
<input type="text" id="searchPattern" placeholder="Search pattern (e.g., *.txt)">
<button onclick="performOperation('search')">Search Files</button>
</div>
<div class="form-group">
<h2>File Information</h2>
<input type="text" id="infoFilename" placeholder="Enter filename">
<button onclick="performOperation('info')">Get Info</button>
</div>
<div id="result"></div>
<script>
function performOperation(operation) {
const resultDiv = document.getElementById('result');
let params = new URLSearchParams({
ajax: '1',
operation: operation,
csrf_token: '<?php echo $_SESSION['csrf_token']; ?>'
});
// Add operation-specific parameters
if (operation === 'view') {
params.append('filename', document.getElementById('viewFilename').value);
} else if (operation === 'search') {
params.append('pattern', document.getElementById('searchPattern').value);
} else if (operation === 'info') {
params.append('filename', document.getElementById('infoFilename').value);
}
fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayResult(data.data);
} else {
displayError(data.error || 'Operation failed');
}
})
.catch(error => {
displayError('Network error: ' + error.message);
});
}
function displayResult(data) {
const resultDiv = document.getElementById('result');
resultDiv.className = 'result';
if (data.operation === 'view') {
resultDiv.innerHTML = `
<h3>File: ${data.filename} (${data.size} bytes)</h3>
<pre>${data.content}</pre>
`;
} else if (data.operation === 'search') {
let html = `<h3>Search Results for "${data.pattern}" (${data.count} files)</h3>`;
if (data.results.length > 0) {
html += '<ul>';
data.results.forEach(file => {
html += `<li>${file.name} - ${file.size} bytes - Modified: ${file.modified}</li>`;
});
html += '</ul>';
} else {
html += '<p>No files found.</p>';
}
resultDiv.innerHTML = html;
} else if (data.operation === 'info') {
resultDiv.innerHTML = `
<h3>File Information: ${data.filename}</h3>
<ul>
<li><strong>Size:</strong> ${data.size_human}</li>
<li><strong>Extension:</strong> ${data.extension}</li>
<li><strong>Modified:</strong> ${data.modified}</li>
<li><strong>Permissions:</strong> ${data.permissions}</li>
<li><strong>MIME Type:</strong> ${data.mime_type}</li>
<li><strong>Readable:</strong> ${data.is_readable ? 'Yes' : 'No'}</li>
<li><strong>Writable:</strong> ${data.is_writable ? 'Yes' : 'No'}</li>
</ul>
`;
}
}
function displayError(message) {
const resultDiv = document.getElementById('result');
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>Error:</strong> ${message}`;
}
</script>
</body>
</html>