Вопрос или проблема
Я хочу написать скрипты для входа на сайты клиентов, чтобы сделать их более безопасными. Я хочу знать, какие лучшие практики я могу внедрить в это. Существует множество панелей управления с защитой паролем, но очень немногие, похоже, реализуют лучшие практики с точки зрения написания кода, скорости и безопасности.
Я буду использовать PHP и базу данных MySQL. Раньше я использовал PBKDF2 и bcrypt для хеширования. Я использую MVC как шаблон проектирования.
Некоторые скрипты для входа записывают IP-адрес на протяжении всей сессии или даже user agent, но я хочу избежать этого, так как это несовместимо с прокси-серверами.
Я также немного отстаю от лучших практик использования сессий в PHP 5, так что некоторые лучшие практики по этому поводу будут полезны, а также пример будет очень кстати.
На самом деле, нецелесообразно в одном ответе давать полный список лучших практик безопасности для такой важной вещи, как аутентификация паролей в веб-приложениях. Даже если сузить тему до PHP, это не сильно помогает.
Несколько вещей, которые пришли на ум, исходя из сказанного вами:
- PHP является полностью автоматической “пушкой” для безопасности; возможно писать безопасный код, но намного проще допустить ошибки безопасности, чем в большинстве других языков из-за таких вещей, как нестрогая гамма объявлений переменных, нестрогая типизация с автоматическим приведением типов и основные API, которые возвращают результат, даже когда они терпят неудачу, вместо того чтобы выдать исключение или явно указать на ошибку. Вы можете смягчить по крайней мере часть из этого (и хороший линтер поможет с остальными), но вам нужно делать это последовательно. И это даже не учитывает старые ужасные ошибки безопасности PHP, такие как автоматическая привязка параметров к переменным.
- Если вы не делаете что-то очень необычное, то использование как PBKDF2, так и bcrypt для хеширования паролей маловероятно. Кроме того, это очень старые функции с заметными уязвимостями (PBKDF2 совсем не требует памяти, а bcrypt требует слишком мало памяти по современным стандартам, поэтому обе могут быть эффективно параллелизированы сильнее, чем это желательно); нет разумных причин не использовать более новые функции. Argon2id весьма надежен.
Некоторые другие советы, основанные на моем опыте тестирования на проникновение (обратите внимание, что эти советы не специфичны для PHP или MVC и могут быть избыточными в зависимости от рассматриваемого сайта):
- Даже когда люди делают все правильно с точки зрения контролей безопасности на странице входа, они часто забывают что-то на страницах для регистрации, запроса сброса пароля или выполнения самого сброса пароля.
- Токены для сброса пароля должны быть криптографически случайными, достаточно длинными, чтобы предотвратить брутфорс, краткосрочными и одноразовыми.
- Вы почти наверняка хотите использовать либо криптографически безопасные случайные токены сессии, либо JWT с токенами обновления. Хотя существуют и другие виды токенов сессии, они обычно плохо спроектированы или их библиотеки плохо реализованы (или и то, и другое). Случайные токены просты, а у JWT есть хорошие библиотеки.
- Я не могу слишком сильно подчеркнуть необходимость криптографически безопасных случайных чисел. Всякий раз, когда вам нужно что-то случайное, используйте их. MT_rand и другие подобные ПСГ, предназначенные в первую очередь для скорости, или что-либо, что предполагает сев последователя / автоматически инициализируется текущим временем или постоянным значением, НЕ безопасны.
- Если вы используете JWT, они должны иметь суперкороткий срок жизни (некоторые минуты – это хороший максимум). Вы не можете аннулировать JWT (когда пользователь выходит из системы или пытается закончить другие сессии, предполагая, что это функция, которую вы поддерживаете), вы можете только ждать, пока он истечет (и надеяться, что злоумышленник не воспользуется им в течение этого времени), поэтому держите этот срок жизни коротким!
- Вы можете помещать токены сессий как в куки, так и в пользовательские заголовки. Куки выглядят просто, но вам нужно использовать флаг secure, а также рекомендуется использовать httponly и samesite=lax. Обратите внимание, что если вы ОЧЕНЬ не уверены, что все ваши пользователи на современных браузерах и что вы полностью контролируете весь контент на домене, включая все поддомены, вам нужна дополнительная защита от CSRF при использовании куки-сессий; samesite этого недостаточно. Если вы используете пользовательские заголовки, вам не нужно беспокоиться о CSRF (если только вы не сделали очень плохие решения по CORS).
- Самое простое правильное решение CORS — не поддерживать его вообще и никогда не возвращать заголовок Access-Control-Allow-Origin. Если вы должны поддерживать CORS, разрешите только те источники, которые вы контролируете или по крайней мере полностью доверяете.
- Логируйте каждую попытку аутентификации (успешную или неуспешную), включая результат, временную метку, для кого и откуда (IP) она поступила. НИКОГДА не записывайте учетные данные (включая пароли, токены сброса пароля, токены сессии и т. д.).
- Поддерживайте многофакторную аутентификацию. Аутентификация по паролю по одному не очень безопасна в Интернете. В идеале используйте что-то более надежное, чем SMS OTP-коды. WebAuthn чрезвычайно надежен (среди прочего, это фактически единственная форма, которая предотвращает успешные фишинговые атаки) и поддерживается почти всеми устройствами и браузерами в наши дни.
- Предотвращайте атаки брутфорса на пароли. Идеально — таким образом, чтобы злонамеренные лица не могли помешать отдельным пользователям или всем остальным за одним и тем же общим IP зайти на ваш сайт. CAPTCHA или отправка пользователю ссылки по электронной почте, которую он может использовать, когда его аккаунт подвергается атаке и кто-то вводит правильный пароль, — оба варианта.
- Предотвращайте возможность угадывания или подбора паролей. Правила сложности паролей почти бесполезны, не используйте их. Вместо этого требуйте какого-то разумного минимального размера пароля — восемь символов — это минимально, 10 или 12 — лучше. И отказывайтесь от строк, таких как название сайта или имя пользователя.
- Убедитесь, что пароль не был скомпрометирован на каком-либо сайте. Используйте список / сервис, такой как https://haveibeenpwned.com/Passwords, чтобы это проверить. Не забудьте потребовать это, когда меняете/сбрасываете пароли тоже! Рассмотрите возможность проверки при каждом входе, если хотите поймать использование недавно скомпрометированных паролей. Если вы используете локальную копию списка паролей, поддерживайте ее в актуальном состоянии. Этот подход не только останавливает атаки “подбором учетных данных”, но и намного эффективнее в обнаружении каждой глупой вариации “P@ssw0rd”, которую люди могут попробовать, чем любой самодельный список.
- Используйте HTTP Strict Transport Security с долгим сроком действия (по идее, не менее года), включая поддомены, и настройте предварительную загрузку HSTS. Никогда не делайте ничего с небезопасным HTTP-запросом, кроме как перенаправить (немедленно и без другой обработки) на HTTPS.
- Если вы поддерживаете управляемое перенаправление после входа в систему, не позволяйте произвольным адресам перенаправления (открытые перенаправления) и убедитесь, что нет векторов XSS (например, что перенаправление не помещается в DOM без контекстно-зависимого экранирования).
- Лучший способ предотвратить SQL-инъекцию — заранее писать запросы на сервере базы данных и просто использовать параметры, запрещая произвольные запросы для вашего пользователя БД. MySQL поддерживает хранимые процедуры (sprocs) с жестко типизированными параметрами, и вы можете (и должны) убрать все разрешения для пользователя, кроме выполнения ваших определенных процедур (см. https://stackoverflow.com/questions/41665626). Однако вы все равно должны избегать конкатенации строк внутри sprocs! Если вы не хотите использовать sprocs, используйте параметрические запросы или подготовленные выражения (без конкатенации строк!) в любом случае, хотя это требует от пользователя БД возможности выполнять запросы, которые могут быть злоупотреблены в случае SQL-инъекции. Если вы хотите быть очень безопасными, создайте множество пользователей БД, каждый с минимальными правами для выполнения только одной задачи, так чтобы SQLi в, например, коде поиска пользователя не мог быть использован для изменения пароля пользователя.
- Не делайте вашу БД (или любые другие бэкэнд-сервисы) общедоступно доступной! Ограничьте минимально возможную поверхность атаки, которая, как правило, является только веб-приложением и ничем больше.
- Никогда не разрешайте доступ к чему-либо – как к функции, так и к объекту – просто угаданием URL. (Это называется IDOR, некорректная прямая ссылка на объект.) Если вы абсолютно ДОЛЖНЫ разрешить доступ к защищенному ресурсу исключительно по URL (например, режим “любой, у кого есть эта ссылка” в Google Docs), этот URL должен иметь очень высокую энтропию (содержать длинный, криптографически безопасный случайный токен), чтобы злоумышленники не могли его угадать или предсказать.
Это не исчерпывающий ответ, и он также не идеальным образом структурирован. Существует множество ресурсов (признаюсь, не всегда хороших) для создания систем аутентификации и управления сессиями. У OWASP есть много материалов. У StackOverflow много материалов (некоторые из них устарели, и сейчас есть лучшие подходы).
Также помните, что страницы входа и прочие являются частью веб-приложения и нуждаются во всей обычной безопасности веб-приложений. Я упомянул некоторые вещи (такие как HSTS и самое распространенное место, где я нашел XSS) в своем ответе, но есть масса других моментов!
Ответ или решение
Проектирование систем управления сессиями и скриптов для авторизации в PHP требует внимания к безопасности, производительности и лучшим практикам кодирования. Вот основные рекомендации, которые помогут вам создать надежную систему аутентификации.
1. Хранение паролей
- Используйте современные алгоритмы хеширования: Вместо PBKDF2 и bcrypt, рекомендуется использовать Argon2, который является более современным и безопасным. Он устойчив к атакам методом перебора и поддерживает параметры памяти и времени выполнения, которые можно настроить в зависимости от возможностей сервера.
- Хеширование: Храните хеши паролей, а не сами пароли. Применяйте соли к паролям, чтобы предотвратить использование заранее скомпилированных таблиц радуги (rainbow tables).
2. Управление сессиями
- Используйте безопасные сессионные токены: PHP предоставляет функции
session_start()
иsession_regenerate_id()
. Это позволит обновлять ID сессии и предотвращать атаки типа "фиксирование сессии". - Безопасные куки: Убедитесь, что куки, содержащие идентификатор сессии, имеют флаги
Secure
,HttpOnly
иSameSite=Lax
. Это поможет защитить сессии от атак межсайтового скриптинга (XSS). - Чистка сессий: Установите таймаут для сессии, чтобы автоматически завершать сессию после 15-30 минут бездействия.
3. Защита от атак
- Многослойная аутентификация: Рассмотрите возможность добавления двухфакторной аутентификации (2FA). WebAuthn является хорошим вариантом для более надежной аутентификации.
- Защита от подбора паролей: Реализуйте блокировку аккаунта после нескольких неудачных попыток входа. Также можно добавить CAPTCHA для дополнительной безопасности.
- Регистрация действий: Логируйте все попытки аутентификации (успешные и неуспешные), включая IP-адреса, временные метки и идентификатор пользователя (без хранения паролей). Это поможет в будущем выявить подозрительные действия.
4. Защита данных
- SQL инъекции: Используйте подготовленные выражения или PDO для предотвращения SQL-инъекций. Запретите выполнение произвольных SQL запросов для ваших пользователей базы данных (грубо говоря, создайте отдельного пользователя для выполнения только определенных запросов).
- Шифрование: Если вам нужно хранить чувствительные данные, рассмотрите возможность их шифрования с использованием надежных алгоритмов, таких как AES.
5. Стандарты безопасности веб-приложений
- HTTP Strict Transport Security (HSTS): Настройте HSTS, чтобы гарантировать, что ваш сайт всегда доступен по HTTPS. Это поможет защитить данные пользователей от прослушивания.
- CORS: Если ваше приложение требует поддержки CORS, разрешите только доверенные источники. Лучше всего не возвращать любой заголовок
Access-Control-Allow-Origin
, если в этом нет необходимости.
6. Рекомендации для кода
- Используйте MVC: Структурируйте свое приложение, используя паттерн проектирования MVC (Модель, Представление, Контроллер), чтобы обеспечить четкое разделение логики, что уменьшает вероятность уязвимостей.
- Чистота кода: Используйте стандарты программирования и линтеры, чтобы минимизировать возможность ошибок.
Пример простого скрипта авторизации
session_start();
require 'db_connection.php'; // Подключение к базе данных
function hashPassword($password) {
return password_hash($password, PASSWORD_ARGON2ID);
}
function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
// Получение пользователя из базы данных
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$user = $stmt->fetch();
if ($user && verifyPassword($password, $user['password_hash'])) {
// صحيح! Настройка сессии
$_SESSION['user_id'] = $user['id'];
// Перенаправление на защищенную страницу
header('Location: /dashboard.php');
} else {
echo "Неверные учетные данные!";
}
}
// Генерация ID сессии
if (!isset($_SESSION['initiated'])) {
session_regenerate_id();
$_SESSION['initiated'] = true;
}
Данный код – это лишь начальная основа. Подходите к проектированию ответственно, соблюдая предложенные рекомендации для достижения максимальной безопасности систем аутентификации и управления сессиями.