Хэширование паролей в PHP: от основ до продвинутых практик

Раздел: Разработка на PHP -> Безопасность

Основы хэширования паролей в PHP

Наиболее эффективное решение для хэширования паролей в современных версиях PHP (5.5+) - использование встроенных функций password_hash() и password_verify(). Эти функции автоматически генерируют криптостойкую соль, выбирают оптимальный алгоритм (по умолчанию bcrypt, но можно указать argon2) и включают информацию о стоимости (cost factor). Такой подход защищает от радужных таблиц, атаки перебором и уязвимостей, связанных с хранением соли отдельно.

Пример базового использования:

$hash = password_hash('secret_password', PASSWORD_DEFAULT);
if (password_verify('secret_password', $hash)) {
    echo 'Пароль верен';
}

Здесь PASSWORD_DEFAULT на данный момент указывает на bcrypt, но в будущих версиях PHP может быть заменён на более сильный алгоритм (например, argon2). Соль генерируется автоматически и встраивается в хэш, поэтому хранить её отдельно не требуется.

Как хранить пароль с помощью ручной соли и устаревших алгоритмов?

Ранее разработчики часто использовали функции md5() или sha1() с добавлением фиксированной соли. Этот способ считается небезопасным, потому что:

  • Фиксированная соль не защищает от атак перебором по словарю.
  • MD5 и SHA1 - быстрые хэш-функции, позволяющие хакеру вычислять миллионы вариантов в секунду.
  • Соль хранится отдельно, что увеличивает риск её компрометации.

Пример устаревшего подхода:

$salt = 'someStaticSalt123';
$hash = md5($salt . 'user_password');
// проверка
if (md5($salt . 'user_password') === $hash) { ... }

Проблема: при использовании md5/sha1 без соли два одинаковых пароля дадут одинаковый хэш. Даже с фиксированной солью злоумышленник может заблаговременно подготовить радужные таблицы. Современные рекомендации - не применять такие функции для хэширования паролей.

Как использовать функцию crypt() напрямую для хэширования пароля?

Функция crypt() может работать с разными алгоритмами, включая bcrypt, если передать соответствующую соль. Пример:

$salt = '$2y$10$' . bin2hex(random_bytes(22));
$hash = crypt('user_password', $salt);
if (hash_equals($hash, crypt('user_password', $hash))) {
    echo 'OK';
}

Примечание: самостоятельная генерация соли и разбор хэша требуют внимательности. Рекомендуется использовать password_hash(), чтобы избежать ошибок.

Типичная ошибка: передача недостаточной соли (меньше 22 символов для bcrypt). В этом случае crypt() может вернуть хэш с другим алгоритмом (DES) или просто короткую строку. Всегда проверяйте длину соли и используйте random_bytes() для генерации криптостойких случайных данных.

Как перейти на более современные алгоритмы - argon2?

Начиная с PHP 7.2, доступен алгоритм Argon2i, а с PHP 7.3 - Argon2id. Он устойчив к атакам по side‑channel и GPU-перебору. Пример использования:

$hash = password_hash('user_password', PASSWORD_ARGON2ID, ['memory_cost' => 1<<17, 'time_cost' => 4, 'threads' => 2]);
if (password_verify('user_password', $hash)) {
    echo 'Пароль верен';
}

Параметры memory_cost (память), time_cost (итерации) и threads (потоки) настраиваются под нагрузку сервера.

Проблема: не все серверы поддерживают argon2 (требуется libargon2 или установка PHP с флагом --with-password-argon2). Также старые версии PHP (до 7.2) не имеют этой поддержки. В таких случаях лучше придерживаться bcrypt.

Как применять хэширование паролей в реальном приложении (регистрация/вход)?

При регистрации создаётся хэш пароля и сохраняется в базу данных. При входе вызывается password_verify(). Пример полного цикла:

// Регистрация
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// сохраняем $hash в БД

// Вход
$passwordFromForm = $_POST['password'];
$hashFromDB = '...';
if (password_verify($passwordFromForm, $hashFromDB)) {
    // успешный вход
} else {
    // ошибка
}

Ошибка: прямое сравнение хэша через == вместо password_verify(). Это может привести к атакам по времени (timing attack) и некорректной обработке хэшей разных алгоритмов. Всегда используйте встроенную функцию проверки.

Как хранить соль отдельно при использовании password_hash?

password_hash() уже включает соль в выходную строку, поэтому отдельное хранение соли не требуется. Тем не менее, некоторые разработчики пытаются извлечь соль из хэша и сохранять её отдельно - это избыточно. Если же требуется совместимость с легаси, следует использовать константу PASSWORD_BCRYPT и явно задавать соль (хотя это не рекомендуется).

Типичная ошибка: попытка извлечь соль из хэша с помощью explode() и затем самостоятельно сравнивать хэши. Это увеличивает риск ошибок и снижает безопасность.

Расширенные примеры хэширования паролей

Ниже представлены детальные примеры, демонстрирующие различные сценарии и нюансы.

Пример 1. Смена алгоритма и стоимости без потери совместимости

Пример
// Текущий хэш: bcrypt с cost=10
$oldHash = '$2y$10$...';
$password = 'user_input';

if (password_verify($password, $oldHash)) {
    // Пароль верен.
    // Проверяем, нужно ли обновить хэш (например, если cost изменился или алгоритм устарел)
    if (password_needs_rehash($oldHash, PASSWORD_BCRYPT, ['cost' => 12])) {
        $newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
        // сохраняем $newHash в БД
    }
}
Результат: Хэш будет пересоздан только если пароль введён верно и требуется обновление. Это позволяет постепенно обновлять хэши у старых пользователей.

Пример 2. Использование Argon2id с явными параметрами

Пример
$options = [
    'memory_cost' => 1 << 17,  // 128 MB
    'time_cost'   => 4,
    'threads'     => 2,
];
$hash = password_hash('my_par0l', PASSWORD_ARGON2ID, $options);
echo $hash;
// Проверка
var_dump(password_verify('my_par0l', $hash)); // true
var_dump(password_verify('wrong', $hash));    // false
Результат: строка вида $argon2id$v=19$m=131072,t=4,p=2$...
bool(true)
bool(false)

Пример 3. Устаревший способ: md5 + соль (только для иллюстрации)

Пример
// Генерация случайной соли
$salt = bin2hex(random_bytes(16));
// Хэш
$hash = md5($salt . 'password123');
// Проверка (безопаснее использовать hash_equals для защиты от timing attack)
$userInput = 'password123';
$storedHash = $hash; // из БД
$storedSalt = $salt; // из БД
if (hash_equals($storedHash, md5($storedSalt . $userInput))) {
    echo 'Успех';
}
Результат: Успех (но не рекомендуется из-за скорости MD5)

Пример 4. Обработка ошибки: неверная длина соли для bcrypt

Пример
$salt = '$2y$10$' . substr(bin2hex(random_bytes(20)), 0, 22); // 22 символа обязательно
$hash = crypt('test', $salt);
echo $hash;
// Если соль короче, crypt может вернуть DES-подобный хэш, начинающийся с '##...'
$badSalt = '$2y$10$abc';
echo crypt('test', $badSalt); // неверный хэш
Результат: Первый вызов возвращает корректный bcrypt-хэш. Второй - короткий хэш, который не будет распознан password_verify.

Пример 5. Использование password_hash с ручным управлением солью (не рекомендуется)

Пример
$salt = base64_encode(random_bytes(16));
$options = ['cost' => 10, 'salt' => $salt];
$hash = password_hash('parol', PASSWORD_BCRYPT, $options);
echo $hash;
// Но при проверке всё равно передаётся только хэш, соль не указывается отдельно.
Результат: Хэш будет создан с указанной солью. Однако в современных версиях PHP опция 'salt' объявлена устаревшей.

Пример 6. Проблема с устаревшими функциями: использование md5 без соли

Пример
$passCode = '123456';
$stored = md5($passCode);
// злоумышленник может найти в радужной таблице, что md5('123456') = 'e10adc3949ba59abbe56e057f20f883e'
echo $stored;
Результат: e10adc3949ba59abbe56e057f20f883e - легко поддаётся восстановлению.

Хэширование паролей в PHP - comments

En
Pass php (php)