Εν ολίγοις: Το CSRF στην PHP το αποτρέπετε απαιτώντας από κάθε αίτημα που αλλάζει κάτι να φέρνει ένα μυστικό token δεμένο με τη συνεδρία, το οποίο ελέγχετε από την πλευρά του διακομιστή με το hash_equals(). Επιπλέον ελέγχετε τις κεφαλίδες Origin ή Sec-Fetch-Site και ορίζετε τα cookies συνεδρίας με SameSite. Τα αιτήματα GET δεν επιτρέπεται ποτέ να αλλάζουν δεδομένα, και η φόρμα σύνδεσης χρειάζεται επίσης προστασία.
Ένα κλικ που κανείς δεν προσέχει
Μια υπάλληλος είναι συνδεδεμένη στο backend του καταστήματος και ανοίγει σε μια δεύτερη καρτέλα μια ξένη σελίδα. Αυτή η σελίδα στέλνει στο παρασκήνιο μια φόρμα προς το backend, για παράδειγμα για αλλαγή μιας διεύθυνσης email ή για δημιουργία λογαριασμού. Ο browser προσθέτει αυτόματα το cookie συνεδρίας. Για την εφαρμογή το αίτημα μοιάζει με κάθε άλλο από τη δική της ομάδα.
Αυτό είναι το Cross-Site Request Forgery, εν συντομία CSRF: ένα αίτημα που ένας συνδεδεμένος χρήστης στέλνει εν αγνοία του σε μια εφαρμογή που εμπιστεύεται. Ο επιτιθέμενος δεν βλέπει την απάντηση, αλλά ούτε τη χρειάζεται. Αρκεί να εκτελεστεί η ενέργεια.
Το CSRF δεν βρίσκεται στους τίτλους όπως το PHP2Shell, είναι όμως συχνό εύρημα σε εφαρμογές PHP που αναπτύσσονται εσωτερικά. Frameworks όπως το Symfony και το Laravel φέρνουν μαζί τους προστασία· οι αδυναμίες προκύπτουν εκεί κυρίως όπου έχει απενεργοποιηθεί για μεμονωμένες διαδρομές. Οι αυτοσχέδιες υλοποιήσεις χωρίς framework συχνά δεν έχουν καθόλου.
CSRF στην PHP: ποια αιτήματα πρέπει να προστατεύονται
Προστασία χρειάζονται όλα τα αιτήματα που αλλάζουν κατάσταση: αποθήκευση, διαγραφή, παραγγελία, απόδοση δικαιωμάτων, αλλαγή ρυθμίσεων, ανέβασμα αρχείων, αποσύνδεση. Οι κλήσεις ανάγνωσης δεν χρειάζονται token, εφόσον πραγματικά μόνο διαβάζουν.
Από αυτό προκύπτει ο πρώτος κανόνας: καμία αλλαγή μέσω GET. Συνδέσμους όπως loeschen.php?id=… μπορούν να τους ενεργοποιήσουν ιδιαίτερα εύκολα ξένες σελίδες, για παράδειγμα μέσω μιας ενσωματωμένης εικόνας. Οι ενέργειες που αλλάζουν δεδομένα ανήκουν σε POST, PUT, PATCH ή DELETE.
Ο δεύτερος κανόνας: προστατεύστε και τη σύνδεση. Στο λεγόμενο CSRF σύνδεσης ένα θύμα συνδέεται απαρατήρητα σε έναν λογαριασμό του επιτιθέμενου. Αν εισαγάγει εκεί στοιχεία πληρωμής ή έγγραφα, αυτά καταλήγουν στον επιτιθέμενο. Ένα token στη φόρμα σύνδεσης και ένα νέο αναγνωριστικό συνεδρίας μετά την επιτυχή σύνδεση το εμποδίζουν.
Το πρότυπο token συγχρονισμού στην PHP
Για κλασικές εφαρμογές με συνεδρίες το OWASP συνιστά το πρότυπο του token συγχρονισμού (Synchronizer Token): ο διακομιστής δημιουργεί ένα τυχαίο token, το αποθηκεύει στη συνεδρία και το περιμένει πίσω σε κάθε φόρμα που αλλάζει δεδομένα. Σημασία έχει το token να αποστέλλεται μέσα στη φόρμα ή σε δική του κεφαλίδα και όχι σε cookie.
<?php
declare(strict_types=1);
function csrfToken(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrfPruefen(): void
{
$gesendet = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!is_string($gesendet) || !hash_equals(csrfToken(), $gesendet)) {
http_response_code(403);
exit('Anfrage abgelehnt.');
}
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
csrfPruefen();
}
?>
<form method="post" action="/konto/email">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES, 'UTF-8') ?>">
<!-- περαιτέρω πεδία -->
</form>
Το hash_equals() συγκρίνει σε σταθερό χρόνο, έτσι ώστε ο χρόνος απάντησης να μην αποκαλύπτει τίποτα για τη σωστή τιμή. Μετά τη σύνδεση πρέπει να ανανεώσετε το αναγνωριστικό συνεδρίας με το session_regenerate_id(true) και να δημιουργήσετε νέο token. Γιατί το random_bytes() είναι η σωστή πηγή και ποιες συναρτήσεις είναι ακατάλληλες, εξηγεί το Ασφαλείς τυχαίοι αριθμοί και tokens στην PHP.
Ένα token ανά συνεδρία αρκεί για τις περισσότερες εφαρμογές. Tokens ανά φόρμα ή ανά αίτημα αυξάνουν λίγο την προστασία, οδηγούν όμως σε προβλήματα με πολλές ανοιχτές καρτέλες και το κουμπί «πίσω». Σημαντικότερο από τη συχνότητα αλλαγής είναι να μην εμφανίζεται ποτέ το token σε URL, όπου μπορεί να καταλήξει σε αρχεία καταγραφής, στο ιστορικό του browser και σε στοιχεία referrer προς ξένες σελίδες.
Για εφαρμογές χωρίς κατάσταση και χωρίς συνεδρία το OWASP αναφέρει ως εναλλακτική το υπογεγραμμένο double-submit cookie: ένα token που δένεται με τη συνεδρία μέσω HMAC. Η απλή, ανυπόγραφη παραλλαγή δεν συνιστάται ρητά.
Δεύτερο επίπεδο: Origin, Sec-Fetch-Site και SameSite
Οι σύγχρονοι browsers στέλνουν με τα αιτήματα πληροφορίες για την προέλευσή τους. Η κεφαλίδα Origin αναφέρει την καλούσα σελίδα, η Sec-Fetch-Site λέει αν το αίτημα προέρχεται από την ίδια σελίδα (same-origin), μια συγγενική (same-site) ή μια ξένη (cross-site). Από αυτά μπορεί να χτιστεί ένας απλός πρόσθετος έλεγχος:
<?php
$erlaubteHerkunft = 'https://www.example.de';
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null;
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
$verdaechtig = ($fetchSite !== null && $fetchSite !== 'same-origin')
|| ($fetchSite === null && $origin !== null && $origin !== $erlaubteHerkunft);
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $verdaechtig) {
http_response_code(403);
exit;
}
Το OWASP τονίζει εδώ δύο σημεία: η προέλευση πρέπει να συγκρίνεται πλήρως, ώστε να μην περνούν domains με παρόμοιο όνομα, και μια απούσα κεφαλίδα ή η τιμή null δεν επιτρέπεται να θεωρείται δική σας προέλευση. Επειδή οι παλαιότεροι browsers δεν στέλνουν πάντα τις κεφαλίδες, το token παραμένει η κύρια άμυνα.
Το τρίτο επίπεδο είναι τα cookies συνεδρίας με SameSite. Η PHP υποστηρίζει το session.cookie_samesite από την έκδοση 7.3, η προεπιλεγμένη τιμή είναι κενή. Ορίστε τουλάχιστον Lax, για καθαρά backends συχνά είναι δυνατό το Strict. Τις υπόλοιπες ρυθμίσεις cookies περιγράφει ο οδηγός Ασφάλεια συνεδριών στην PHP.
Αν χρησιμοποιείτε CMS, συνήθως φέρνει δικά του εργαλεία. Το WordPress, για παράδειγμα, λειτουργεί με τα λεγόμενα nonces, που τα πρόσθετα ενσωματώνουν μέσω συναρτήσεων όπως οι wp_nonce_field() και check_admin_referer(). Οι αδυναμίες προκύπτουν στην πράξη κυρίως σε επεκτάσεις που ξεχνούν τέτοιους ελέγχους σε μεμονωμένες ενέργειες. Ελέγξτε γι' αυτό ιδιαίτερα πρόσθετα που γράφτηκαν από εσάς ή που είναι λίγο διαδεδομένα.
Σε διεπαφές που χρησιμοποιούν οι browsers από άλλα domains, αποφασίζει επιπλέον η διαμόρφωση CORS. Αν ο διακομιστής επιστρέφει κάθε προέλευση που ζητά ως επιτρεπτή και ταυτόχρονα επιτρέπει cookies, ακυρώνει ο ίδιος την προστασία του browser. Το OWASP συνιστά να ανοίγετε το CORS μόνο για ρητά αξιόπιστες προελεύσεις· μια γενική άδεια για όλους τους υποτομείς γίνεται πρόβλημα μόλις ένας από αυτούς περάσει σε ξένα χέρια.
Λίστα ελέγχου για υπάρχουσες εφαρμογές
Σε έργα που έχουν μεγαλώσει με τον καιρό βοηθά μια συστηματική επισκόπηση:
- Καταγράψτε όλες τις διαδρομές και τα σενάρια που αλλάζουν δεδομένα, και τα τελικά σημεία Ajax και τα ανεβάσματα αρχείων.
- Ελέγξτε αν χρησιμοποιείται γι' αυτά το GET και μετατρέψτε σε POST ή κατάλληλες μεθόδους.
- Για κάθε διαδρομή που αλλάζει δεδομένα αποδείξτε ότι ελέγχεται ένα token· αναζητήστε στοχευμένα εξαιρέσεις στο framework.
- Δοκιμάστε χωριστά τη σύνδεση, την αποσύνδεση και την αλλαγή κωδικού.
- Προσθέστε το
session.cookie_samesiteκαι τον έλεγχο προέλευσης. - Διορθώστε τις αδυναμίες XSS, γιατί ο κώδικας που εισάγεται μπορεί να διαβάσει κάθε token. Τη διαδικασία περιγράφει το Αποτροπή του Cross-Site Scripting στην PHP.
Μια προστασία CSRF που ελέγχεται μόνο στο frontend με JavaScript είναι ανίσχυρη. Ο έλεγχος πρέπει να γίνεται στον διακομιστή, προτού εκτελεστεί η ενέργεια.
Πώς βοηθά συγκεκριμένα η PHP SOS;
- Καταγράφουμε όλες τις διαδρομές της εφαρμογής PHP σας που αλλάζουν δεδομένα και ελέγχουμε πού λείπει ένα token ή έχει απενεργοποιηθεί.
- Ενσωματώνουμε ένα κεντρικό σημείο ελέγχου, αντί να διανέμουμε ελέγχους token σε πολλά σενάρια.
- Προσθέτουμε έλεγχο προέλευσης και ρυθμίσεις SameSite, χωρίς να διαταράσσουμε διαδικασίες πληρωμής ή σύνδεσης.
- Δοκιμάζουμε ξανά μετά τη μετάβαση και τεκμηριώνουμε τα αποτελέσματα.
Έναν πλήρη έλεγχο της εφαρμογής σας περιγράφει η υπηρεσία Έλεγχος ασφαλείας και δοκιμή διείσδυσης.
Συχνές ερωτήσεις
Αρκεί το SameSite=Lax ως προστασία από CSRF;
Όχι. Τα cookies SameSite αποτελούν σύμφωνα με το OWASP ένα πρόσθετο επίπεδο και όχι υποκατάστατο. Το Lax εξακολουθεί να στέλνει cookies σε κανονικές προσβάσεις σελίδων με GET, και οι επιθέσεις από υποτομείς του ίδιου ιστότοπου δεν καλύπτονται.
Χρειάζονται τα API JSON ένα token CSRF;
Αν το API πιστοποιεί με cookies, ναι, ή έναν ισοδύναμο έλεγχο. Μια εναλλακτική είναι δικές σας κεφαλίδες αιτήματος, τις οποίες οι browsers επιτρέπουν σε ξένες σελίδες μόνο μετά από προκαταρκτικό έλεγχο CORS. Τότε όμως η άδεια CORS πρέπει να περιλαμβάνει μόνο ρητά αξιόπιστες προελεύσεις.
Πρέπει να προστατεύεται και η φόρμα σύνδεσης;
Ναι. Στο CSRF σύνδεσης ένα θύμα συνδέεται απαρατήρητα σε ξένο λογαριασμό και εισάγει εκεί δικά του δεδομένα. Ένα token στη φόρμα σύνδεσης και μια νέα συνεδρία μετά τη σύνδεση το εμποδίζουν.
Προστατεύει η προστασία από CSRF και από XSS;
Το αντίστροφο: μια ευπάθεια XSS ακυρώνει κάθε προστασία από CSRF, επειδή ο κώδικας που εισάγεται μπορεί να διαβάσει και να στείλει έγκυρα tokens εκ μέρους της σελίδας. Και οι δύο αδυναμίες πρέπει να διορθωθούν χωριστά.