Недавно мне выдалась случайная возможность поработать с несколькими старыми PHP-приложениями. Я заметил несколько распространённых антипаттернов, которые пришлось исправлять. Эта статья не о том, как переписывать старое PHP-приложение на <вставьте сюда название чудесного фреймворка>, а о том, как сделать его более удобным в сопровождении и менее хлопотным в работе.
Антипаттерн №1: credential’ы в коде
Это самый распространённый из худших паттернов, что мне встречались. Во многих проектах в версионированном коде зашита важная информация, например, имена и пароли для доступа к базе данных. Очевидно, что это плохая практика, потому что она не позволяет создавать локальные окружения, потому что код привязан к определённому окружению. К тому же любой, у кого есть доступ к коду, может увидеть учётные данные, обычно подходящие для эксплуатационного окружения.
Для исправления этого я предпочитаю способ, подходящий для любого приложения: устанавливаю пакет phpdotenv, который позволяет создавать файл окружения и обращаться к переменным с помощью суперпеременных окружения.
Создадим два файла:
.env.example
, который будет версионирован и служить шаблоном для файла .env
, который будет содержать учётные данные. Файл .env
не версионируется, так что добавьте его в .gitignore
. Это хорошо объяснено в официальной документации.Ваш файл
.env.example
будет перечислять учётные данные:DB_HOST=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
А сами данные будут в файле
.env
:DB_HOST=localhost
DB_DATABASE=mydb
DB_USERNAME=root
DB_PASSWORD=root
В обычном файле загрузите
.env
:$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
Затем можете обратиться к учётным данным с помощью, скажем,
$_ENV['DB_HOST']
.Не рекомендуется использовать пакет в эксплуатации «как есть», для этого лучше:
- Внедрить переменные окружения в runtime вашего контейнера, если у вас развёртывание на основе Docker, либо в серверную HTTP-конфигурацию, если это возможно.
- Закэшировать переменные окружения, чтобы избежать накладных расходов на чтение .env при каждом запросе. Вот как это делает Laravel.
Файлы с учётными данными можно удалить из истории Git.
Антипаттерн №2: не используют Composer
Раньше было очень популярно иметь папку lib с большими библиотеками вроде PHPMailer. Этого нужно всячески избегать, когда речь заходит о версионировании, так что этими зависимостями следует управлять с помощью Composer. Тогда вам будет очень легко увидеть, какая версия пакета используется, и обновить её при необходимости.
Так что поставьте Composer и используйте его для управления.
Антипаттерн №3: отсутствие локального окружения
В большинстве приложений, с которыми я работал, было только одно окружение: production.
Но избавившись от антипаттерна №1, вы сможете легко настроить локальное окружение. Возможно, часть конфигурации у вас была жёстко прописана в коде, например, пути загрузки, но теперь вы можете перенести это в
.env
.Для создания локальных окружений я использую Docker. Он особенно хорошо подходит для старых проектов, потому что в них часто применяются старые версии PHP, которые не хочется или не получается устанавливать.
Можете воспользоваться сервисом наподобие PHPDocker, или применить небольшой файл
docker-compose.yml
.Антипаттерн №4: не используют папку Public
Оказалось, что большинство этих старых проектов доступно из их корневых папок. То есть любой файл, лежащий в корне, будет доступен для публичного чтения. Это особенно плохо, когда злоумышленники (например, скрипт-кидди) попытаются напрямую обратиться к включённым файлам, ведь вы могли не определить выход, если скрипт обратится напрямую ко всем вашим включённым файлам.
Очевидно, что эта ситуация несовместима с использованием
.env
или Composer, потому что открывать папку vendor — плохая идея. Да, есть некоторые хитрости, позволяющие это сделать; но если это возможно, перенесите все открытые для клиентов PHP-файлы в папку Public
и поменяйте конфигурацию сервера, чтобы эта папка стала корневой для вашего приложения.Обычно я делаю так:
- Создаю папку
docker
для файлов, относящихся к Docker (Nginx-конфигурация, PHP Dockerfile и т.д.). - Создаю папку
app
, в которой храню бизнес-логику (сервисы, классы и т.д.). - Создаю папку
public
, в которой храню открытые для клиентов PHP-скрипты и ресурсы (JS/CSS). Это корневая папка приложения с точки зрения клиентов. - Создаю в корне файлы
.env
и.env.example
.
Антипаттерн №5: вопиющие проблемы с безопасностью
PHP-приложения, особенно старые, которые не используют фреймворки, часто страдают от вопиющих проблем с безопасностью:
- Из-за отсутствия экранирования параметров в запросе есть опасность SQL-инъекций. Чтобы их предотвратить, используйте PDO!
- Из-за отображения не экранированных пользовательских данных есть опасность XSS-инъекций. Чтобы их предотвратить, используйте htmlspecialchars.
- Загрузка файлов… это отдельная тема. Если разработчик реализовал собственную загрузку, самодельное решение, то высока вероятность, что у него возникла одна или несколько проблем с безопасностью.
- Из-за отсутствия проверки источника запроса есть опасность CSRF-атак. Рекомендую использовать пакет Anti-CSRF, который можно легко интегрировать в имеющееся приложение.
- Плохое шифрование паролей. Я видел много проектов, до сих пор использующих SHA-1 и даже MD5 для хэширования паролей. В PHP начиная с 5.5 из коробки хорошая поддержка BCrypt, стыдно этим не пользоваться. Чтобы комфортно переносить пароли, я предпочитаю обновлять хэши в базе данных по мере входов пользователей. Главное убедиться, что что колонка
password
достаточно длинная и вмещает BCrypt-пароли, вполне подходит VARCHAR(255). Вот псевдокод, чтобы было понятнее:
<?php // Пароль в старом хэше: не начинается с $ // Пароль верный: преобразуем его и журналируем пользователя if (strpos($oldPasswordHash, '$') !== 0 && hash_equals($oldPasswordHash, sha1($clearPasswordInput))) { $newPasswordHash = password_hash($clearPasswordInput, PASSWORD_DEFAULT); // Обновляем колонку password // Пользователь вошёл: возвращаем сообщение об успешности } // Пароль уже преобразован if (password_verify($clearPasswordInput, $currentPasswordHash)) { // Пользователь вошёл: возвращаем сообщение об успешности } // Пользователь не вошёл: возвращаем сообщение о неуспешности
Антипаттерн №6: отсутствие тестов
Такое очень часто встречается в старых приложениях. Вряд ли возможно начинать писать модульные тесты для всего приложения, так что можно писать функциональные тесты.
Это высокоуровневые тесты, которые помогут вам убедиться, что последующий рефакторинг приложения не сломал его. Тесты могут быть простыми, например, запускаем браузер и входим в приложение, затем ожидаем получения HTTP-кода об успешности операции и/или соответствующего сообщения на финальной странице. Для тестов можно использовать PHPUnit, или Cypress, или codeception.
Антипаттерн №7: плохая обработка ошибок
Если (или вероятнее всего, когда) что-то ломается, вам нужно побыстрее об этом узнать. Но многие старые приложения плохо обрабатывают ошибки, полагаясь на снисходительность PHP.
Вам нужно иметь возможность вылавливать и журналировать как можно больше ошибок, чтобы исправлять их. По этому теме есть хорошие статьи.
Также вам будет легче находить места, где возникают ошибки, если система будет кидать конкретные исключения.
Антипаттерн №8: глобальные переменные
Думал, я их больше не увижу, пока не начал работать со старыми проектами. Глобальные переменные делают непредсказуемым чтение и понимание поведения кода. Короче, это зло.
Лучше вместо них использовать внедрение зависимостей, потому что это позволяет вам контролировать, какие экземпляры используются, и где. Например, хорошо показал себя пакет Pimple.
Что ещё улучшить?
В зависимости от судьбы приложения или бюджета, можно предпринять ещё несколько шагов, чтобы улучшить проект.
Во-первых, если приложение работает на древней версии PHP (ниже 7), постарайтесь обновить её. Чаще всего это не доставляет больших проблем, и больше всего времени уйдёт, скорее всего, на избавление от вызовов
mysql_ calls
, если они есть. Чтобы наспех это исправить, можете воспользоваться подобной библиотекой, но лучше переписать все запросы на PDO, чтобы все параметры экранировались одновременно.Если в приложении не используется паттерн MVC, то есть бизнес-логика и шаблоны разделены, то самое время добавить библиотеку шаблонов (я знаю, что PHP шаблонный язык, но современные библиотеки гораздо удобнее), например, Smarty, Twig или Blade.
Наконец, в долгосрочной перспективе лучше будет переписать приложение на современном PHP-фреймворке вроде Laravel или Symfony. У вас будут все инструменты, необходимые для безопасной и продуманной PHP-разработки. Если приложение большое, то рекомендую использовать паттерн strangler, чтобы избежать big bang-переписывания, которое может (вероятно, так и будет) плохо закончиться. Поэтому вы можете мигрировать в новую систему те части кода, над которыми вы сейчас работаете, сохраняя старые работающие части в неприкосновенности, пока до них не дойдёт очередь.
Это эффективный подход, который позволит вам создать современное PHP-окружение для повседневной работы, избежав заморозки фич на недели или месяцы, в зависимости от проекта.
impwx
"Антипаттерн: плохая обработка ошибок. Решение: обрабатывать ошибки хорошо". И все в таком духе.
Проблема в том, что все эти "антипаттерны" поощряются дизайном языка. В нем на всех уровнях разложены грабли, облегчающие написание кода, который будет проглатывать ошибки, портить данные, предоставлять доступ злоумышленникам. И вместо того, чтобы сражаться с используемым инструментом, возможно стоит рассмотреть альтернативы, благо их сейчас очень много.
damewigit
Задам вероятно наивный вопрос, но что не так с md5 хешами паролей? Да, есть базы на сотни миллиардов вариантов паролей с их хешами, но ведь такие базы будут появляться для всех алгоритмом хеширования.
Thoth777
Двойной хеш с солью ок ок. MD5 от короткого пароля — не ок, наверное это имел в виду автор
Rukis
Наверное смотря для чего, кончено, но, в целом, нет, не ок.
VolCh
Чистый хэш от короткого пароля — не ок, с любым, наверное, известным алгоритмом
Ну и надо различать разные цели атак: подобрать пароль для конкретного хэша или по большому даму вскрыть хоть несколько паролей
VolCh
По нынешним временам md5 можно спокойно брутфорсить, даже не пользуясь обнаруженными уязвимостями. Особенно, если цель не конкретный хэш взломать, а пройтись по базе в сотню тысяч и больше пользователей и взломать как можно больше
Rukis
md5 очень быстро считается, не защищен от поиска по радужным таблицам. Почитайте, про рекомендованный в статье Bcrypt.
trawl
А причем здесь версионирование? Знаю проекты, в которых
vendor
не.gitignore
тся, бывали случаи, когда библиотеку удаляли или блокировали (mgp25/instagram-php к примеру).Сам по себе PDO не спасёт, а вот параметризованные запросы спасут (а они есть не только в PDO, например, в расширении mysqli).
Тема не раскрыта. Что использовать-то?