Верификация файлов в PHP: от file_exists до finfo
Основные методы проверки файлов
Наиболее эффективный способ проверки файла перед его использованием - комбинировать функции file_exists() и is_file(), а также учитывать права доступа с помощью is_readable() и is_writable(). Такой подход позволяет избежать ошибок при открытии, чтении или записи.
$filename = '/path/to/file.txt';
if (file_exists($filename) && is_file($filename)) {
// файл существует и является обычным файлом
if (is_readable($filename)) {
// можно читать
}
if (is_writable($filename)) {
// можно писать
}
}
Как проверить, существует ли файл и не является ли он директорией?
Как проверить, доступен ли файл для чтения или записи?
Для проверки прав доступа используются функции is_readable() и is_writable(). Они возвращают true, если у текущего процесса есть соответствующие разрешения.
$file = 'data.csv';
if (is_readable($file)) {
echo 'Файл доступен для чтения';
}
if (is_writable($file)) {
echo 'Файл доступен для записи';
}
Как определить MIME-тип файла (например, является ли он изображением)?
Для получения MIME-типа используйте расширение fileinfo и функцию finfo_file(). Это более надёжный способ, чем проверка расширения.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, 'image.jpg');
finfo_close($finfo);
if (str_starts_with($mime, 'image/')) {
echo 'Это изображение, тип: ' . $mime;
}
Как определить тип файла по его расширению?
Расширение извлекается с помощью pathinfo(). Этот метод полезен для быстрой фильтрации, но не является безопасным.
$path = 'document.pdf';
$ext = pathinfo($path, PATHINFO_EXTENSION);
$allowed = ['pdf', 'doc', 'docx'];
if (in_array($ext, $allowed)) {
echo 'Расширение разрешено';
}
Как проверить размер файла и убедиться, что он не превышает лимит?
Размер файла в байтах возвращает filesize(). Для проверки доступного места на диске подходит disk_free_space().
$filename = 'upload.zip';
$maxSize = 10 * 1024 * 1024; // 10 МБ
if (file_exists($filename) && filesize($filename) > $maxSize) {
echo 'Файл слишком большой (макс. 10 МБ)';
}
$free = disk_free_space('/');
if ($free < 100 * 1024 * 1024) {
echo 'Мало свободного места на диске';
}
Как проверить, когда файл был изменен или создан?
Время последнего изменения возвращает filemtime(). Для времени последнего доступа - fileatime(), для времени создания - filectime() (на самом деле это время изменения inode).
$filename = 'config.php';
$mtime = filemtime($filename);
if (time() - $mtime > 86400) {
echo 'Файл не изменялся более суток';
}
$ctime = filectime($filename);
echo 'Дата создания (inode): ' . date('Y-m-d H:i:s', $ctime);
Как обработать символические ссылки при проверке файлов?
Проверить, является ли путь символической ссылкой, можно с помощью is_link(). Чтобы получить реальный путь (без ссылок), используйте realpath().
$path = 'link_to_file';
if (is_link($path)) {
$target = readlink($path);
echo 'Ссылка ведёт на ' . $target;
}
$real = realpath($path);
if ($real !== false) {
echo 'Реальный путь: ' . $real;
}
Как проверить, что файл является PHP-скриптом?
Комбинация проверки расширения (.php), MIME-типа (text/x-php или application/x-php) и просмотр первых байтов на наличие открывающего тега <?. Надёжнее всего анализировать содержимое с помощью token_get_all() для парсинга.
$filename = 'script.php';
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$isPhp = false;
if ($ext === 'php') {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filename);
if ($mime === 'text/x-php' || $mime === 'application/x-httpd-php') {
$content = file_get_contents($filename, false, null, 0, 100);
if (str_contains($content, '<?')) {
$isPhp = true;
}
}
finfo_close($finfo);
}
echo $isPhp ? 'Это PHP-скрипт' : 'Не PHP-скрипт';
Расширенные примеры проверки файлов
Пример 1: Проверка загружаемого файла с комплексной валидацией
Функция проверяет расширение, MIME-тип, размер, а также наличие вредоносного содержимого путём поиска опасных конструкций в первых байтах.
function validateUploadedFile(string $filepath, array $allowedExtensions, array $allowedMimes, int $maxSize): array {
$result = ['valid' => false, 'error' => ''];
if (!file_exists($filepath) || !is_file($filepath)) {
$result['error'] = 'Файл не существует';
return $result;
}
$ext = pathinfo($filepath, PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), $allowedExtensions)) {
$result['error'] = 'Недопустимое расширение';
return $result;
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filepath);
finfo_close($finfo);
if (!in_array($mime, $allowedMimes)) {
$result['error'] = 'Недопустимый MIME-тип: ' . $mime;
return $result;
}
$size = filesize($filepath);
if ($size > $maxSize) {
$result['error'] = 'Файл превышает лимит в ' . $maxSize . ' байт';
return $result;
}
// Базовый поиск опасных конструкций (например, PHP-код)
$dangerousPatterns = ['<?', '
Вывод: Файл прошёл все проверки (если файл соответствует критериям) или Ошибка: ...
Для реальных проектов следует использовать более глубокий анализ, например, через библиотеки вроде PHP-Parser.
Пример 2: Рекурсивная проверка файлов в директории
Сценарий обходит все файлы в папке и проверяет их размеры, права доступа и расширения. Результат собирается в массив.
function checkDirectoryFiles(string $dir, int $maxSize = 10485760): array {
$result = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($iterator as $fileinfo) {
if ($fileinfo->isFile()) {
$path = $fileinfo->getPathname();
$size = $fileinfo->getSize();
$readable = $fileinfo->isReadable();
$writable = $fileinfo->isWritable();
$ext = $fileinfo->getExtension();
$problems = [];
if ($size > $maxSize) $problems[] = 'Размер превышает лимит';
if (!$readable) $problems[] = 'Нет прав на чтение';
if (!$writable) $problems[] = 'Нет прав на запись';
$result[] = [
'path' => $path,
'size' => $size,
'ext' => $ext,
'problems' => $problems
];
}
}
return $result;
}
$dir = '/var/www/uploads';
$checks = checkDirectoryFiles($dir);
foreach ($checks as $check) {
if (!empty($check['problems'])) {
echo $check['path'] . ': ' . implode(', ', $check['problems']) . "\n";
}
}
Пример вывода: /var/www/uploads/large.zip: Размер превышает лимит /var/www/uploads/protected.txt: Нет прав на чтение
Обратите внимание на использование RecursiveDirectoryIterator - это идиоматичный способ обхода директорий в PHP.
Пример 3: Проверка реального типа файла с помощью finfo и магических чисел
Иногда MIME-тип не даёт полной информации. Например, файл может быть переименован. Этот пример использует finfo_buffer для проверки сигнатур.
function detectRealFileType(string $filepath): ?string {
if (!is_file($filepath) || !is_readable($filepath)) {
return null;
}
$buffer = file_get_contents($filepath, false, null, 0, 20);
if ($buffer === false) {
return null;
}
$finfo = finfo_open(FILEINFO_EXTENSION); // возвращает расширение по сигнатуре
$extension = finfo_buffer($finfo, $buffer);
finfo_close($finfo);
return $extension ?: null;
}
$file = 'fake.pdf'; // на самом деле ZIP-архив
$realExt = detectRealFileType($file);
echo 'Реальное расширение: ' . ($realExt ?? 'неизвестно');
Реальное расширение: zip
Метод полезен для выявления попыток маскировки файлов под другой тип.
Пример 4: Работа с символическими ссылками и проверка целевого файла
Функция проверяет, является ли путь ссылкой, и если да - проверяет сам целевой файл. Также учитывает возможные циклические ссылки.
function checkResolvedFile(string $path): array {
$result = ['valid' => false, 'type' => 'unknown', 'realpath' => ''];
// Проверка на циклические ссылки
if (is_link($path)) {
$target = readlink($path);
$depth = 0;
while (is_link($target) && $depth < 10) {
$target = readlink($target);
$depth++;
}
if ($depth >= 10) {
$result['error'] = 'Обнаружена циклическая ссылка';
return $result;
}
$path = $target;
}
$realpath = realpath($path);
if ($realpath === false) {
$result['error'] = 'Целевой файл не существует';
return $result;
}
$result['realpath'] = $realpath;
$result['type'] = is_dir($realpath) ? 'directory' : (is_file($realpath) ? 'file' : 'other');
$result['valid'] = true;
return $result;
}
$link = '/home/user/link_to_config';
$info = checkResolvedFile($link);
if ($info['valid']) {
echo 'Реальный путь: ' . $info['realpath'] . ', тип: ' . $info['type'];
} else {
echo 'Ошибка: ' . $info['error'];
}
Пример вывода: Реальный путь: /etc/nginx/nginx.conf, тип: file
Важно ограничивать глубину разрешения ссылок, чтобы избежать бесконечного цикла.