Уязвимости PHP на старых проектах: как защитить сайт, который нельзя обновить

Уязвимости PHP на старых проектах: как защитить сайт, который нельзя обновить

У каждого, кто давно делает сайты, есть такой проект. Ему лет десять, внутри Joomla 1.5 или WordPress времён темы Twenty Twelve, под капотом PHP 5.6, а на сервере стоит Ubuntu, которую последний раз обновляли, когда в моде были спиннеры. Клиент произносит волшебную фразу: «Работает — не трогай». Бюджета на переписывание, разумеется, нет.

Я слышу эту фразу с 2008 года. И могу сказать одно: «не трогать» в 2026 году — это тоже решение. Просто его последствия достаются не вам, а ботам, которые круглосуточно сканируют интернет и очень любят такие сайты.

Ниже — что именно делает старый PHP опасным и как защитить проект, который пока нельзя переписать. От мер на пять минут до таких, после которых коллеги начнут смотреть на вас с уважением и лёгкой тревогой.

Главное за 30 секунд

  • PHP 5.6 не получает исправлений безопасности с конца 2018 года, вся ветка 7.x — с ноября 2022-го. 8.0 и 8.1 тоже уже вне поддержки.
  • Взламывают чаще не сам PHP, а CMS и её расширения. Старый PHP и старая ОС превращают такой взлом из неприятности в катастрофу.
  • Самое важное: запрет PHP в папках загрузок, изоляция сайтов друг от друга, бэкапы вне сервера и поддерживаемая операционная система.
  • Настоящее лечение одно — миграция на PHP 8.4 или 8.5. Всё остальное — способ до неё дожить.

Что не так со старым PHP

У PHP чёткий жизненный цикл. Сейчас каждая ветка два года получает обычные исправления и ещё два — только исправления безопасности (php.net). После этого ветка объявляется End of Life, и разработчики перестают её чинить. Совсем. Даже если завтра найдут дыру, через которую можно выполнить любой код на сервере.

ВерсияКонец поддержки безопасностиСтатус на октябрь 2026
5.631 декабря 2018EOL, последний релиз 5.6.40 — 10 января 2019
7.010 января 2019EOL
7.11 декабря 2019EOL
7.230 ноября 2020EOL
7.36 декабря 2021EOL
7.428 ноября 2022EOL
8.026 ноября 2023EOL
8.131 декабря 2025EOL
8.231 декабря 2026Только безопасность, осталось два месяца
8.331 декабря 2027Только безопасность
8.431 декабря 2028Активная поддержка
8.531 декабря 2029Активная поддержка

Источник: php.net/eol и php.net/supported-versions.

Что это значит на практике? Любая уязвимость в ядре PHP, найденная после 2018 года, в 5.6 остаётся открытой навсегда. Два примера хорошо показывают масштаб.

CVE-2019-11043. Удалённое выполнение кода в PHP-FPM при вполне типовом конфиге nginx — с fastcgi_split_path_info и без проверки существования файла. Достаточно специально сформированного URL. Исправление вышло в 7.1.33, 7.2.24 и 7.3.11. Автор публичного эксплойта прямо пишет, что эксплойт рассчитан на PHP 7+, но сама ошибка есть и в более ранних версиях (phuip-fpizdam). Для 5.6 патча не было и не будет.

CVE-2024-2961. Это ошибка не в PHP, а в iconv из glibc. Исследователь Charles Fol показал, как через фильтры php://filter она превращает обычное чтение файла в выполнение кода (разбор Lexfo). Исправляется обновлением glibc, то есть операционной системы. Если ОС старая и не обновляется, дыра открыта при любой версии PHP.

Второй пример — хороший мостик к главному.

Настоящая проблема — цепочка

Если честно, сам интерпретатор ломают редко. Типичный взлом старого сайта выглядит скучно. Дальше всё зависит от того, что окружает сайт:

  1. Бот находит уязвимое расширение CMS или форму загрузки без проверки и заливает webshell — PHP-файл, через который можно выполнять команды.
  2. Если все сайты на сервере работают от одного пользователя, атакующий читает чужие configuration.php и wp-config.php — вместе с паролями от баз.
  3. Если ОС старая, он берёт публичный эксплойт повышения привилегий и становится root.
  4. С правами root можно всё: майнер, спам-рассылка, шифровальщик, удаление бэкапов, если они лежат на том же сервере.

Пункт 3 — не теория. В гостиничной сети, которую я обслуживаю, был взлом, который пришёл не через PHP, а через уязвимость устаревшей операционной системы. Вывод оттуда простой: защищать старый PHP и оставлять под ним старую ОС — всё равно что ставить бронированную дверь в картонную стену.

Несколько уязвимостей, для которых на GitHub давно лежат готовые эксплойты:

  • PwnKit (CVE-2021-4034) — polkit/pkexec, root для любого локального пользователя. Ошибка жила в коде с 2009 года и есть почти во всех дистрибутивах.
  • Baron Samedit (CVE-2021-3156) — переполнение буфера в sudo, тоже root. В коде с 2011 года.
  • CVE-2024-1086 — ошибка в nf_tables ядра Linux, затронуты ядра от 3.15 до 6.8-rc1. CISA внесла её в каталог эксплуатируемых уязвимостей в мае 2024 года, а в октябре 2025-го подтвердила, что её используют шифровальщики (BleepingComputer).

На обновлённой системе эти эксплойты не работают. На сервере, который не видел apt upgrade с 2020 года, срабатывают с первого раза. А таких серверов много: стандартная поддержка Ubuntu 20.04 закончилась в мае 2025 года, Debian 11 полностью снят с поддержки 31 августа 2026-го (debian.org).

Хорошая новость: большую часть цепочки можно разорвать, не трогая код сайта. Пойдём от простого к сложному. Перед любыми изменениями сохраните копию конфигов, после — проверяйте nginx -t и открывайте сайт.

Уровень 1. Что сделать прямо сейчас

Эти шаги занимают от пяти минут до часа, почти ничего не ломают и закрывают самые массовые сценарии атак.

1. Посмотрите, что у вас вообще стоит

Начните с инвентаризации: версия ОС, ядро, PHP и пришли ли патчи на две самые известные дыры. Номер версии pkexec или sudo сам по себе мало что скажет — дистрибутивы переносят исправления в старые версии. Надёжнее поискать номер CVE в changelog пакета.

cat /etc/os-release | grep PRETTY_NAME
uname -r
ls /etc/php/                       # какие версии PHP установлены

# Debian/Ubuntu: 0 означает, что патч не пришёл
zgrep -c CVE-2021-4034 /usr/share/doc/policykit-1/changelog.Debian.gz
zgrep -c CVE-2021-3156 /usr/share/doc/sudo/changelog.Debian.gz

# CentOS/RHEL
rpm -q --changelog polkit | grep -c CVE-2021-4034
rpm -q --changelog sudo | grep -c CVE-2021-3156

# Что слушает сеть
ss -tulpn

2. Если ОС уже без поддержки — включите ESM

Для Ubuntu 18.04 и 20.04 есть Expanded Security Maintenance — исправления безопасности через подписку Ubuntu Pro: для 18.04 до 2028 года, для 20.04 до 2030-го (ubuntu.com). Для личного использования она бесплатна до пяти машин, для бизнеса — платная. Если PHP установлен из штатного репозитория Ubuntu, ESM патчит и его.

pro attach ВАШ_ТОКЕН
pro enable esm-infra esm-apps livepatch
apt update && apt upgrade

Если ESM недоступен (Debian 10–11, CentOS 7, Ubuntu 16.04), сделайте минимум. Уберите SUID-бит с pkexec — так PwnKit перестаёт работать. Закройте файрволом всё, кроме 80, 443 и SSH. И поставьте переезд в план, он описан в третьем уровне.

chmod 0755 /usr/bin/pkexec

3. Выключите Xdebug

Xdebug — отладчик, на боевом сервере ему не место. В Xdebug 2 с включённым xdebug.remote_connect_back любой желающий может заставить сервер подключиться к своему компьютеру и выполнять код через отладчик.

phpdismod -v 5.6 xdebug            # или: apt remove php5.6-xdebug
systemctl reload php5.6-fpm

Проверять через php -m бесполезно: у CLI и FPM разные php.ini. Положите временный файл с phpinfo(), найдите в выводе секцию xdebug и сразу удалите файл.

4. Не светите версиями

Это не защита, а снижение заметности: по заголовку X-Powered-By: PHP/5.6.40 сканер сразу понимает, какие эксплойты пробовать первыми.

; php.ini
expose_php = Off

# nginx, блок http {}
server_tokens off;
fastcgi_hide_header X-Powered-By;

5. Запретите выполнение PHP в папках загрузок

Это самый важный шаг первого уровня. Даже если через дырявую форму на сервер попадёт shell.php, запустить его не получится.

# В server {} — ОБЯЗАТЕЛЬНО выше location ~ \.php$
location ~* ^/(?:images|media|cache|tmp|logs|uploads|files|administrator/cache|wp-content/uploads)/.*\.(?:php\d?|phtml|phar)$ {
    return 403;
}

Порядок важен: nginx проверяет regex-локации сверху вниз. Если location ~ \.php$ окажется выше, запрос уйдёт в PHP раньше, чем сработает запрет. Если за nginx стоит Apache, продублируйте запрет файлом .htaccess в каждой папке загрузок:

<FilesMatch "\.(php\d?|phtml|phar)$">
    Require all denied
</FilesMatch>

Проверка: положите в папку тестовый PHP-файл и запросите его.

echo '<?php echo 1;' > /var/www/site1/public/images/test.php
curl -sI https://site1.ru/images/test.php | head -1    # должно быть 403
rm /var/www/site1/public/images/test.php

6. Закройте служебные файлы и каталоги

Боты первым делом проверяют .git, .env, дампы баз и забытые установщики. Исключение для /.well-known/acme-challenge/ обязательно, иначе Let's Encrypt не сможет продлить сертификат.

location ^~ /.well-known/acme-challenge/ {
    allow all;
}

# Скрытые файлы и каталоги: .git, .env, .svn, .htaccess
location ~ /\. {
    return 444;
}

# Дампы и бэкапы в webroot (уберите zip, если сайт раздаёт архивы)
location ~* \.(?:sql|sql\.gz|bak|old|orig|swp|tar|tgz|zip|log)$ {
    return 444;
}

# Забытые установщики
location ~* ^/(?:installation|install|setup)(?:/|$) {
    return 444;
}

Код 444 — особенность nginx: соединение закрывается без ответа, и сканер не получает даже заголовков. А папку installation в Joomla лучше просто удалить.

7. Закройте CVE-2019-11043 двумя строками

Та самая уязвимость PHP-FPM лечится конфигом, даже если патча для вашей версии нет. Достаточно, чтобы nginx проверял существование файла до передачи запроса в FPM.

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass unix:/run/php/php5.6-fpm-site1.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
; php.ini
cgi.fix_pathinfo = 0

; пул FPM: исполнять только .php
security.limit_extensions = .php

Одна оговорка. Если сайт использует адреса вида /index.php/страница (в Joomla так бывает при выключенном mod_rewrite), после cgi.fix_pathinfo = 0 их нужно проверить.

8. Отключите опасные функции

; php.ini — строго одной строкой, переносы ломают директиву
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,putenv,imap_open,dl,show_source,phpinfo

Почему именно эти:

  • putenv в паре с mail() — классический обход через LD_PRELOAD: атакующий подкладывает свою библиотеку и выполняет код, даже если exec запрещён.
  • imap_open — CVE-2018-19518, выполнение команд через параметр подключения. Исправлено в 5.6.39, но на старых серверах часто стоят более ранние сборки 5.6.
  • pcntl_exec и dl — запуск произвольного бинарника и загрузка расширений на лету.

А вот curl_exec и parse_ini_file без анализа трогать не стоит. Через cURL работают оплата, доставка и любые API, а Joomla 3 через parse_ini_file читает языковые файлы. Ещё одна ловушка: PHPMailer в режиме sendmail использует popen, и почта с сайта может молча перестать уходить.

И честно: на PHP 5.6 disable_functions — это усложнение атаки, а не защита. Через ошибки работы с памятью в самом интерпретаторе этот список обходится. Поэтому дальше — меры, которые работают на уровне ОС.

9. Уберите phpMyAdmin и закройте MySQL

phpMyAdmin и Adminer на боевом сервере — любимая цель сканеров. Удалите их, а базой управляйте через SSH-туннель со своего компьютера.

apt remove phpmyadmin
rm -f /var/www/*/public/adminer*.php

# На своей машине: туннель к MySQL сервера
ssh -L 3307:127.0.0.1:3306 user@server
# и подключайтесь любым клиентом к 127.0.0.1:3307
# my.cnf — MySQL не должен слушать внешние интерфейсы
[mysqld]
bind-address = 127.0.0.1
-- Нет ли пользователей, доступных отовсюду, и с опасными правами
SELECT user, host FROM mysql.user WHERE host = '%';
SELECT user, host FROM mysql.user WHERE File_priv = 'Y' OR Super_priv = 'Y';

10. SSH — только по ключам

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

# /etc/ssh/sshd_config.d/00-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
sshd -t && systemctl reload ssh       # на CentOS: systemctl reload sshd
sshd -T | grep -Ei 'passwordauth|permitroot'

Имя файла с префиксом 00- выбрано не случайно. В sshd побеждает первое найденное значение, а на облачных образах Ubuntu рядом лежит 50-cloud-init.conf с PasswordAuthentication yes. Поэтому финальную проверку делайте через sshd -T, а не по содержимому файла.

Уровень 2. Серьёзная защита: день-два работы

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

Изоляция сайтов друг от друга

Когда все сайты работают от www-data, взлом одного означает взлом всех. Решение — отдельный системный пользователь и отдельный пул PHP-FPM на каждый сайт. Структура каталогов в примерах такая: /var/www/site1/{public,tmp,sessions,logs}, корень сайта — public.

useradd --system --shell /usr/sbin/nologin --home-dir /var/www/site1 site1_usr
mkdir -p /var/www/site1/{tmp,sessions,logs}
; /etc/php/5.6/fpm/pool.d/site1.conf
[site1]
user = site1_usr
group = site1_usr
listen = /run/php/php5.6-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
security.limit_extensions = .php

php_admin_value[open_basedir] = /var/www/site1/
php_admin_value[upload_tmp_dir] = /var/www/site1/tmp
php_admin_value[sys_temp_dir] = /var/www/site1/tmp
php_admin_value[session.save_path] = /var/www/site1/sessions
php_admin_value[error_log] = /var/www/site1/logs/php-error.log
php_admin_flag[allow_url_include] = off
php_admin_flag[display_errors] = off

Обратите внимание: в open_basedir нет /tmp. Общий /tmp — это дыра в изоляции, поэтому временные файлы и сессии у каждого сайта свои.

Теперь права. Принцип такой: код принадлежит отдельному пользователю deployer, PHP только читает его через группу, а писать может лишь туда, где PHP не исполняется. Без ACL nginx перестанет видеть CSS, JS и картинки, поэтому даём ему право на чтение явно.

chown -R deployer:site1_usr /var/www/site1/public
find /var/www/site1/public -type d -exec chmod 2750 {} +
find /var/www/site1/public -type f -exec chmod 0640 {} +

# nginx (www-data) должен читать статику и проходить в каталог сайта
setfacl -m u:www-data:x /var/www/site1
setfacl -R -m u:www-data:rX /var/www/site1/public
setfacl -R -d -m u:www-data:rX /var/www/site1/public

# Запись — только в папки, где PHP запрещён правилом из уровня 1
chown -R site1_usr:site1_usr /var/www/site1/{tmp,sessions,logs} \
    /var/www/site1/public/{images,cache,administrator/cache}

Проверьте, что изоляция действительно работает:

sudo -u site1_usr cat /var/www/site2/public/configuration.php   # Permission denied
sudo -u site1_usr touch /var/www/site1/public/index.php          # Permission denied
sudo -u site1_usr touch /var/www/site1/tmp/test.txt              # OK

У этой схемы есть цена. Обновить CMS или поставить расширение из админки больше не получится: обновления делаются от deployer или с временным открытием записи. Для старого проекта, который и так никто не обновляет, это скорее плюс.

Отдельный пользователь базы на каждый сайт

Сайту для работы не нужны права FILE (чтение файлов сервера через LOAD DATA INFILE), SUPER и доступ к чужим базам. Для обновлений CMS, которым нужны CREATE и ALTER, заведите второго пользователя и используйте его только на время обновления.

CREATE USER 'site1'@'localhost' IDENTIFIED BY 'длинный-случайный-пароль';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES, LOCK TABLES
    ON site1_db.* TO 'site1'@'localhost';

CREATE USER 'site1_admin'@'localhost' IDENTIFIED BY 'другой-пароль';
GRANT ALL PRIVILEGES ON site1_db.* TO 'site1_admin'@'localhost';

Ограничьте подбор паролей

Строгий лимит на весь сайт — плохая идея: посетители за одним NAT (офис, гостиница, B2B-клиент) делят один IP и получат ошибку на ровном месте. Ограничиваем только POST-запросы на страницы входа. Запросы с пустым ключом nginx не считает, поэтому GET-запросы лимит не затронет.

# http {}
map $request_method $auth_limit_key {
    POST    $binary_remote_addr;
    default "";
}
limit_req_zone $auth_limit_key zone=auth:10m rate=10r/m;
limit_req_status 429;

# server {} — внутри обязательно полноценная передача в FPM,
# иначе nginx отдаст исходник PHP как статику
location = /administrator/index.php {       # для WordPress: /wp-login.php
    limit_req zone=auth burst=10 nodelay;
    try_files $uri =404;
    fastcgi_pass unix:/run/php/php5.6-fpm-site1.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

CrowdSec: бан по поведению

CrowdSec — бесплатный open-source инструмент. Он читает логи, узнаёт сканирование, брутфорс и попытки эксплуатации известных CVE и банит IP на уровне файрвола. Плюс получает общий список атакующих адресов от сообщества.

curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | bash
apt install crowdsec crowdsec-firewall-bouncer-nftables

cscli collections install crowdsecurity/nginx crowdsecurity/base-http-scenarios \
    crowdsecurity/http-cve crowdsecurity/sshd crowdsecurity/linux

Если у каждого сайта свой лог, CrowdSec должен о них знать. Проверьте раздел Acquisition в cscli metrics: если вашего лога там нет, он не анализируется.

# /etc/crowdsec/acquis.d/nginx-sites.yaml
filenames:
  - /var/log/nginx/*access.log
  - /var/log/nginx/*error.log
labels:
  type: nginx

Свои IP добавьте в белый список, чтобы не забанить себя или редактора, который активно правит сайт:

# /etc/crowdsec/parsers/s02-enrich/my-whitelist.yaml
name: local/my-whitelist
description: "Офис и админы"
whitelist:
  reason: "свои"
  ip:
    - "203.0.113.10"

Для брутфорса админки Joomla можно написать свой сценарий. Поля событий лучше сверить на реальной строке лога командой cscli explain: сценарий с опечаткой в имени поля не выдаст ошибку, он просто никогда не сработает.

# /etc/crowdsec/scenarios/joomla-bf.yaml
type: leaky
name: local/joomla-bf
description: "Брутфорс /administrator/ в Joomla"
filter: "evt.Meta.log_type == 'http_access-log' && evt.Meta.http_verb == 'POST' && evt.Meta.http_path startsWith '/administrator/'"
groupby: evt.Meta.source_ip
capacity: 10
leakspeed: 30s
blackhole: 1m
labels:
  remediation: true
cscli explain --log '<строка из access.log>' --type nginx --verbose
systemctl reload crowdsec
cscli decisions list

Параметр blackhole часто понимают неправильно. Это не длительность бана, а пауза, в течение которой сценарий не срабатывает повторно для того же IP. Сколько длится бан, задаётся в /etc/crowdsec/profiles.yaml, по умолчанию 4 часа.

Бэкапы, которые взломщик не сможет удалить

Если бэкап лежит на том же сервере или сервер сам пишет его в хранилище с полными правами, то получивший root атакующий удалит его вместе с сайтом. Шифровальщики делают это первым делом. Правильный бэкап — версионированный, вне сервера и такой, что боевой сервер может только добавлять данные, но не удалять их.

Простой вариант — restic плюс rest-server в режиме append-only на отдельной машине.

# На бэкап-сервере
rest-server --path /srv/restic --append-only --htpasswd-file /etc/restic/.htpasswd
# На боевом сервере: /etc/restic/env (права 600)
RESTIC_REPOSITORY=rest:https://site1:ПАРОЛЬ@backup.example.ru:8000/site1
RESTIC_PASSWORD_FILE=/etc/restic/password
#!/bin/bash
# /usr/local/bin/backup.sh
set -euo pipefail
set -a; . /etc/restic/env; set +a

mysqldump --single-transaction --all-databases \
    | restic backup --stdin --stdin-filename all-databases.sql
restic backup /var/www /etc/nginx /etc/php
echo "0 3 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1" > /etc/cron.d/backup

# Чистку старых снапшотов запускайте НА бэкап-сервере, у боевого на это нет прав
restic -r /srv/restic/site1 forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Вместо rest-server подойдёт S3-хранилище с Object Lock и ключом без права удаления. И раз в месяц проверяйте восстановление: бэкап, который ни разу не разворачивали, — это надежда, а не бэкап.

restic check
restic restore latest --target /tmp/restore-test --include /var/www/site1/public/configuration.php

Узнайте о взломе за пять минут, а не за месяц

Обычно о webshell узнают по жалобам на спам или по письму от хостера. Git умеет делать это быстрее: он видит каждый новый или изменённый файл. Главное, чтобы репозиторий лежал вне webroot и принадлежал root. Иначе атакующий просто закоммитит свой shell в эталон.

mkdir -p /var/lib/integrity && chmod 700 /var/lib/integrity
git init --bare /var/lib/integrity/site1.git
G="git -c safe.directory=* -c user.name=integrity -c user.email=root@localhost \
   --git-dir=/var/lib/integrity/site1.git --work-tree=/var/www/site1/public"
# /var/lib/integrity/site1.git/info/exclude — шум не следим, но PHP в загрузках ловим
/cache/
/tmp/
/logs/
/administrator/cache/
/images/**
!/images/**/
!/images/**/*.php
$G add -A && $G commit -qm baseline
#!/bin/bash
# /usr/local/bin/check-integrity.sh
BOT_TOKEN="ваш_токен"
CHAT_ID="ваш_chat_id"

for SITE in site1 site2; do
  OUT=$(git -c safe.directory='*' --git-dir=/var/lib/integrity/$SITE.git \
        --work-tree=/var/www/$SITE/public status --porcelain 2>&1)
  if [ -n "$OUT" ]; then
    curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
      --data-urlencode "chat_id=${CHAT_ID}" \
      --data-urlencode "text=⚠️ $(hostname) / ${SITE}:
${OUT:0:3500}" > /dev/null
  fi
done
chmod 700 /usr/local/bin/check-integrity.sh
echo "*/5 * * * * root /usr/local/bin/check-integrity.sh" > /etc/cron.d/integrity

Ошибки git не глотаются: если что-то сломается, вы тоже получите сообщение. После легального деплоя обновите эталон командой $G add -A && $G commit -qm deploy. Пока вы этого не сделали, бот будет напоминать каждые пять минут. Это не баг, а фича.

Для разовой проверки уже заражённого сервера пригодится и старый добрый grep:

grep -rl --include='*.php' -E 'eval\s*\(\s*(base64_decode|gzinflate|str_rot13)' /var/www/
find /var/www -name '*.php' -mtime -3 -not -path '*/cache/*'

Проверьте саму CMS

Повторю мысль из начала: ломают обычно не PHP, а расширения. Для старой Joomla сверьте список установленных расширений с Vulnerable Extensions List, для WordPress прогоните сайт через WPScan. Всё, что не используется, удаляйте полностью, а не просто выключайте. Заодно проверьте список администраторов: «забытый» аккаунт подрядчика пятилетней давности — классика жанра.

Уровень 3. Самые упоротые шаги

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

Переезд на свежую ОС с сохранением PHP 5.6

Это мера, которая закрывает целый класс угроз ниже уровня приложения: ядро, glibc, OpenSSL, sudo, polkit. Цель — Ubuntu 24.04 LTS: стандартная поддержка до мая 2029 года, с Ubuntu Pro — до 2034-го (ubuntu.com). Старый PHP на неё ставится штатно из PPA Ondřej Surý — там есть сборки от 5.6 до 8.5 (инструкция Vultr). Ubuntu 26.04 уже вышла, но для legacy-PHP пока надёжнее 24.04 — под неё сборки 5.6 точно есть.

add-apt-repository ppa:ondrej/php
apt install php5.6-fpm php5.6-mysql php5.6-mbstring php5.6-xml php5.6-gd php5.6-curl
apt install unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades

Переезжайте на новый сервер, а не через do-release-upgrade: так чище, а старая машина остаётся точкой отката. Порядок такой:

  1. Поднять новый VPS с Ubuntu 24.04 и сразу настроить на нём уровни 1 и 2.
  2. Перенести файлы и базы, проверить сайты через подмену адреса в hosts.
  3. Снизить TTL DNS заранее, переключить записи.
  4. Держать старый сервер выключенным, но не удалённым ещё 2–4 недели.

Главные подводные камни — в базе данных:

  • PHP до 7.4 не умеет caching_sha2_password — способ аутентификации по умолчанию в MySQL 8. В MySQL 8.4 старый mysql_native_password выключен по умолчанию, а в 9.x удалён. Проще всего взять MariaDB из репозитория Ubuntu: с ней этой проблемы нет.
  • Новые версии строже к SQL: STRICT_TRANS_TABLES и ONLY_FULL_GROUP_BY роняют запросы старых CMS. Если сайт посыпался ошибками, начните с sql_mode.

Настоящий WAF вместо регулярок

Регулярки в nginx ловят только самых тупых ботов: они не видят тело POST-запроса и пропускают URL-кодирование. Настоящий WAF разбирает запрос целиком. Вариантов два.

  • ModSecurity + OWASP CRS — классика, тысячи сигнатур под SQLi, XSS, RCE и LFI. На старых CMS почти гарантированы ложные срабатывания, поэтому первые недели держите движок в режиме SecRuleEngine DetectionOnly: только логи, без блокировок. Отключайте мешающие правила точечно и лишь потом включайте блокировку.
  • CrowdSec AppSec — WAF-компонент того же CrowdSec с virtual patching: правила под конкретные CVE приходят из хаба автоматически. Требует nginx-баунсер с поддержкой Lua. Схема настройки меняется от версии к версии, поэтому идите по официальной документации.

Запретите PHP ходить в интернет

Webshell почти всегда что-то скачивает или куда-то стучится: майнер, обратная оболочка, спам-рассылка. Если пользователям PHP-пулов запрещены исходящие соединения, половина «полезной нагрузки» просто не работает. В nftables это делается по UID процесса.

# /etc/nftables.d/php-egress.nft  (UID смотрите через: id -u site1_usr)
table inet php_egress {
    set allowed {
        type ipv4_addr; flags interval;
        elements = { 192.0.2.0/24 }        # платёжный шлюз, API доставки и т.п.
    }
    chain output {
        type filter hook output priority 0; policy accept;
        meta skuid != { 998, 997 } accept  # остальные процессы не трогаем
        oifname "lo" accept                # MySQL, локальная почта, DNS-резолвер
        ct state established,related accept
        ip daddr @allowed accept
        log prefix "php-egress: " counter reject
    }
}

Ограничение тут одно: у платёжных шлюзов и API адреса меняются. Если список IP поддерживать больно, поставьте исходящий прокси со списком разрешённых доменов (Squid, tinyproxy), пропишите его в CMS и разрешите PHP-пользователям ходить только к нему.

PHP 5.6 в контейнере с read-only файловой системой

Если на сервере нужно держать и современные проекты, и древний PHP, старое окружение можно запереть в контейнер. Код монтируется только для чтения, запись разрешена лишь в загрузки, кеш и сессии, у процесса нет никаких Linux capabilities.

# Dockerfile
FROM php:5.6-fpm
RUN docker-php-ext-install mysqli pdo_mysql
COPY php.ini /usr/local/etc/php/php.ini
# docker-compose.yml
services:
  site1-php56:
    build: .
    user: "998:998"                      # UID/GID site1_usr на хосте
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    tmpfs:
      - /tmp:size=64m
    volumes:
      - /var/www/site1/public:/var/www/site1/public:ro
      - /var/www/site1/public/images:/var/www/site1/public/images:rw
      - /var/www/site1/public/cache:/var/www/site1/public/cache:rw
      - /var/www/site1/sessions:/var/www/site1/sessions:rw
      - /run/mysqld:/run/mysqld          # сокет MariaDB с хоста
    ports:
      - "127.0.0.1:9056:9000"            # nginx: fastcgi_pass 127.0.0.1:9056;
    restart: unless-stopped

Несколько деталей, на которых спотыкаются:

  • Пути внутри контейнера должны совпадать с хостовыми, иначе SCRIPT_FILENAME из nginx не найдётся.
  • PHP в контейнере ищет сокет MySQL в /tmp/mysql.sock. Пропишите в php.ini mysqli.default_socket и pdo_mysql.default_socket = /run/mysqld/mysqld.sock.
  • Образ php:5.6-fpm давно не обновляется и собран на Debian 9. Для расширений с системными зависимостями вроде gd придётся переключить apt на archive.debian.org.
  • Контейнер делит ядро с хостом. Поэтому вся затея имеет смысл только на свежей ОС: старое пользовательское окружение заперто внутри, а ядро снаружи — новое и с патчами.

Платные патчи для EOL-версий

Если переезд невозможен, а риски нужно закрыть по-взрослому, существуют коммерческие сборки старого PHP с перенесёнными исправлениями безопасности. Например, TuxCare ELS for PHP поддерживает ветки от 5.2 до 7.4 на основных дистрибутивах. Это не замена миграции, но реальные патчи CVE на время подготовки к ней.

И всё-таки миграция

Все меры выше — это способ дожить до переезда на поддерживаемый PHP. Сам переезд сейчас проще, чем кажется. PHPCompatibility для PHP_CodeSniffer показывает несовместимые места, а Rector автоматически переписывает значительную часть кода. Целиться стоит сразу в 8.4 или 8.5: поддержка 8.2 заканчивается через два месяца.

composer require --dev phpcompatibility/php-compatibility
vendor/bin/phpcs -p --standard=PHPCompatibility --runtime-set testVersion 8.4 ./

Сводный план

УровеньМераВремяРиск что-то сломать
1Проверить ОС и патчи, при EOL включить ESM30 минутНет
1Выключить Xdebug, скрыть версии10 минутНет
1Запрет PHP в папках загрузок30 минутНизкий
1Закрыть служебные файлы, try_files, cgi.fix_pathinfo30 минутНизкий, проверить URL
1disable_functions30 минутСредний, нужен тест
1Убрать phpMyAdmin, MySQL на localhost, SSH по ключам30 минутНет, если ключ уже работает
2Изоляция сайтов: пользователи, пулы, права2–4 часаСредний, нужен тест
2Лимиты на вход, CrowdSec1–2 часаНизкий
2Бэкапы append-only вне сервера2–4 часаНет
2Контроль целостности через git30 минутНет
3Переезд на Ubuntu 24.041–2 дняПлановый
3WAF, запрет исходящих, контейнер1–3 дняНужен стенд
3Миграция на PHP 8.4/8.5ИндивидуальноЗависит от проекта

Что НЕ поможет

Несколько мер, которые создают ощущение безопасности, но не саму безопасность.

«Сменим порт SSH». Сканеры перебирают все 65 535 портов за минуты. Количество мусора в логах уменьшится, количество атак — нет. Ключи вместо паролей работают, порт — нет.

«Спрячем версию PHP, и всё». Боты не читают заголовки, они пробуют все эксплойты подряд. Скрытие версии убирает вас из прицельных списков, но не закрывает ни одной дыры.

«У нас HTTPS». HTTPS шифрует канал между браузером и сервером. Эксплойту он ничуть не мешает: тот приходит по тому же зашифрованному каналу.

«Сайт маленький, кому он нужен». Никому лично. Ботам нужен ваш процессор для майнинга, ваш IP для рассылки спама и ваш домен для фишинга. Размер сайта значения не имеет.

«Заблокируем сканеры по User-Agent». sqlmap меняет User-Agent одним флагом. А под раздачу попадают Ahrefs, Semrush и прочие SEO-краулеры, которые вообще не инструменты атаки. Если вы сами делаете SEO-аудиты, заблокируете себя же.

«Антивирус на сервере найдёт». Найдёт — после заражения. Обнаружение после факта тоже ценно, но git-контроль из второго уровня делает это быстрее и точнее.

Чек-лист для админа

Эпилог

В 2008 году безопасность сайта выглядела так: поставить пароль подлиннее и не держать phpinfo.php в корне. В 2026 году я пишу правила nftables, чтобы PHP 5.6 не мог позвонить домой своему майнеру.

Прогресс налицо.

Но у этой истории хороший конец. Почти всё из первого уровня делается за вечер, второй — за пару дней, и после этого старый сайт перестаёт быть бомбой с часовым механизмом. Он становится тем, чем и должен быть: легаси, которое спокойно ждёт своей миграции.

Главное — не путать «ждёт миграции» с «не трогаем никогда». Первое — план. Второе — просто вопрос времени.

Узнали свой сервер? Давайте починим, пока не починили за вас

Если, пока вы читали, в голове всплыл конкретный сервер, — это он. Тот самый, где PHP 5.6, Ubuntu из прошлого десятилетия и «работает — не трогай». Боты о нём уже знают. Вопрос только в том, кто доберётся до него первым: вы или они.

Я занимаюсь сайтами с 2008 года и разгребал взломы, после которых от проекта оставались только бэкапы. Если, конечно, они были. Поэтому предпочитаю приходить до инцидента, а не после.

Что мы сделаем в Студии Павла Сайка:

  • Аудит за пару дней. Проверим ОС, PHP, CMS и расширения и скажем честно, где у вас дыра и насколько всё плохо. Без «надо срочно всё переписать за миллион».
  • Харденинг под ключ. Всё из этой статьи, от запрета PHP в загрузках до WAF и бэкапов, которые взломщик не удалит. Вам не придётся разбираться в nftables.
  • Переезд на свежую ОС с сохранением старого PHP. Сайт продолжит работать как работал, а под ним будет система с патчами.
  • Миграция на PHP 8.4/8.5, когда будете готовы закрыть вопрос навсегда.

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

Источники

Уязвимости PHP на старых проектах: как защитить сайт, который нельзя обновить

«Работает — не трогай» — любимая фраза владельцев сайтов на PHP 5.6. И ботов, которые такие сайты ломают. Причём чаще всего не через PHP, а через ОС под ним. Разбираем, как разорвать цепочку взлома, не переписывая ни строчки кода.

Сайт не открывается с мобильного, и это не ваш баг. Это ТСПУ

Сайт открывается с десктопа нормально, с мобильного — вечная загрузка. Не баг кода, не упавший сервер, не «у всех всё нормально». Это ТСПУ научился новым трюкам в 2025–2026. Разбираем на свежем кейсе клиента и собираем чек-лист.

Google показал будущее поиска. И оно может убить привычный интернет

Google показал AI-поиск нового поколения, где пользователь всё реже переходит на сайты. Звучит удобно, но у такой модели есть опасный побочный эффект: сайты теряют трафик, доход и мотивацию создавать контент. В итоге AI рискует уничтожить сам открытый интернет, на котором обучается. Разбираю, почему эпоха SEO-портянок заканчивается и почему реальный человеческий опыт становится важнее ключевых слов.

Почему сайт может ломаться, даже если кажется, что всё работает

Рассказываю из 17-летнего опыта, почему техническая поддержка сайта — это не развод на деньги. Реальные примеры, цены и подводные камни обслуживания.

Последние кейсы
Посмотреть все проекты
Начать проект вместе с нами
Заполните форму и отправьте
нам сообщение!
Если у Вас возникли вопросы, предложения, либо Вы желаете оформить заявку на заказ услуги — Добро пожаловать!
Контакты:
Бронзовый партнер October CMS:
Бронзовый партнер October CMS