Jak mogę bezpiecznie przechowywać hasła moich użytkowników?


169

O ile jest to bezpieczniejsze niż zwykły MD5 ? Właśnie zacząłem zajmować się bezpieczeństwem hasłem. Jestem całkiem nowy w PHP.

$salt = 'csdnfgksdgojnmfnb';

$password = md5($salt.$_POST['password']);
$result = mysql_query("SELECT id FROM users
                       WHERE username = '".mysql_real_escape_string($_POST['username'])."'
                       AND password = '$password'");

if (mysql_num_rows($result) < 1) {
    /* Access denied */
    echo "The username or password you entered is incorrect.";
} 
else {
    $_SESSION['id'] = mysql_result($result, 0, 'id');
    #header("Location: ./");
    echo "Hello $_SESSION[id]!";
}

Uwaga: php 5.4+ ma to wbudowane
Benjamin Gruenbaum,

Zobacz także framework do haszowania haseł PHP Openwall (PHPass). Jest przenośny i zabezpieczony przed wieloma typowymi atakami na hasła użytkowników.
jww

1
Obowiązkowe „używaj PDO zamiast interpolacji ciągów” dla osób, które dziś natkną się na to pytanie.
Załóż pozew Moniki

Odpowiedzi:


270

Najłatwiejszym sposobem zabezpieczenia schematu przechowywania haseł jest użycie biblioteki standardowej .

Ponieważ zabezpieczenia wydają się być o wiele bardziej skomplikowane i mają więcej niewidocznych możliwości zepsucia, niż większość programistów mogłaby rozwiązać samodzielnie, korzystanie z biblioteki standardowej jest prawie zawsze najłatwiejszą i najbezpieczniejszą (jeśli nie jedyną) dostępną opcją.


Nowy interfejs API haseł PHP (5.5.0+)

Jeśli używasz PHP w wersji 5.5.0 lub nowszej, możesz użyć nowego uproszczonego interfejsu API do mieszania haseł

Przykład kodu używającego API haseł PHP:

<?php
// $hash is what you would store in your database
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT, ['cost' => 12]);

// $hash would be the $hash (above) stored in your database for this user
$checked = password_verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

(Jeśli nadal używasz starszej wersji 5.3.7 lub nowszej, możesz zainstalować ircmaxell / password_compat, aby mieć dostęp do wbudowanych funkcji)


Poprawa po solonych haszyszach: dodaj pieprz

Jeśli chcesz dodatkowego bezpieczeństwa, specjaliści od bezpieczeństwa teraz (2017) zalecają dodanie „ pieprzu ” do (automatycznie) zasolonych skrótów haseł.

Jest prosta, upuszczona klasa, która bezpiecznie implementuje ten wzorzec, polecam: Netsilik / PepperedPasswords ( github ).
Pochodzi z licencją MIT, więc możesz go używać w dowolny sposób, nawet w projektach zastrzeżonych.

Przykład kodu wykorzystującego Netsilik/PepperedPasswords:

<?php
use Netsilik/Lib/PepperedPasswords;

// Some long, random, binary string, encoded as hexadecimal; stored in your configuration (NOT in your Database, as that would defeat the entire purpose of the pepper).
$config['pepper'] = hex2bin('012345679ABCDEF012345679ABCDEF012345679ABCDEF012345679ABCDEF');

$hasher = new PepperedPasswords($config['pepper']);

// $hash is what you would store in your database
$hash = $hasher->hash($_POST['password']);

// $hash would be the $hash (above) stored in your database for this user
$checked = $hasher->verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}


Stara biblioteka standardowa

Uwaga: nie powinieneś już tego potrzebować! To jest tutaj tylko w celach historycznych.

Spójrz na: Przenośny framework do haszowania haseł PHP : phpass i upewnij się, że używasz CRYPT_BLOWFISHalgorytmu, jeśli to w ogóle możliwe.

Przykład kodu wykorzystującego phpass (v0.2):

<?php
require('PasswordHash.php');

$pwdHasher = new PasswordHash(8, FALSE);

// $hash is what you would store in your database
$hash = $pwdHasher->HashPassword( $password );

// $hash would be the $hash (above) stored in your database for this user
$checked = $pwdHasher->CheckPassword($password, $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

PHPass został zaimplementowany w kilku dobrze znanych projektach:

  • phpBB3
  • WordPress 2.5+ oraz bbPress
  • wydanie Drupal 7 (moduł dostępny dla Drupal 5 i 6)
  • inni

Dobrą rzeczą jest to, że nie musisz martwić się o szczegóły, te szczegóły zostały zaprogramowane przez ludzi z doświadczeniem i sprawdzone przez wiele osób w Internecie.

Aby uzyskać więcej informacji na temat schematów przechowywania haseł, przeczytaj wpis w blogu Jeffa : Prawdopodobnie przechowujesz hasła nieprawidłowo

Cokolwiek zrobisz, jeśli zdecydujesz się na podejście „ Zrobię to sam, dziękuję ”, nie używaj MD5lub SHA1więcej . Są niezłym algorytmem mieszającym, ale ze względów bezpieczeństwa uważa się, że jest uszkodzony .

Obecnie najlepszym rozwiązaniem jest używanie crypt z CRYPT_BLOWFISH.
CRYPT_BLOWFISH w PHP to implementacja skrótu Bcrypt. Bcrypt jest oparty na szyfrze blokowym Blowfish, wykorzystując kosztowną konfigurację klucza, aby spowolnić algorytm.


29

Twoi użytkownicy będą znacznie bezpieczniejsi, jeśli użyjesz zapytań parametrycznych zamiast łączenia instrukcji SQL. I sól powinna być unikalna dla każdego użytkownika i powinny być przechowywane razem z hash hasła.


1
Na Nettuts + jest dobry artykuł o bezpieczeństwie w PHP, wspomina się również o solowaniu haseł. Może powinieneś rzucić
Fábio Antunes

3
Nettuts + to bardzo zły artykuł do wykorzystania jako model - zawiera użycie MD5, który można bardzo łatwo brutalnie wymusić, nawet solą. Zamiast tego po prostu użyj biblioteki PHPass, która jest o wiele, dużo lepsza niż jakikolwiek kod, który możesz znaleźć na stronie z samouczkami, tj. Ta odpowiedź: stackoverflow.com/questions/1581610/ ...
RichVel

11

Lepszym sposobem byłoby posiadanie unikalnej soli przez każdego użytkownika.

Zaletą posiadania soli jest to, że utrudnia napastnikowi wstępne wygenerowanie sygnatury MD5 każdego słowa w słowniku. Ale jeśli napastnik dowie się, że masz ustaloną sól, może następnie wstępnie wygenerować sygnaturę MD5 każdego słowa słownikowego poprzedzonego ustaloną solą.

Lepszym sposobem jest za każdym razem, gdy użytkownik zmienia swoje hasło, system generuje losową sól i przechowuje tę sól wraz z rekordem użytkownika. Sprawdzenie hasła jest nieco droższe (ponieważ musisz sprawdzić sól, zanim będziesz mógł wygenerować podpis MD5), ale znacznie utrudni atakującemu wstępne wygenerowanie MD5.


3
Sole są zwykle przechowywane razem z hashem hasła (np. Dane wyjściowe crypt()funkcji). A ponieważ i tak musisz pobrać skrót hasła, użycie soli specyficznej dla użytkownika nie spowoduje, że procedura będzie droższa. (A może miałeś na myśli generowanie nowej losowej soli jest drogie? Naprawdę nie sądzę). W przeciwnym razie +1.
Inshallah

Ze względów bezpieczeństwa możesz chcieć zapewnić dostęp do tabeli tylko za pośrednictwem procedur składowanych i zapobiec zwracaniu wartości skrótu. Zamiast tego klient przekazuje to, co uważa za skrót, i otrzymuje flagę sukcesu lub niepowodzenia. Pozwala to przechowywanemu procesowi rejestrować próbę, tworzyć sesję itp.
Steven Sudit

@Inshallah - jeśli wszyscy użytkownicy mają tę samą sól, możesz ponownie użyć ataku słownikowego, którego używasz na użytkownika1 przeciwko użytkownikowi2. Ale jeśli każdy użytkownik ma unikalną sól, będziesz musiał wygenerować nowy słownik dla każdego użytkownika, którego chcesz zaatakować.
R Samuel Klatchko

@R Samuel - właśnie dlatego zagłosowałem za Twoją odpowiedź, ponieważ zaleca najlepszą praktykę strategii unikania takich ataków. Mój komentarz miał wyrazić moje zdumienie z powodu tego, co powiedziałeś o dodatkowym koszcie soli na użytkownika, którego w ogóle nie rozumiałem. (ponieważ „sole są zwykle przechowywane razem z hashem hash”, wszelkie dodatkowe wymagania dotyczące pamięci masowej i procesora dla soli na użytkownika są tak mikroskopijne, że nie trzeba ich nawet wspominać ...)
Inshallah

@Inshallah - myślałem o przypadku, w którym masz sprawdzoną bazę danych, czy zaszyfrowane hasło jest w porządku (wtedy masz jedną bazę danych, aby uzyskać sól i drugą, aby sprawdzić zaszyfrowane hasło). Masz rację co do przypadku, w którym pobierasz sól / zaszyfrowane hasło w jednym pobraniu, a następnie dokonujesz porównania na kliencie. Przepraszam za zamieszanie.
R Samuel Klatchko

11

W przypadku PHP 5.5 (to, co opisuję, jest dostępne dla jeszcze wcześniejszych wersji, patrz poniżej) tuż za rogiem chciałbym zasugerować użycie jego nowego, wbudowanego rozwiązania: password_hash()i password_verify(). Zapewnia kilka opcji umożliwiających osiągnięcie wymaganego poziomu bezpieczeństwa hasła (na przykład poprzez określenie parametru „koszt” w $optionstablicy)

<?php
var_dump(password_hash("my-secret-password", PASSWORD_DEFAULT));

$options = array(
    'cost' => 7, // this is the number of rounds for bcrypt
    // 'salt' => 'TphfsM82o1uEKlfP9vf1f', // you could specify a salt but it is not recommended
);
var_dump(password_hash("my-secret-password", PASSWORD_BCRYPT, $options));
?>

wróci

string(60) "$2y$10$w2LxXdIcqJpD6idFTNn.eeZbKesdu5y41ksL22iI8C4/6EweI7OK."
string(60) "$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d."

Jak widać, ciąg zawiera sól, a także koszt określony w opcjach. Zawiera również zastosowany algorytm.

Dlatego podczas sprawdzania hasła (na przykład podczas logowania użytkownika), podczas korzystania z bezpłatnego password_verify() funkcji wyodrębni niezbędne parametry kryptograficzne z samego skrótu hasła.

Jeśli nie określisz soli, wygenerowany skrót hasła będzie inny przy każdym wywołaniu password_hash() ponieważ sól jest generowana losowo. Dlatego porównanie poprzedniego skrótu z nowo wygenerowanym zakończy się niepowodzeniem, nawet w przypadku poprawnego hasła.

Weryfikacja działa w ten sposób:

var_dump(password_verify("my-secret-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));
var_dump(password_verify("wrong-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));

var_dump(password_verify("my-secret-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));
var_dump(password_verify("wrong-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));

Mam nadzieję, że udostępnienie tych wbudowanych funkcji wkrótce zapewni lepsze bezpieczeństwo hasła w przypadku kradzieży danych, ponieważ zmniejsza ilość myśli, jaką programista musi włożyć w właściwą implementację.

Istnieje mała biblioteka (jeden plik PHP), która da ci PHP 5.5 password_hashw PHP 5.3.7+: https://github.com/ircmaxell/password_compat


2
W większości przypadków lepiej pominąć parametr soli. Funkcja tworzy sól z losowego źródła systemu operacyjnego, jest bardzo mała szansa, że ​​możesz samodzielnie zapewnić lepszą sól.
martinstoeckli

1
To właśnie napisałem, prawda? „jeśli nie podano soli, jest ona generowana losowo, dlatego lepiej nie podawać soli”
akirk

Większość przykładów pokazuje, jak dodać oba parametry, nawet jeśli nie zaleca się dodawania soli, więc zastanawiam się, dlaczego? I szczerze mówiąc, czytam tylko komentarz za kodem, a nie w następnym wierszu. Zresztą, czy nie byłoby lepiej, gdyby przykład pokazał, jak najlepiej korzystać z funkcji?
martinstoeckli

Masz rację, zgadzam się. Odpowiednio zmieniłem odpowiedź i zakomentowałem linię. Dzięki
akirk

jak mam sprawdzić, czy zapisane hasło i wprowadzone hasło są takie same? używam password_hash()i password_verifybez względu na to, jakiego hasła (poprawnego czy nie),
otrzymałem

0

Dla mnie w porządku. Pan Atwood napisał o sile MD5 przeciwko tęczowym stołom , a właściwie z taką długą solą siedzisz ładnie (choć niektóre przypadkowe znaki interpunkcyjne / cyfry mogą to poprawić).

Możesz również przyjrzeć się SHA-1, który wydaje się być obecnie coraz bardziej popularny.


6
Notatka na dole postu pana Atwooda (na czerwono) odsyła do innego posta od specjalisty ds. Bezpieczeństwa, który stwierdza, że ​​używanie MD5, SHA1 i innych szybkich skrótów do przechowywania haseł jest bardzo błędne.
sipwiz

2
@Matthew Scharley: Nie zgadzam się, że dodatkowy wysiłek narzucony przez drogie algorytmy haszowania haseł jest fałszywym zabezpieczeniem. Ma chronić przed brutalnym wymuszaniem łatwych do odgadnięcia haseł. Jeśli ograniczasz próby logowania, chronisz się przed tym samym (choć trochę skuteczniej). Ale jeśli przeciwnik ma dostęp do skrótów przechowywanych w DB, będzie w stanie dość szybko brutalnie wymusić takie (łatwe do odgadnięcia) hasła (w zależności od tego, jak łatwe do odgadnięcia). Wartość domyślna dla algorytmu kryptograficznego SHA-256 to 10000 rund, więc byłoby to 10000 razy trudniejsze.
Inshallah

3
Powolne skróty są w rzeczywistości tworzone przez iterację szybkiego szybkiego bardzo wiele razy i tasowanie danych między każdą iteracją. Celem jest zapewnienie, że nawet jeśli złoczyńca otrzyma kopię haszów twojego hasła, będzie musiał spalić znaczną ilość czasu procesora, aby przetestować swój słownik pod kątem twoich skrótów.
kawiarnia

4
@caf: Uważam, że algorytm bcrypt wykorzystuje parametryzowalną kosztowność planowania klucza Eksblowfish; Nie do końca jestem pewien, jak to działa, ale planowanie kluczy jest często bardzo kosztowną operacją wykonywaną podczas inicjowania obiektu kontekstu szyfrującego, zanim zostanie wykonane jakiekolwiek szyfrowanie.
Inshallah

3
Inshallah: To prawda - algorytm bcrypt to inny projekt, w którym podstawowym prymitywem kryptograficznym jest szyfr blokowy, a nie funkcja skrótu. Miałem na myśli schematy oparte na funkcjach skrótu, takie jak crypt () MD5 firmy PHK.
kawiarnia

0

Chcę dodać:

  • Nie ograniczaj haseł użytkowników według długości

Aby zapewnić kompatybilność ze starymi systemami, często ustawia się limit maksymalnej długości hasła. To jest zła zasada bezpieczeństwa: jeśli ustawisz ograniczenie, ustaw je tylko na minimalną długość haseł.

  • Nie wysyłaj haseł użytkowników pocztą e-mail

W celu odzyskania zapomnianego hasła należy przesłać adres, pod którym użytkownik może zmienić hasło.

  • Zaktualizuj skróty haseł użytkowników

Skrót hasła może być nieaktualny (parametry algorytmu mogą zostać zaktualizowane). Korzystając z funkcji password_needs_rehash()możesz to sprawdzić.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.