Защита каталогов загрузки
Загрузка файлов — одна из самых опасных функций веб-приложения, поскольку пользователям намеренно разрешается отправлять файлы на сервер. Если логика проверки ненадёжна, злоумышленник может попытаться замаскировать исполняемый код под изображение или документ.
Первое правило, которого я придерживаюсь, простое: загруженные файлы не должны быть исполняемыми.
В каталоге загрузки на базе Apache выполнение PHP можно заблокировать с помощью конфигурации, аналогичной следующей:
<FilesMatch "\.(php|phtml|phar|php[0-9]*)$">
Require all denied
</FilesMatch>
В зависимости от среды размещения также могут быть уместны дополнительные ограничения для обработчиков PHP. Цель состоит в том, чтобы сервер отказывался выполнять вредоносный скрипт, даже если каким-то образом он попадёт в каталог загрузок.
Я также избегаю доверять имени файла, указанному пользователем. Браузер может утверждать, что файл называется vacation.jpg, но приложение должно самостоятельно проверить тип файла и создать для хранения собственное имя.
Базовый процесс загрузки в PHP может выглядеть так:
<?php
$allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp'
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['upload']['tmp_name']);
if (!isset($allowedTypes[$mime])) {
throw new RuntimeException('Unsupported file type.');
}
$filename = bin2hex(random_bytes(16)) . '.' . $allowedTypes[$mime];
?>
Важно, что расширение определяет сервер, а не принимает то, которое указал пользователь.
Никогда не доверяйте MIME-типам, отправленным браузером
MIME-тип, включённый в HTTP-загрузку, можно подделать. Если PHP получает файл с заявленным типом image/jpeg, одного этого значения недостаточно, чтобы определить, безопасен ли файл.
Использование finfo позволяет PHP проверить фактическое содержимое файла и определить более надёжный MIME-тип. Но даже в этом случае я всё равно обращаюсь с загруженными файлами осторожно.
Для изображений дополнительной мерой защиты будет фактически декодировать и повторно закодировать изображение. Вместо сохранения исходных двоичных данных загруженного файла приложение может загрузить изображение в библиотеку обработки изображений и создать новый файл JPEG или WebP. Это удаляет значительную часть неожиданных данных, которые могли быть встроены в исходный загруженный файл.
У этого подхода есть ещё одно преимущество. Он стандартизирует размеры, сжатие и формат изображений, одновременно повышая безопасность.
Используйте учётные записи базы данных с минимальными привилегиями
Ещё одна распространённая ошибка — предоставление пользователю базы данных веб-сайта большего количества привилегий, чем фактически требуется приложению. Публичному веб-сайту обычно не нужны возможности создавать новых пользователей базы данных, предоставлять привилегии или администрировать весь сервер MySQL.
У приложения должна быть собственная выделенная учётная запись базы данных только с теми разрешениями, которые необходимы приложению.
Упрощённый пример может выглядеть так:
CREATE USER 'website_app'@'localhost'
IDENTIFIED BY 'strong-random-password';
GRANT SELECT, INSERT, UPDATE, DELETE
ON production_database.*
TO 'website_app'@'localhost';
Необходимость в дополнительных привилегиях зависит от принципа работы приложения, но я избегаю предоставления публичному приложению полных административных привилегий, если для этого нет конкретной причины.
Принцип минимальных привилегий ограничивает масштаб последствий компрометации приложения. Если учётные данные базы данных веб-сайта будут украдены, злоумышленник получит только разрешения, назначенные этой учётной записи, а не контроль над каждой базой данных на сервере.
Повсеместно используйте подготовленные выражения
SQL-инъекции остаются одной из наиболее серьёзных уязвимостей веб-приложений, поскольку могут превратить контролируемые пользователем данные в исполняемый SQL-код. Решение заключается не в попытках вручную экранировать подозрительные символы. Решение — в отделении структуры SQL от пользовательских данных.
При работе с PDO я использую подготовленные выражения.
<?php
$stmt = $pdo->prepare(
'SELECT id, username, email
FROM users
WHERE email = :email'
);
$stmt->execute([
':email' => $email
]);
$user = $stmt->fetch();
?>
Важно то, что $email обрабатывается как данные, а не конкатенируется с SQL-командой.
Я избегаю такого кода:
<?php
$sql = "SELECT * FROM users WHERE email = '$email'";
?>
Даже когда разработчики считают, что контролируют входные данные, приложения со временем меняются. Подготовленные выражения должны использоваться по умолчанию, а не только для полей, считающихся опасными.
Безопасная аутентификация администратора
Интерфейс администратора заслуживает более сильной защиты, чем общедоступная часть сайта. Если возможно, я использую многофакторную аутентификацию для административных аккаунтов. Как минимум, пароли администраторов должны быть длинными, уникальными и храниться с использованием функций хеширования паролей PHP.
Хранение паролей должно выглядеть примерно так:
<?php
$hash = password_hash($password, PASSWORD_DEFAULT);
?>
Затем аутентификация использует:
<?php
if (password_verify($password, $storedHash)) {
// Successful authentication.
}
?>
Пароли никогда не следует хранить в открытом виде, шифровать с помощью обратимого ключа или хешировать с использованием устаревших алгоритмов, таких как MD5.
Я также не раскрываю, существует ли конкретное имя пользователя или адрес электронной почты. Вместо сообщения «Аккаунта с таким адресом электронной почты нет» приложение может отображать общее сообщение об ошибке аутентификации.
Это не позволяет странице входа превратиться в инструмент выявления аккаунтов.
Усиление безопасности PHP-сессий
Безопасность аутентификации не заканчивается после принятия пароля. После входа пользователя cookie сессии фактически становится его временным учётным данным.
Я настраиваю сессионные cookie так, чтобы при работе сайта по HTTPS использовались атрибуты безопасности.
<?php
session_set_cookie_params([
'httponly' => true,
'secure' => true,
'samesite' => 'Lax'
]);
?>
Атрибут HttpOnly затрудняет доступ клиентского JavaScript к cookie. Атрибут Secure указывает браузеру отправлять cookie только по HTTPS. Атрибут SameSite может помочь снизить риск некоторых атак с межсайтовыми запросами.
После успешного входа в систему я регенерирую идентификатор сессии.
<?php
session_regenerate_id(true);
?>
Это снижает риск атак с фиксацией сессии.
Для административных сессий также можно использовать тайм-ауты бездействия. Если администратор на несколько часов отходит от браузера, в котором выполнен вход, сессия не должна обязательно оставаться активной неопределённо долго.
Защищайте каждый запрос, изменяющий состояние, с помощью CSRF-токенов
Атака межсайтовой подделки запроса (Cross-Site Request Forgery) обманом заставляет браузер аутентифицированного пользователя отправить нежелательный запрос. Представьте, что администратор вошёл в CMS и одновременно посещает другой вредоносный сайт. Если CMS не проверяет источник важных запросов, вредоносная страница может получить возможность запускать действия, используя аутентифицированную сессию администратора.
Для форм, которые создают, обновляют или удаляют информацию, я использую CSRF-токены.
Токен можно сгенерировать и сохранить в сессии:
<?php
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
Затем токен помещается внутрь формы:
<input type="hidden"
name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
При отправке формы приложение проверяет его.
<?php
if (
empty($_POST['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
) {
http_response_code(403);
exit('Invalid request.');
}
?>
Токен не заменяет аутентификацию. Он добавляет дополнительный шаг проверки, подтверждающий, что запрос был отправлен из формы, сгенерированной приложением.
Экранируйте вывод, а не только входные данные
Разработчики иногда полностью сосредотачиваются на очистке входящих данных, однако обработка вывода не менее важна. Значение, сохранённое в базе данных, позднее может быть отображено внутри HTML, JavaScript, атрибута или URL. Для каждого контекста требуются свои правила экранирования.
Для обычного вывода в HTML я использую:
<?php
echo htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
?>
Это особенно важно для комментариев, имён пользователей, заголовков статей, отправленных форм и любых других данных, поступивших извне приложения.
Сохранённый межсайтовый скриптинг может быть особенно опасен, поскольку вредоносная полезная нагрузка остаётся в базе данных. Если эта полезная нагрузка отображается в панели администратора, она может выполняться каждый раз, когда администратор просматривает соответствующую запись.
Не раскрывайте посетителям подробные ошибки PHP
Ошибки разработки полезны во время программирования, но пользователи в рабочей среде не должны получать трассировки стека, ошибки базы данных, пути к файлам или сведения о конфигурации.
В рабочей среде PHP обычно не должен напрямую отображать внутренние ошибки в браузере.
display_errors = Off
log_errors = On
Вместо этого ошибки следует записывать в защищённый файл журнала.
Такое исключение базы данных:
SQLSTATE[HY000] [1045] Access denied for user...
может раскрыть имена пользователей базы данных, версии программного обеспечения, внутренние пути и структуру приложения. Анонимному посетителю не нужно показывать ничего из этого.
Пользователь может получить общее сообщение, в то время как настоящая ошибка будет записана в закрытый журнал для отладки.
Создайте действительно полезное журналирование событий безопасности
Стандартные журналы доступа ценны, но я также предпочитаю журналирование событий безопасности на уровне приложения. Приложение может записывать подозрительные события, такие как повторяющиеся неудачные попытки входа, заблокированные типы загрузки, попытки доступа к защищённым конечным точкам, недействительные токены CSRF и запросы файлов, которые ни при каких обстоятельствах не должны быть общедоступными.
Базовая запись журнала безопасности может содержать временную метку, IP-адрес, URI запроса, пользовательский агент, идентификатор аутентифицированного пользователя, если он доступен, и правило, вызвавшее оповещение.
Например:
<?php
$entry = sprintf(
"[%s] IP=%s URI=%s EVENT=%s\n",
date('c'),
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
$_SERVER['REQUEST_URI'] ?? '',
'Blocked sensitive-file probe'
);
file_put_contents(
'/home/account/logs/security.log',
$entry,
FILE_APPEND | LOCK_EX
);
?>
Журналы безопасности не следует хранить внутри общедоступного каталога, доступного для скачивания. Кроме того, в них не следует записывать такие секреты, как пароли, полные файлы cookie сеансов, заголовки аутентификации или закрытые API-ключи.
Обнаруживайте распространённые запросы разведки
Злоумышленники часто проверяют веб-сайты на наличие распространённых файлов и приложений, независимо от того, существуют ли эти приложения на самом деле. Пользовательский PHP-сайт всё равно может получать запросы к /wp-login.php, /wp-admin, /.env, /.git/config и /phpmyadmin.
Эти запросы не обязательно означают, что злоумышленник что-либо знает о сайте. Значительная часть такого сканирования выполняется автоматически. Однако журналирование этих запросов всё равно может быть полезным, поскольку оно выявляет подозрительные модели поведения.
Apache может немедленно запрещать доступ к определённым конфиденциальным путям.
RedirectMatch 404 (?i)/\.git
RedirectMatch 404 (?i)/\.env
Обычно я предпочитаю возвращать обычный ответ с ошибкой, а не раскрывать лишнюю информацию о том, существовал ли когда-либо этот файл.
Ограничение частоты попыток аутентификации
Страница входа не должна позволять выполнять неограниченное количество попыток аутентификации с машинной скоростью. Даже надёжные пароли выигрывают от ограничения частоты запросов, поскольку это сокращает количество автоматизированных атак перебором и атак с использованием повторно применяемых учётных данных.
Базовое приложение может отслеживать неудачные попытки входа по идентификатору учётной записи и IP-адресу. После нескольких неудачных попыток оно может вводить временный период ожидания.
Для реализации можно использовать MySQL, Redis, локальный кэш или другой механизм хранения данных. Конкретная система имеет меньшее значение, чем основной принцип: попытки аутентификации должны иметь определённую стоимость.
Я избегаю постоянной блокировки учётных записей, запускаемой только неудачными попытками, поскольку злоумышленник может намеренно заблокировать добросовестным пользователям доступ к их учётным записям. Временное ограничение обычно является более удачным решением.
Не доверяйте User-Agent или IP-адресу как средствам аутентификации
IP-адреса и строки User-Agent браузера могут быть полезны для ведения журналов и обнаружения аномалий, но ни один из этих параметров не должен использоваться как основной механизм идентификации. В течение одного дня пользователи могут переходить между мобильными сетями, сетями Wi-Fi, VPN и меняющимися IP-адресами.
Системам безопасности следует использовать эти сигналы как контекст, а не как абсолютное доказательство.
Например, внезапный вход администратора из новой страны может служить основанием для дополнительной аутентификации, но одно лишь изменение IP-адреса не должно обязательно приводить к завершению работы легитимной учётной записи.
Разделяйте публичные и административные маршруты
Я предпочитаю чётко разделять маршруты приложения, доступные пользователям, и административные функции. Административный интерфейс не должен случайно повторно использовать публичные контроллеры, которые предоставляют больше функций, чем предполагалось.
Например, CMS может использовать:
/admin/posts
/admin/users
/admin/settings
Каждый административный маршрут должен выполнять проверку авторизации на сервере.
Скрытие ссылки в навигации не является авторизацией. JavaScript, удаляющий кнопку администратора, не является авторизацией. Если пользователь вручную вводит административный URL без необходимых разрешений, он должен получить отказ на стороне сервера.
Это различие чрезвычайно важно. Всё, что обеспечивается только внутри браузера, потенциально можно обойти.
Используйте авторизацию на основе ролей
Аутентификация отвечает на вопрос: «Кто этот пользователь?» Авторизация отвечает на вопрос: «Что этому пользователю разрешено делать?»
Система с администраторами, редакторами, авторами, клиентами или бизнес-учётными записями должна явно определять эти разрешения.
Редактору может быть разрешено изменять статьи, но не создавать учётные записи администраторов. Сотрудник службы поддержки может иметь возможность просматривать сеансы клиентов, но не изменять конфигурацию биллинга. Обычный пользователь ни в коем случае не должен получать доступ к внутреннему административному интерфейсу лишь потому, что обнаружил конечную точку API.
Каждая чувствительная операция должна независимо проверять наличие авторизации.
Резервные копии тоже нуждаются в защите
Резервные копии необходимы после взлома, но они также могут стать угрозой безопасности. Разработчики иногда создают файлы с такими именами:
database-backup.sql
website-old.zip
public_html-backup.tar.gz
config.php.bak
и оставляют их внутри общедоступного веб-каталога.
Эти файлы могут содержать исходный код, записи базы данных, хеши паролей, адреса электронной почты, учетные данные API и другую конфиденциальную информацию.
По возможности резервные копии следует хранить за пределами корневого каталога документов. В идеале резервные копии также должны быть зашифрованы, защищены средствами контроля доступа и скопированы в независимое место хранения.
Что особенно важно, резервные копии следует действительно тестировать. Резервная копия, из которой ни разу не выполнялось восстановление, — лишь предположение.
Проверьте запланированные задачи
О заданиях Cron легко забыть во время реагирования на инцидент. Злоумышленник, получивший достаточный уровень доступа, может создать запланированный процесс, который восстановит удаленное вредоносное ПО или позднее загрузит полезную нагрузку.
Поэтому после серьезного взлома я проверяю запланированные задачи.
В Linux это может включать:
crontab -l
В зависимости от конфигурации сервера администратору сервера также может потребоваться проверить системные каталоги Cron.
Чистый каталог веб-сайта не обязательно означает, что система чиста, если другой процесс способен автоматически заразить его повторно.
Проверьте наличие неожиданных учетных записей администраторов
Если CMS содержит таблицу пользователей, я вручную проверяю привилегированные учетные записи. Я хочу знать, кто обладает правами администратора, когда были созданы эти учетные записи и можно ли распознать их адреса электронной почты и имена пользователей.
Тот же принцип применяется к учетной записи хостинга, системе управления базами данных, пользователям SSH, учетным записям FTP и пользователям панели управления.
Злоумышленники часто предпочитают создавать доступ, выглядящий легитимным, вместо того чтобы полностью полагаться на вредоносное ПО. Дополнительная учётная запись администратора может пережить очистку кода и продолжать предоставлять им доступ.
Проверьте базу данных на наличие внедрённого содержимого
Не всё вредоносное ПО на веб-сайте находится внутри файлов. Злоумышленник может вставить спам-ссылки, скрипты, перенаправления, поддельные статьи или вредоносный HTML непосредственно в записи базы данных.
Я ищу в содержимом базы данных подозрительные домены, неожиданные теги <script>, элементы iframe, закодированный JavaScript и большие блоки незнакомого HTML.
Это особенно важно для CMS-систем, в которых содержимое страниц в основном хранится в MySQL. Очистка файловой системы без проверки базы данных может оставить компрометацию частично сохранённой.
Используйте Content Security Policy там, где это возможно
Content Security Policy, или CSP, может уменьшить последствия некоторых атак с использованием межсайтового скриптинга, ограничивая набор скриптов, стилей, фреймов и других ресурсов, которые браузеру разрешено загружать.
Очень строгая CSP требует тщательного тестирования, поскольку современные приложения часто зависят от сторонних сервисов, аналитики, платёжных провайдеров, CDN и встроенных скриптов. Однако даже тщательно разработанная политика может значительно сократить число источников, из которых принимается исполняемое содержимое.
В качестве базовой отправной точки можно использовать следующий вариант:
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'self';"
Этот пример намеренно минимален. Реальному приложению могут понадобиться дополнительные директивы для Stripe, аналитики, шрифтов, изображений, API или внешних ресурсов.
Важно понимать, что CSP следует рассматривать как дополнительный уровень безопасности браузера, а не как замену безопасному программированию.
Добавьте другие полезные заголовки безопасности
Несколько заголовков HTTP-ответа могут ограничить ненужное поведение браузера или усилить безопасность приложения.
Например:
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Frame-Options "SAMEORIGIN"
Эти заголовки решают разные задачи. X-Content-Type-Options не позволяет браузерам угадывать типы содержимого. Referrer-Policy управляет объёмом передаваемой информации о ссылающемся URL. X-Frame-Options может снизить риск атак с использованием фреймов в браузерах, которые его поддерживают.
Современные приложения могут использовать директиву CSP frame-ancestors вместо X-Frame-Options или в дополнение к ней.
Принудительно использовать HTTPS
Система входа в систему не должна работать по незашифрованному HTTP. HTTPS защищает учётные данные, файлы cookie сеанса, отправленные формы и другую конфиденциальную информацию во время её передачи между браузером и сервером.
Я перенаправляю общедоступный HTTP-трафик на HTTPS и убеждаюсь, что само приложение формирует URL-адреса HTTPS.
Базовое перенаправление Apache может выглядеть так:
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
В системах, использующих обратные прокси или балансировщики нагрузки, обнаружение HTTPS может потребовать другой конфигурации, поэтому необходимо учитывать архитектуру хостинга.
Контролируйте зависимости
Зависимости фактически являются сторонним кодом, работающим внутри вашего приложения. Каждый пакет, установленный через Composer, npm или другой менеджер пакетов, становится частью поверхности атаки приложения.
Я периодически проверяю зависимости и удаляю пакеты, которые больше не нужны. Меньшее количество зависимостей означает меньше компонентов, требующих обновления, и меньше возможностей для того, чтобы уязвимый код оставался незамеченным.
В PHP-проектах на основе Composer аудит зависимостей может быть частью рабочего процесса обслуживания.
composer audit
Важная эксплуатационная привычка заключается не просто в бездумной установке обновлений. Вы должны знать, от каких пакетов зависит приложение, что они делают и не остаются ли заброшенные пакеты в рабочей среде.
Защищайте ключи API от фронтенда
Секретные учётные данные API никогда не следует встраивать в общедоступный JavaScript. Всё, что отправляется в браузер, в конечном счёте может быть просмотрено пользователем.
Если API требует секретные учётные данные сервера, запрос, как правило, следует выполнять с сервера.
Например, секрет OpenAI, Stripe, почтового или внутреннего сервиса должен храниться в конфигурации на стороне сервера, а не внутри пакета JavaScript.
Публичные ключи отличаются. Некоторые провайдеры намеренно используют безопасные для браузера публикуемые ключи. Разработчики должны понимать различие между публикуемыми и секретными учётными данными, а не считать, что любой ключ API можно безопасно размещать во фронтенд-коде.
Проверяйте вебхуки Stripe
Платёжные системы требуют особого внимания, поскольку злоумышленник не должен иметь возможности сообщить вашему приложению об успешной оплате, просто отправив собственный HTTP-запрос на URL вебхука.
При использовании вебхуков Stripe приложение должно проверять подпись события с помощью секрета подписи вебхука, прежде чем доверять содержимому полезной нагрузки.
Общая идея выглядит так:
<?php
$event = \Stripe\Webhook::constructEvent(
$payload,
$signature,
$endpointSecret
);
?>
Только после успешной проверки приложение должно обновлять заказ, открывать доступ к платному контенту, отмечать счёт как оплаченный или предоставлять услугу.
База данных приложения никогда не должна считать перенаправление браузера на страницу «оплата успешно завершена» доказательством фактического получения денег.
Защищайте платный контент на сервере
Это особенно важно для членских сайтов и контента за платной подпиской. Скрытие платного контента с помощью CSS или JavaScript не является настоящим контролем доступа.
Если полная статья доставляется в браузер, а JavaScript лишь скрывает её, кто-то может открыть инструменты разработчика и прочитать якобы защищённый контент.
Сервер должен определить, имеет ли посетитель необходимые права, прежде чем возвращать ограниченный материал.
Концептуально:
<?php
if (!$userHasAccess) {
echo $preview;
exit;
}
echo $fullArticle;
?>
Важно понимать, что неавторизованные посетители никогда не получают защищённую часть статьи.
То же правило применяется к премиальным загрузкам. Не размещайте PDF по общедоступному предсказуемому URL, предполагая, что люди будут переходить к нему только через страницу покупки. Приложение должно проверять права доступа перед передачей файла.
Используйте безопасные обработчики загрузок
Защищённые файлы в идеале следует хранить за пределами общедоступного веб-каталога. Когда авторизованный клиент запрашивает файл, PHP может проверить разрешение и передать файл в браузер потоком.
Расположение хранилища может выглядеть так:
/home/account/private-downloads/security-checklist.pdf
а не так:
/public_html/downloads/security-checklist.pdf
Это не позволяет обойти авторизацию приложения, обнаружив прямой URL.
Отслеживайте трафик 404
Один из самых простых способов обнаружить автоматическое сканирование — отслеживать повторяющиеся запросы к несуществующим файлам. Обычный посетитель иногда вызывает ошибку 404 из-за неработающей ссылки. Сканер может отправить сотни запросов, возвращающих ошибку 404, в поисках плагинов WordPress, конфигурационных файлов, архивов резервных копий, административных интерфейсов, файлов окружения и известных уязвимых скриптов.
Я не блокирую автоматически каждого посетителя, вызвавшего ошибку 404, но закономерности имеют значение.
Один запрос к /wp-login.php мало о чём говорит. Пятьдесят запросов за несколько секунд к .env, phpmyadmin, xmlrpc.php, .git/config, резервным копиям баз данных и старым плагинам говорят уже о другой картине.
Решения в области безопасности следует принимать на основе поведения, а не одного изолированного запроса.
Будьте осторожны с автоматической блокировкой IP-адресов
Автоматическая блокировка может быть полезной, но чрезмерно агрессивные системы способны создавать собственные проблемы. Поисковые системы, мониторы доступности, сканеры безопасности, корпоративные прокси, VPN-сервисы и добросовестные пользователи могут генерировать необычные шаблоны запросов.
Вместо того чтобы навсегда блокировать IP-адрес из-за одного подозрительного URL, я предпочитаю использовать пороговые значения и временные блокировки.
Например, приложение может временно заблокировать клиента после неоднократных попыток получить доступ к конфиденциальным файлам в течение короткого промежутка времени.
Это снижает автоматическое сканирование, не превращая каждый необычный запрос в повод считать отправителя постоянным врагом.
Регистрируйте административные изменения
Хорошая CMS должна вести журнал аудита важных действий. Я хочу знать, когда пользователь входит в систему, создаёт страницу, удаляет запись, изменяет привилегии другого пользователя, обновляет платёжные настройки, изменяет конфигурацию сайта или выполняет другую операцию с существенным влиянием.
В журнале аудита могут записываться идентификатор администратора, действие, затронутая запись, отметка времени и исходный IP-адрес.
Например:
2026-08-29T21:44:12
USER=14
ACTION=delete_post
POST_ID=381
IP=203.0.113.10
Журналы аудита становятся чрезвычайно ценными, когда что-то идёт не так, поскольку позволяют восстановить ход событий, а не полагаться исключительно на память.
Создавайте резервные копии перед серьёзными изменениями
Само усиление безопасности может нарушить работу веб-сайта, если вносить изменения неосторожно. Ограничительное правило Apache может заблокировать легитимные маршруты. Новая CSP может помешать загрузке необходимого JavaScript. Изменения прав доступа к файлам могут нарушить загрузку файлов. Настройки сеансов могут повлиять на аутентификацию.
Перед внесением серьёзных изменений в систему безопасности я создаю резервную копию приложения и базы данных.
Разница лишь в том, что после этого я не оставляю эту резервную копию в общедоступном каталоге.
Тестируйте веб-сайт как человек, который ему не доверяет
После завершения базового усиления безопасности я тестирую приложение с точки зрения человека, пытающегося заставить его работать неправильно.
Что произойдёт, если я изменю идентификатор записи в запросе? Может ли один клиент получить доступ к файлу другого клиента? Может ли обычная учётная запись вручную запросить URL администратора? Что произойдёт, если я загружу файл с вводящим в заблуждение расширением? Что произойдёт, если я отправлю HTML-код во все формы? Что произойдёт, если я отправлю запрос без CSRF-токена? Могу ли я инициировать действие завершения оплаты, фактически не заплатив?
Такой подход ценен, поскольку многие серьёзные уязвимости существуют не в синтаксисе кода, а в предположениях разработчиков о том, как будут вести себя пользователи.
Обычный пользователь следует интерфейсу. Атакующий — нет.
Мой контрольный список после компрометации
Когда я оцениваю веб-сайт на PHP после инцидента безопасности, я проверяю всю среду, а не только страницу, на которой стало очевидно, что произошла компрометация. Я проверяю недавно изменённые файлы, каталоги загрузок, учётные записи администраторов, содержимое базы данных, задания cron, журналы сервера, учётные данные API, конфигурационные файлы, резервные копии, разрешения файлов и поведение при аутентификации.
Я заменяю скомпрометированные учётные данные, по возможности перемещаю секреты за пределы корневого каталога документов, отключаю ненужные функции, запрещаю выполнение скриптов в каталогах загрузок, проверяю разрешения базы данных, убеждаюсь, что используются подготовленные выражения, усиливаю защиту сессий, внедряю защиту от CSRF и проверяю, что защищённое содержимое действительно защищено сервером.
Я также проверяю, что резервные копии хранятся отдельно от рабочего веб-сайта и что с их помощью действительно можно восстановить приложение.
Цель состоит не в том, чтобы сделать веб-сайт теоретически невозможным для взлома. Это нереалистичная модель безопасности. Цель — устранить очевидные слабые места, уменьшить поверхность атаки, ограничить возможности злоумышленника в случае отказа одного из компонентов и обеспечить достаточное журналирование, чтобы подозрительная активность становилась заметной.
Безопасность должна быть многоуровневой
Одной меры безопасности никогда не бывает достаточно. Брандмауэр может выйти из строя. В приложении может содержаться программная ошибка. Пароль могут украсть. В зависимости может появиться уязвимость. Сервер может быть неправильно настроен.
Решение — эшелонированная защита.
Если злоумышленнику удастся загрузить вредоносный файл, каталог загрузок должен запретить его выполнение. Если злоумышленник узнает пароль базы данных, учётная запись базы данных должна иметь ограниченные права. Если пароль администратора украден, многофакторная аутентификация должна создать ещё один барьер. Если вредоносный JavaScript попадёт в базу данных, экранирование вывода и Content Security Policy должны ограничить возможность его выполнения. Если что-то всё же пойдёт не так, журналы и резервные копии должны упростить обнаружение и восстановление.
Именно эта философия лежит в основе почти каждой меры безопасности в данной статье.
Безопасность — это не продукт, который устанавливают один раз и забывают о нём. Это архитектура, привычка при разработке и операционный процесс.
Как только вы начинаете проектировать системы, исходя из предположения, что пользователи будут манипулировать запросами, сканеры — проверять несуществующие файлы, боты — атаковать страницы входа, а ошибки рано или поздно неизбежно произойдут, ваши приложения становится значительно сложнее взломать.
И после того, как вы хотя бы раз действительно очистили взломанный веб-сайт, вы обычно перестаёте считать эти меры предосторожности чрезмерными.