Статьи

Контроль целостности файлов

Спросите себя, как вы можете учесть следующие обстоятельства при управлении веб-сайтом:

  • Файл случайно добавлен, изменен или удален
  • Файл злонамеренно добавлен, изменен или удален
  • Файл поврежден

Что еще более важно, вы даже знаете, произошло ли одно из этих обстоятельств? Если ваш ответ отрицательный, то продолжайте читать. В этом руководстве я покажу, как создать профиль вашей файловой структуры, который можно использовать для контроля целостности ваших файлов.

Лучший способ определить, был ли файл изменен, — это хэшировать его содержимое. В PHP доступно несколько функций хеширования, но для этого проекта я решил использовать hash_file() . Он предоставляет широкий спектр различных алгоритмов хеширования, которые позволят позже легко изменить мой код, если я решу внести изменения.

Хеширование используется в самых разных приложениях, начиная от защиты паролем и заканчивая секвенированием ДНК. Алгоритм хеширования работает путем преобразования данных в повторяющуюся криптографическую строку фиксированного размера. Они разработаны таким образом, что даже небольшая модификация данных должна привести к совершенно другому результату. Когда два или более разных фрагмента данных выдают одну и ту же строку результата, это называется «столкновением». Сила каждого алгоритма хеширования может быть измерена как по его скорости, так и по вероятности столкновений.

В моих примерах я буду использовать алгоритм SHA-1, потому что он быстрый, вероятность столкновений мала, и он широко используется и хорошо проверен. Конечно, вы можете исследовать другие алгоритмы и использовать любой, который вам нравится.

Как только хеш файла получен, его можно сохранить для последующего сравнения. Если последующее хеширование файла не возвращает ту же хеш-строку, что и раньше, мы знаем, что файл каким-то образом был изменен.

База данных

Для начала нам нужно сначала создать базовую таблицу для хранения хэшей наших файлов. Я буду использовать следующую схему:

 CREATE TABLE integrity_hashes ( file_path VARCHAR(200) NOT NULL, file_hash CHAR(40) NOT NULL, PRIMARY KEY (file_path) ); 

file_path хранит местоположение файла на сервере, и, поскольку значение всегда будет уникальным, поскольку два файла не могут занимать одно и то же место в файловой системе, наша основная задача. Я указал его максимальную длину в 200 символов, которая должна учитывать некоторые длинные пути к файлам. file_hash хранит хеш-значение файла, которое будет шестнадцатеричной строкой SHA-1 из 40 символов.

Сбор файлов

Следующим шагом является построение профиля файловой структуры. Мы определяем путь, с которого мы хотим начать сбор файлов, и рекурсивно перебираем каждый каталог, пока не охватим всю ветвь файловой системы, и при необходимости исключаем определенные каталоги или расширения файлов. Мы собираем хэши, которые нам нужны, когда мы просматриваем файловое дерево, которое затем сохраняется в базе данных или используется для сравнения.

PHP предлагает несколько способов навигации по дереву файлов; для простоты я буду использовать класс RecursiveDirectoryIterator .

 <?php define("PATH", "/var/www/"); $files = array(); // extensions to fetch, an empty array will return all extensions $ext = array("php"); // directories to ignore, an empty array will check all directories $skip = array("logs", "logs/traffic"); // build profile $dir = new RecursiveDirectoryIterator(PATH); $iter = new RecursiveIteratorIterator($dir); while ($iter->valid()) { // skip unwanted directories if (!$iter->isDot() && !in_array($iter->getSubPath(), $skip)) { // get specific file extensions if (!empty($ext)) { // PHP 5.3.4: if (in_array($iter->getExtension(), $ext)) { if (in_array(pathinfo($iter->key(), PATHINFO_EXTENSION), $ext)) { $files[$iter->key()] = hash_file("sha1", $iter->key()); } } else { // ignore file extensions $files[$iter->key()] = hash_file("sha1", $iter->key()); } } $iter->next(); } 

Обратите внимание, как я дважды ссылался на одни и те же logs папок в массиве $skip . То, что я предпочитаю игнорировать конкретный каталог, не означает, что итератор также будет игнорировать все подкаталоги, которые могут быть полезными или раздражающими в зависимости от ваших потребностей.

Класс RecursiveDirectoryIterator предоставляет нам доступ к нескольким методам:

  • valid() проверяет, работаем ли мы с действительным файлом
  • isDot() определяет, является ли каталог « . »Или« .. »
  • getSubPath() возвращает имя папки, в которой в данный момент находится указатель файла
  • key() возвращает полный путь и имя файла
  • next() снова запускает цикл

Есть также еще несколько методов, с которыми можно работать, но в основном перечисленные выше — это действительно все, что нам нужно для этой задачи, хотя в PHP 5.3.4 был добавлен метод getExtension() который возвращает расширение файла. Если ваша версия PHP поддерживает его, вы можете использовать его для фильтрации нежелательных записей, а не того, что я использовал с помощью pathinfo() .

При выполнении код должен заполнить массив $files с результатами, подобными следующим:

  массив
 (
     [/var/www/test.php] => b6b7c28e513dac784925665b54088045cf9cbcd3
     [/var/www/sub/hello.php] => a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
     [/var/www/sub/world.php] => da39a3ee5e6b4b0d3255bfef95601890afd80709
 ) 

После того, как мы создали профиль, обновление базы данных становится проще простого.

 <?php $db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASSWORD); // clear old records $db->query("TRUNCATE integrity_hashes"); // insert updated records $sql = "INSERT INTO integrity_hashes (file_path, file_hash) VALUES (:path, :hash)"; $sth = $db->prepare($sql); $sth->bindParam(":path", $path); $sth->bindParam(":hash", $hash); foreach ($files as $path => $hash) { $sth->execute(); } 

Проверка на расхождения

Теперь вы знаете, как создать новый профиль структуры каталогов и как обновить записи в базе данных. Следующий шаг — собрать его в какое-то реальное приложение, такое как cron, с уведомлением по электронной почте, административным интерфейсом или чем-то еще, что вы предпочитаете.

Если вы просто хотите собрать список файлов, которые изменились, и вам все равно, как они изменились, то самый простой подход — извлечь данные из базы данных в массив, похожий на $files а затем использовать array_diff_assoc() PHP array_diff_assoc() отсеять риффрафа.

 <?php // non-specific check for discrepancies if (!empty($files)) { $result = $db->query("SELECT * FROM integrity_hashes")->fetchAll(); if (!empty($result)) { foreach ($result as $value) { $tmp[$value["file_path"]] = $value["file_hash"]; } $diffs = array_diff_assoc($files, $tmp); unset($tmp); } } с <?php // non-specific check for discrepancies if (!empty($files)) { $result = $db->query("SELECT * FROM integrity_hashes")->fetchAll(); if (!empty($result)) { foreach ($result as $value) { $tmp[$value["file_path"]] = $value["file_hash"]; } $diffs = array_diff_assoc($files, $tmp); unset($tmp); } } 

В этом примере $diffs будет заполнен любыми найденными несоответствиями, или это будет пустой массив, если структура файла не повреждена. В отличие от array_diff() , array_diff_assoc() будет использовать ключи в сравнении, что важно для нас в случае коллизии, например, два пустых файла с одинаковым значением хеш-функции.

Если вы хотите сделать что-то еще, вы можете добавить простую логику, чтобы точно определить, как файл был затронут, был ли он удален, изменен или добавлен.

 <?php // specific check for discrepancies if (!empty($files)) { $result = $db->query("SELECT * FROM integrity_hashes")->fetchAll(); if (!empty($result)) { $diffs = array(); $tmp = array(); foreach ($result as $value) { if (!array_key_exists($value["file_path"], $files)) { $diffs["del"][$value["file_path"]] = $value["file_hash"]; $tmp[$value["file_path"]] = $value["file_hash"]; } else { if ($files[$value["file_path"]] != $value["file_hash"]) { $diffs["alt"][$value["file_path"]] = $files[$value["file_path"]]; $tmp[$value["file_path"]] = $files[$value["file_path"]]; } else { // unchanged $tmp[$value["file_path"]] = $value["file_hash"]; } } } if (count($tmp) < count($files)) { $diffs["add"] = array_diff_assoc($files, $tmp); } unset($tmp); } } с <?php // specific check for discrepancies if (!empty($files)) { $result = $db->query("SELECT * FROM integrity_hashes")->fetchAll(); if (!empty($result)) { $diffs = array(); $tmp = array(); foreach ($result as $value) { if (!array_key_exists($value["file_path"], $files)) { $diffs["del"][$value["file_path"]] = $value["file_hash"]; $tmp[$value["file_path"]] = $value["file_hash"]; } else { if ($files[$value["file_path"]] != $value["file_hash"]) { $diffs["alt"][$value["file_path"]] = $files[$value["file_path"]]; $tmp[$value["file_path"]] = $files[$value["file_path"]]; } else { // unchanged $tmp[$value["file_path"]] = $value["file_hash"]; } } } if (count($tmp) < count($files)) { $diffs["add"] = array_diff_assoc($files, $tmp); } unset($tmp); } } 

Поскольку мы перебираем результаты из базы данных, мы делаем несколько проверок. Во-первых, array_key_exists() используется для проверки, присутствует ли путь к файлу из нашей базы данных в $files , и если нет, то файл должен быть удален. Во-вторых, если файл существует, но значения хеш-функции не совпадают, файл должен быть изменен или иным образом не изменен. Мы сохраняем каждую проверку во временном массиве с именем $tmp , и, наконец, если количество $files больше, чем в нашей базе данных, мы знаем, что эти оставшиеся непроверенные файлы были добавлены.

После завершения $diffs будет либо пустым массивом, либо он будет содержать любые несоответствия, обнаруженные в форме многомерного массива, которые могут выглядеть следующим образом:

  массив
 (
     [alt] => Массив
         (
             [/var/www/test.php] => eae71874e2277a5bc77176db14ac14bf28465ec3
             [/var/www/sub/hello.php] => a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
         )

     [add] => Array
         (
             [/var/www/sub/world.php] => da39a3ee5e6b4b0d3255bfef95601890afd80709
         )

 ) 

Чтобы отобразить результаты в более удобном для пользователя формате, для административного интерфейса и т. П., Вы можете, например, просмотреть результаты и вывести их в виде маркированного списка.

 <?php // display discrepancies if (!empty($diffs)) { echo "<p>The following discrepancies were found:</p>"; echo "<ul>"; foreach ($diffs as $status => $affected) { if (is_array($affected) && !empty($affected)) { echo "<li>" . $status . "</li>"; echo "<ol>"; foreach($affected as $path => $hash) { echo "<li>" . $path . "</li>"; } echo "</ol>"; } } echo "</ul>"; } else { echo "<p>File structure is intact.</p>"; } 

На этом этапе вы можете предоставить ссылку, которая запускает действие по обновлению базы данных с новой файловой структурой, и в этом случае вы можете сохранить $files в переменной сеанса , или если вы не одобряете расхождения, вы можете обращайтесь к ним так, как считаете нужным.

Резюме

Надеемся, что это руководство поможет вам лучше понять целостность файлов. Наличие чего-то подобного на вашем веб-сайте является бесценной мерой безопасности, и вы можете чувствовать себя комфортно, зная, что ваши файлы остаются именно такими, как вы предполагали. Конечно, не забывайте регулярно делать резервные копии. Вы знаете, на всякий случай.

Изображение через Semisatch / Shutterstock