Εν ολίγοις: Ασφαλή tokens δημιουργείτε στην PHP με το random_bytes(), περίπου 32 byte, που τα μετατρέπετε σε κείμενο με bin2hex() ή Base64-URL. Για τυχαίους αριθμούς σε ένα εύρος υπάρχει το random_int(), για κωδικούς από σταθερό σύνολο χαρακτήρων, από την PHP 8.3, το Random\Randomizer::getBytesFromString(). Τα tokens τα αποθηκεύετε ως hash, περιορίζετε τη διάρκεια ισχύος τους, τα χρησιμοποιείτε μόνο μία φορά και τα συγκρίνετε με το hash_equals().
Ένας σύνδεσμος που μαντεύεται πολύ εύκολα
«Ξεχάσατε τον κωδικό σας;» Ένα κλικ και το σύστημα στέλνει έναν σύνδεσμο με έναν μακρύ κωδικό. Όποιος γνωρίζει τον σύνδεσμο, μπορεί να ορίσει τον κωδικό. Ακριβώς γι' αυτό αυτός ο κωδικός πρέπει να είναι απρόβλεπτος. Αν δεν είναι, ένας επιτιθέμενος δεν χρειάζεται ούτε το γραμματοκιβώτιο ούτε τον παλιό κωδικό, μόνο αρκετές προσπάθειες.
Σε ελέγχους εφαρμογών PHP βρίσκονται εκπληκτικά συχνά tokens που έχουν φτιαχτεί από την ώρα, το αναγνωριστικό χρήστη ή μια απλή γεννήτρια τυχαίων αριθμών. Φαίνονται μακριά και τυχαία, δεν είναι όμως. Το ίδιο ισχύει για κλειδιά API, συνδέσμους πρόσκλησης, συνδέσμους κοινοποίησης εγγράφων, κωδικούς επιβεβαίωσης και tokens CSRF.
Ο κίνδυνος είναι χειροπιαστός: ένας προβλέψιμος σύνδεσμος επαναφοράς επιτρέπει την κατάληψη κάθε λογαριασμού του οποίου η διεύθυνση email είναι γνωστή, χωρίς ο κάτοχος να προσέξει κάτι, μέχρι να μην μπορεί πια να συνδεθεί ο ίδιος.
Η PHP προσφέρει από την έκδοση 7.0 μια απλή και ασφαλή λύση. Αρκεί να χρησιμοποιείται με συνέπεια.
random_bytes στην PHP: από πού προέρχονται ασφαλείς τυχαίες τιμές
Το random_bytes() επιστρέφει σύμφωνα με το εγχειρίδιο της PHP κρυπτογραφικά ασφαλή τυχαία byte, κατάλληλα και για μακρόβια μυστικά όπως κλειδιά. Η συνάρτηση χρησιμοποιεί την πηγή τυχαιότητας του λειτουργικού συστήματος, στο Linux για παράδειγμα το getrandom(), στα Windows τη διεπαφή CNG. Αν δεν βρει κατάλληλη πηγή, πετά από την PHP 8.2 μια Random\RandomException.
Η επισκόπηση δείχνει ποιες συναρτήσεις ταιριάζουν για μυστικά:
| Συνάρτηση | Κατάλληλη για tokens; | Υπόδειξη |
|---|---|---|
random_bytes() |
ναι | ασφαλή byte, από την PHP 7.0 |
random_int() |
ναι | ασφαλής αριθμός σε εύρος, από την PHP 7.0 |
Random\Randomizer με Secure engine |
ναι | το Secure είναι σύμφωνα με το εγχειρίδιο η προεπιλεγμένη engine |
rand(), mt_rand() |
όχι | προβλέψιμες, μόνο για σκοπούς άσχετους με την ασφάλεια |
uniqid() |
όχι | βασίζεται στον χρόνο, προορίζεται για μοναδικότητα |
md5(time()), sha1($id) |
όχι | η είσοδος μαντεύεται, το hash δεν αλλάζει κάτι |
str_shuffle(), array_rand() |
όχι | δεν χρησιμοποιούν ασφαλή γεννήτρια |
Δημιουργία tokens: μήκος και κωδικοποίηση
32 τυχαία byte αντιστοιχούν σε 256 bit και για σχεδόν κάθε σκοπό είναι περισσότερα από αρκετά. Για τη μεταφορά σε συνδέσμους ή κεφαλίδες το token πρέπει να μετατραπεί σε κείμενο. Δύο παραλλαγές είναι συνηθισμένες:
<?php
declare(strict_types=1);
// Hex: 64 χαρακτήρες, μόνο 0-9 και a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 χαρακτήρες, ασφαλές σε URL χωρίς περαιτέρω κωδικοποίηση
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Τυχαίος αριθμός σε εύρος, για παράδειγμα για μια επιλογή
$index = random_int(0, 9);
// Από την PHP 8.3: εξαψήφιος αριθμητικός κωδικός, π.χ. για επιβεβαίωση με SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Οι σύντομοι αριθμητικοί κωδικοί είναι αποδεκτοί μόνο όταν ο αριθμός των προσπαθειών περιορίζεται αυστηρά και ο κωδικός λήγει μετά από λίγα λεπτά. Για συνδέσμους που στέλνονται με email, χρησιμοποιείτε πάντα το μακρύ token.
Για κλειδιά API που χρησιμοποιούν συνεργάτες ή δικά σας συστήματα μόνιμα, έχουν αποδειχθεί χρήσιμες δύο προσθήκες. Ένα σύντομο, μη μυστικό πρόθεμα πριν από το τυχαίο τμήμα, για παράδειγμα για το περιβάλλον ή τον σκοπό, διευκολύνει την αναγνώριση στα αρχεία καταγραφής και σε ευρήματα σε αποθετήρια κώδικα. Και το κλειδί εμφανίζεται μόνο μία φορά κατά τη δημιουργία· έπειτα το σύστημα γνωρίζει μόνο το hash του. Αν ένα κλειδί χαθεί, αντικαθίσταται και δεν ανακτάται.
Προσοχή σε tokens που φτιάχνετε μόνοι σας και περιέχουν πληροφορίες, για παράδειγμα αναγνωριστικό χρήστη και χρόνο λήξης σε Base64. Η κωδικοποίηση δεν είναι ασφάλιση: ο καθένας μπορεί να διαβάσει και να αλλάξει τέτοια tokens. Αν ένα token πρέπει να μεταφέρει δεδομένα, πρέπει να υπογράφεται με ένα μυστικό κλειδί, για παράδειγμα με hash_hmac('sha256', ...), και η υπογραφή να ελέγχεται με το hash_equals(). Για πιο σύνθετες περιπτώσεις, οι δοκιμασμένες βιβλιοθήκες είναι προτιμότερες από δικές σας κατασκευές.
Αποθήκευση, έλεγχος και λήξη των tokens
Ένα token που δημιουργήθηκε με ασφάλεια είναι μόνο το μισό της δουλειάς. Τουλάχιστον εξίσου σημαντικό είναι πώς το χειρίζεται η εφαρμογή. Μια δοκιμασμένη διαδικασία για συνδέσμους επαναφοράς και πρόσκλησης:
- Αποθήκευση μόνο του hash: Στη βάση δεδομένων βρίσκεται το
hash('sha256', $token), το ίδιο το token πηγαίνει στον χρήστη μόνο μέσω του συνδέσμου. - Περιορισμός της ισχύος: Αποθηκεύστε και τον χρόνο λήξης και ελέγξτε τον κατά την εξαργύρωση.
- Μία χρήση: Μετά από επιτυχή χρήση διαγράψτε την εγγραφή ή σημειώστε την ως χρησιμοποιημένη.
- Ακύρωση σε αλλαγές: Μετά από αλλαγή κωδικού, διαγράψτε όλα τα ανοιχτά tokens επαναφοράς του λογαριασμού.
- Σύγκριση σε σταθερό χρόνο: Όπου ένα token συγκρίνεται απευθείας με μια αποθηκευμένη τιμή, χρησιμοποιήστε το
hash_equals(), με την αποθηκευμένη τιμή ως πρώτη παράμετρο.
<?php
// Δημιουργία
$token = bin2hex(random_bytes(32));
$eintrag = [
'konto_id' => $kontoId,
'token_hash' => hash('sha256', $token),
'gueltig_bis'=> (new DateTimeImmutable('+30 minutes'))->format('Y-m-d H:i:s'),
];
// να σταλεί το $token μόνο στον σύνδεσμο, να αποθηκευτεί το $eintrag
// Εξαργύρωση
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// αναζήτηση της εγγραφής μέσω token_hash με prepared statement,
// έλεγχος λήξης, έπειτα άμεση διαγραφή της εγγραφής
Περιορίστε επιπλέον τον αριθμό των αιτημάτων επαναφοράς ανά λογαριασμό και χρονικό διάστημα, ώστε η λειτουργία να μην καταχραστεί για μαζική αποστολή email. Η απάντηση σε ένα αίτημα επαναφοράς δεν πρέπει επίσης να αποκαλύπτει αν η δηλωμένη διεύθυνση έχει εν γένει λογαριασμό.
Ένα γρήγορο hash όπως το SHA-256 αρκεί εδώ, επειδή το ίδιο το token είναι ήδη μακρύ και τυχαίο. Για κωδικούς που επιλέγουν άνθρωποι αυτό δεν ισχύει· εκεί χρειάζονται αργές μέθοδοι, όπως εξηγεί το Ασφαλής αποθήκευση κωδικών στην PHP.
Τα tokens δεν ανήκουν στα αρχεία καταγραφής. Ελέγξτε αν ο διακομιστής ιστού καταγράφει πλήρη URL με παραμέτρους query και αν οι αναφορές σφαλμάτων στέλνουν δεδομένα αιτήματος.
Μετά από ένα περιστατικό: ανανέωση tokens και κλειδιών
Όποιος μπορούσε να εκτελέσει κώδικα σε έναν διακομιστή, είχε κατά κανόνα πρόσβαση και σε αποθηκευμένα tokens, κλειδιά API και δεδομένα συνεδριών. Αν υπάρχει μετά το PHP2Shell ή άλλο περιστατικό υποψία εισβολής, πρέπει γι' αυτό να ανανεώσετε όχι μόνο κωδικούς, αλλά και μακρόβια tokens και κλειδιά: προσβάσεις API, κλειδιά υπογραφής για cookies ή συνδέσμους, προσβάσεις σε υπηρεσίες πληρωμών και αποστολών.
Οι συνεδρίες είναι ειδική περίπτωση: την PHP δημιουργεί η ίδια τα αναγνωριστικά συνεδρίας. Πώς ασφαλίζετε τις ρυθμίσεις τους και τερματίζετε συνεδρίες μετά από περιστατικό, περιγράφει ο οδηγός Ασφάλεια συνεδριών στην PHP. Για tokens φορμών απέναντι σε πλαστά αιτήματα δείτε Προστασία από CSRF σε εφαρμογές PHP. Περαιτέρω ενσωματωμένα εργαλεία παρουσιάζουν οι Ενσωματωμένες λειτουργίες ασφαλείας της PHP.
Πώς βοηθά συγκεκριμένα η PHP SOS;
- Εντοπίζουμε στην εφαρμογή σας όλα τα σημεία όπου δημιουργούνται tokens, κωδικοί και κλειδιά και αντικαθιστούμε τις ανασφαλείς πηγές.
- Ελέγχουμε την αποθήκευση, τη λήξη και τη μία χρήση συνδέσμων επαναφοράς, πρόσκλησης και κοινοποίησης.
- Φροντίζουμε ώστε τα tokens να μην καταλήγουν σε αρχεία καταγραφής, αναφορές σφαλμάτων ή εργαλεία ανάλυσης.
- Μετά από ένα περιστατικό σχεδιάζουμε μαζί σας την οργανωμένη αντικατάσταση tokens και κλειδιών.
Τον έλεγχο του δικού σας λογισμικού PHP προσφέρουμε ως έλεγχο ασφαλείας και δοκιμή διείσδυσης.
Συχνές ερωτήσεις
Γιατί είναι το mt_rand ακατάλληλο για tokens;
Τα mt_rand και rand προορίζονται για στατιστική και παιχνίδια, όχι για μυστικά. Οι έξοδοί τους μπορούν υπό προϋποθέσεις να προβλεφθούν. Για ό,τι δεν επιτρέπεται να μαντέψει ένας επιτιθέμενος, χρησιμοποιήστε random_bytes ή random_int.
Πόσο μακρύ πρέπει να είναι ένα token;
Για συνδέσμους επαναφοράς κωδικών, προσκλήσεις ή κλειδιά API, 32 τυχαία byte είναι μια στέρεη επιλογή, ως κείμενο hex 64 χαρακτήρες. Τα 16 byte, δηλαδή 128 bit, είναι ένα διαδεδομένο κατώτατο όριο.
Είναι το uniqid ένα ασφαλές token;
Όχι. Το uniqid βασίζεται στην τρέχουσα ώρα και προορίζεται για μοναδικότητα, όχι για μυστικότητα. Και συνδυασμοί όπως md5 της ώρας ή του αναγνωριστικού χρήστη είναι προβλέψιμοι.
Πρέπει να κρυπτογραφώ τα tokens επαναφοράς στη βάση δεδομένων;
Αποθηκεύστε αντί για το token ένα hash του, για παράδειγμα με SHA-256. Όποιος διαβάζει τη βάση δεδομένων δεν μπορεί έτσι να φτιάξει έγκυρους συνδέσμους. Επειδή το ίδιο το token είναι ήδη μακρύ και τυχαίο, δεν χρειάζεται αργή μέθοδος όπως για τους κωδικούς.
Τι συμβαίνει αν το σύστημα δεν βρει πηγή τυχαιότητας;
Τα random_bytes και random_int πετούν από την PHP 8.2 μια Random\RandomException. Πιάστε αυτή την περίπτωση και διακόψτε τη διαδικασία, αντί να καταφύγετε σε ασθενέστερη πηγή.