Ένας μυστικός σύνδεσμος μίας χρήσης είναι ένα URL που εμφανίζει το
περιεχόμενό του ακριβώς μία φορά και μετά παύει οριστικά να λειτουργεί.
Το πρόγραμμα περιήγησης του αποστολέα κρυπτογραφεί το μυστικό με
AES-GCM-256 και ο διακομιστής αποθηκεύει μόνο το κρυπτογράφημα. Το
κλειδί ταξιδεύει στο fragment του URL μετά το #k=, το οποίο τα
προγράμματα περιήγησης δεν στέλνουν ποτέ στον διακομιστή. Η πρώτη
εμφάνιση από τον παραλήπτη το αποκρυπτογραφεί και διαγράφει οριστικά το
αποθηκευμένο αντίγραφο στην ίδια λειτουργία.
Μπορείτε να φτιάξετε έναν χωρίς λογαριασμό με το δωρεάν εργαλείο μυστικών συνδέσμων.
Ο μηχανισμός είναι απλός. Το ενδιαφέρον βρίσκεται στις οριακές περιπτώσεις, και εκεί ακριβώς οι πρόχειρες υλοποιήσεις διαρρέουν χωρίς να το καταλάβει κανείς.
Η βασική ροή
- Ο αποστολέας επικολλά ένα μυστικό. Το πρόγραμμα περιήγησης δημιουργεί
ένα τυχαίο κλειδί, κρυπτογραφεί το περιεχόμενο με AES-GCM-256 μέσω του
Web Crypto API και στέλνει
μόνο το κρυπτογράφημα. Ο διακομιστής το αποθηκεύει με μια τυχαία
κατάληξη συνδέσμου που δύσκολα μαντεύεται και επιστρέφει ένα URL. Το
πρόγραμμα περιήγησης προσθέτει το κλειδί ως fragment
#k=, το οποίο δεν φεύγει ποτέ από τη συσκευή του αποστολέα. - Ο αποστολέας μοιράζεται το URL από οποιοδήποτε κανάλι: email, chat, SMS.
- Ο παραλήπτης ανοίγει το URL. Ένα απλό
GETεπιστρέφει μόνο την κατάσταση. Όταν πατήσει «Εμφάνιση», ο διακομιστής σημειώνει το μυστικό ως αναλωμένο και επιστρέφει το κρυπτογράφημα σε ένα ενιαίο, αδιαίρετο βήμα. Το πρόγραμμα περιήγησης του παραλήπτη το αποκρυπτογραφεί με το κλειδί από το fragment. - Σε κάθε επόμενη επίσκεψη εμφανίζεται, αντί για το περιεχόμενο, μια σελίδα με την ειδοποίηση ότι το περιεχόμενο έχει διαγραφεί.
Το κρίσιμο σημείο είναι το βήμα 3. Αν η ανάγνωση και η ενημέρωση της
κατάστασης γίνονται με δύο ξεχωριστά ερωτήματα, πρώτα SELECT για
έλεγχο και μετά UPDATE για σήμανση, δύο ταυτόχρονοι επισκέπτες μπορούν
να περάσουν και οι δύο τον έλεγχο πριν γράψει οποιοσδήποτε. Παίρνουν και
οι δύο το περιεχόμενο. Αυτό είναι ένα κλασικό σφάλμα TOCTOU
(time-of-check to time-of-use) και μετατρέπει την εγγύηση «ακριβώς μία
φορά» σε «συνήθως μία φορά».
Γιατί η διαγραφή πρέπει να γίνεται στο ίδιο αδιαίρετο βήμα με την ανάγνωση;
Γιατί αλλιώς δύο ταυτόχρονοι επισκέπτες μπορούν να περάσουν και οι δύο
τον έλεγχο πριν σημειώσει κάποιος το μυστικό ως αναλωμένο. Η λύση είναι
ο έλεγχος, η σήμανση και η ανάγνωση να γίνονται σε μία λειτουργία της
βάσης δεδομένων. Στην Postgres, με
UPDATE ... RETURNING,
αυτό μοιάζει έτσι:
UPDATE secrets
SET burned_at = now()
WHERE slug = $1
AND burned_at IS NULL
AND expires_at > now()
RETURNING payload;
Το κλείδωμα γραμμής που παίρνει το UPDATE σημαίνει ότι οι ταυτόχρονες
κλήσεις εκτελούνται η μία μετά την άλλη. Η πρώτη ταιριάζει με τον όρο
WHERE, αλλάζει το burned_at και παίρνει πίσω το περιεχόμενο μέσω του
RETURNING. Κάθε επόμενη κλήση βρίσκει το burned_at ήδη συμπληρωμένο,
δεν ταιριάζει με καμία γραμμή και παίρνει την κατάσταση ότι το μυστικό
έχει διαγραφεί. Δεν υπάρχει κανένα χρονικό κενό ανάμεσα στον «έλεγχο»
και στην «ανάλωση», επειδή είναι η ίδια εντολή.
Για αυτόν ακριβώς τον λόγο το LinkPilot καταγράφει τις προβολές σε ένα αδιαίρετο βήμα, μέσω μιας συνάρτησης της βάσης δεδομένων: ταυτόχρονες απόπειρες εμφάνισης δεν μπορούν να εκθέσουν το μυστικό δύο φορές. Αν αξιολογείτε ένα εργαλείο, είναι εύλογο να ρωτήσετε απευθείας τον πάροχο: γίνεται η διαγραφή στο ίδιο αδιαίρετο βήμα με την ανάγνωση;
Γιατί οι διαγραμμένοι σύνδεσμοι επιστρέφουν HTTP 410 και όχι 404;
Γιατί το 410 Gone δηλώνει ότι ο πόρος υπήρχε και δεν είναι πλέον διαθέσιμος, οριστικά. Αυτό ακριβώς είναι ένα μυστικό που έχει διαγραφεί. Μόλις αναλωθεί, ο σύνδεσμος πρέπει να αποτυγχάνει εμφανώς και οριστικά. Το LinkPilot κάνει δύο πράγματα κατά τη διαγραφή:
- Το αποθηκευμένο περιεχόμενο αντικαθίσταται με
[REDACTED]στην ίδια εγγραφή που αλλάζει την κατάσταση. Έτσι ακόμη και μια μεταγενέστερη παραβίαση της βάσης δεδομένων δεν μπορεί να ανακτήσει ένα αναλωμένο μυστικό. - Οι επόμενες απόπειρες εμφάνισης επιστρέφουν HTTP 410 Gone με μια σαφή σελίδα «έχει ήδη προβληθεί και καταστραφεί».
Το 410 είναι ο σωστός κωδικός κατάστασης: σημαίνει ότι ο πόρος υπήρχε και σκόπιμα δεν είναι πλέον διαθέσιμος, οριστικά. Το 404, αντίθετα, λέει «δεν το έχω ακούσει ποτέ». Η διάκριση μετράει και για τους ανθρώπους. Ένας παραλήπτης που βλέπει «αυτό το μυστικό έχει ήδη προβληθεί» ξέρει ότι πρέπει να ενημερώσει τον αποστολέα. Εκείνος μπορεί να ελέγξει το ιστορικό ενεργειών και να αλλάξει τα διαπιστευτήρια, αν δεν ήταν ο παραλήπτης αυτός που το είδε.
Καταλήξεις που δύσκολα μαντεύονται, και γιατί δεν αρκούν
Η κατάληξη είναι ό,τι στέκεται ανάμεσα στο δημόσιο διαδίκτυο και σε ένα κρυπτογράφημα που δεν έχει διαβαστεί (το κλειδί είναι στο fragment, όχι στον διακομιστή), οπότε πρέπει να έχει πραγματική εντροπία. Μια κατάληξη από κρυπτογραφικά ασφαλή πηγή τυχαιότητας με, ας πούμε, 22 χαρακτήρες base62 έχει περίπου 128 bit. Η απαρίθμηση δεν είναι ρεαλιστική επίθεση με οποιονδήποτε ρυθμό αιτημάτων που θα άντεχε ένας διακομιστής.
Η εντροπία της κατάληξης όμως προστατεύει μόνο από το μάντεμα. Δεν κάνει τίποτα αν το URL γίνει ορατό: ιστορικό περιήγησης, αρχεία καταγραφής proxy, προωθημένα email, κοινή χρήση οθόνης, εφαρμογές διαχείρισης προχείρου. Για αυτό οι σοβαρές υλοποιήσεις προσθέτουν επιπλέον επίπεδα:
- Λήξη, ώστε ένα URL που έγινε ορατό να πάψει να λειτουργεί στην ώρα του.
- Φράση πρόσβασης, ώστε το URL μόνο του να μην αρκεί.
- Διαγραφή μετά την ανάγνωση, ώστε ένα URL που έγινε ορατό να μην έχει καμία αξία μετά τη νόμιμη ανάγνωση.
Προεπισκοπήσεις συνδέσμων και bot
Οι εφαρμογές chat (Slack, Teams, iMessage) και οι σαρωτές ασφαλείας
email δημιουργούν προεπισκοπήσεις των URL ανοίγοντάς τα με ένα αίτημα
GET τη στιγμή που παραδίδεται το μήνυμα. Αν το endpoint εμφάνισης
διαγράφει το μυστικό με GET, ο μηχανισμός προεπισκόπησης το
καταστρέφει πριν το δει ο άνθρωπος. Στην πράξη, αυτή είναι η πιο συχνή
αστοχία των απλοϊκών εργαλείων διαγραφής μετά την ανάγνωση.
Οι άμυνες, όπως τις υλοποιεί το LinkPilot:
- Ξεχωριστές ενέργειες για τα μεταδεδομένα και την εμφάνιση. Ένα
GETστο endpoint εμφάνισης επιστρέφει μόνο την κατάσταση και αν απαιτείται φράση πρόσβασης. Μια προβολή καταγράφεται μόνο με ρητόPOST, την ενέργεια που κάνει ένας άνθρωπος όταν πατά το κουμπί «Εμφάνιση». - Φράση πρόσβασης (Pro). Όταν έχει οριστεί φράση πρόσβασης, το περιεχόμενο δεν επιστρέφεται σε κανέναν, bot ή άνθρωπο, χωρίς το σωστό hash.
Cache-Control: no-storeσε κάθε απόκριση εμφάνισης, ώστε οι ενδιάμεσοι κόμβοι να μην αποθηκεύουν ποτέ προσωρινά το περιεχόμενο των μυστικών.
Η σελίδα Αρχιτεκτονική ασφάλειας του LinkPilot περιγράφει αναλυτικά όλη αυτή τη διαδικασία, από την αρχή ως το τέλος.
Ο κύκλος ζωής, κατάσταση προς κατάσταση
| Κατάσταση | Τι την προκαλεί | Τι βλέπει ο επισκέπτης | Ανακτάται το περιεχόμενο; |
|---|---|---|---|
| Ενεργός | Δημιουργία | Σελίδα εμφάνισης (με αίτημα για φράση πρόσβασης, αν έχει οριστεί) | Ναι, απομένει μία εμφάνιση |
| Διαγράφηκε | Συμπληρώθηκε το όριο προβολών | «Έχει ήδη προβληθεί και καταστραφεί», HTTP 410 | Όχι, το περιεχόμενο έχει αντικατασταθεί |
| Έληξε | Πέρασε η προθεσμία λήξης | Σελίδα κατάστασης λήξης | Όχι, αφαιρείται στην επόμενη πρόσβαση |
| Ανακλήθηκε | Ο αποστολέας τον ανακαλεί από τον πίνακα ελέγχου | Αρνείται την εμφάνιση, HTTP 410 | Όχι, αμέσως και οριστικά |
Κάθε μετάβαση καταγράφεται στο ιστορικό ενεργειών (audit log), μια
λειτουργία του πακέτου Pro στο LinkPilot. Οι τύποι συμβάντων
είναι created, viewed, failed_passphrase, burned, expired
και revoked, ο καθένας με χρονική σήμανση και hash της IP (SHA-256 με
salt που αλλάζει καθημερινά, ενώ η ίδια η IP δεν αποθηκεύεται ποτέ).
Αυτό το ιστορικό μετατρέπει το «νομίζω ότι το πήρε» σε «προβλήθηκε στις
10:05 από την αναμενόμενη χώρα, μία προβολή, και μετά διαγράφηκε».
Συνηθισμένες επεκτάσεις
- Φράση πρόσβασης: απαιτεί μια φράση πρόσβασης που στέλνεται από άλλο κανάλι πριν από την εμφάνιση, ώστε ένα URL που διέρρευσε να μην είναι από μόνο του μυστικό που διέρρευσε. Στο LinkPilot είναι λειτουργία του πακέτου Pro (29 $/μήνα), μαζί με την προσαρμοσμένη λήξη και το ιστορικό ενεργειών. Το εργαλείο χωρίς λογαριασμό δεν μπορεί να ορίσει φράση. Βάλτε όριο στον ρυθμό των αποπειρών: το LinkPilot επιτρέπει 5 ανά IP ανά λεπτό για κάθε μυστικό και καταγράφει κάθε αποτυχία.
- Λήξη με βάση τον χρόνο: το μυστικό παύει να υπάρχει μετά από ένα ορισμένο διάστημα, ακόμη κι αν δεν διαβαστεί ποτέ. Η φόρμα του LinkPilot για συνδεδεμένους χρήστες προσφέρει 1 ώρα, 24 ώρες ή 7 ημέρες σε κάθε πακέτο, και προσαρμοσμένη ημερομηνία και ώρα στο Pro. Το εργαλείο χωρίς λογαριασμό ορίζει 7 ημέρες από προεπιλογή. Καλύπτει την περίπτωση του μηνύματος που ξεχάστηκε στα εισερχόμενα, την οποία η διαγραφή μετά την ανάγνωση δεν καλύπτει.
- Χειροκίνητη ανάκληση: ο αποστολέας μπορεί να ανακαλέσει ένα μυστικό που δεν έχει διαβαστεί από τον πίνακα ελέγχου, μόλις κάτι του φανεί περίεργο. Μια φθηνή δικλίδα για το «το έστειλα σε λάθος Alex».
Οριακές περιπτώσεις που ξεχωρίζουν τις καλές υλοποιήσεις
Το ανοίγει πρώτος λάθος άνθρωπος
Η διαγραφή μετά την ανάγνωση δεν σταματά τον λάθος παραλήπτη. Κάνει όμως την αποστολή στον λάθος άνθρωπο ορατή. Ο σωστός παραλήπτης βρίσκει μια σελίδα που λέει ότι το περιεχόμενο έχει διαγραφεί, το ιστορικό ενεργειών δείχνει μία προβολή που δεν περιμένατε, και αλλάζετε τα διαπιστευτήρια. Είναι δυσάρεστο, αλλά έχει όρια: ακριβώς μία έκθεση, που εντοπίζεται γρήγορα. Συγκρίνετέ το με ένα μυστικό επικολλημένο σε chat, όπου δεν θα το μαθαίνατε ποτέ.
Το πρόβλημα του στιγμιότυπου οθόνης
Μίας χρήσης σημαίνει μία εμφάνιση, όχι ένα αντίγραφο. Ο παραλήπτης μπορεί να τραβήξει στιγμιότυπο, να αντιγράψει ή να ξαναπληκτρολογήσει το μυστικό. Τίποτα στον σύνδεσμο δεν το εμποδίζει, και οι πάροχοι που υπονοούν το αντίθετο πουλάνε παραμύθια. Η ειλικρινής διατύπωση: η διαγραφή μετά την ανάγνωση ελέγχει το κανάλι παράδοσης, όχι τον παραλήπτη. Η εμπιστοσύνη στον παραλήπτη είναι προϋπόθεση, όχι αποτέλεσμα.
Προφόρτωση και φόρτωση κατά πρόβλεψη στο πρόγραμμα περιήγησης
Κάποια προγράμματα περιήγησης και εφαρμογές φορτώνουν εκ των προτέρων τα
URL που είναι πιθανό να πατηθούν. Επειδή ένα καλοφτιαγμένο εργαλείο
διαγράφει μόνο με ρητό POST, η προφόρτωση συμπεριφέρεται ακριβώς όπως
ένας μηχανισμός προεπισκόπησης: μπορεί να φέρει το κέλυφος της σελίδας, ποτέ το
περιεχόμενο.
Πότε να μη χρησιμοποιείτε σύνδεσμο μίας χρήσης
- Η τιμή θα χρειαστεί ξανά. Η δυνατότητα ανάκτησης είναι όλος ο σκοπός ενός διαχειριστή κωδικών πρόσβασης και ακριβώς το αντίθετο από τον σκοπό ενός συνδέσμου μίας χρήσης. Χρησιμοποιήστε ένα στοιχείο της κρύπτης.
- Το χρειάζονται πολλοί παραλήπτες. Ένας σύνδεσμος, μία ανάγνωση. Για πολλούς παραλήπτες είτε φτιάχνετε έναν σύνδεσμο για τον καθένα (κανένα πρόβλημα, και καταγράφεται) είτε παραδέχεστε ότι χρειάζεστε κοινόχρηστο έλεγχο πρόσβασης.
- Τον διαβάζει μηχανή. Οι υπηρεσίες και το CI πρέπει να διαβάζουν από έναν διαχειριστή μυστικών, όχι να πατούν συνδέσμους.
Αν θέλετε να δείτε τον μηχανισμό στην πράξη, φτιάξτε έναν με το δωρεάν εργαλείο μυστικών συνδέσμων: χωρίς λογαριασμό, κρυπτογραφημένο στο πρόγραμμα περιήγησής σας, διαγράφεται μετά την πρώτη εμφάνιση.
Σχετικά άρθρα
- Ασφαλής κοινή χρήση κωδικών πρόσβασης: σύγκριση 5 μεθόδων
- One time secret, σύνδεσμος μίας χρήσης και μυστικός σύνδεσμος: οι σύντομες σελίδες με το εργαλείο στην κορυφή
- Είναι ασφαλές το OneTimeSecret;
- Οι καλύτερες εναλλακτικές του OneTimeSecret
- OneTimeSecret, Privnote ή Password Pusher
- Σύνδεσμοι που λήγουν: το μυστικό καταστρέφεται, δεν ανακατευθύνεται απλώς
- Δωρεάν εργαλείο μυστικών συνδέσμων
Πηγές
- W3C, Web Cryptography API.
- IETF, RFC 3986, ενότητα 3.5 Fragment.
- IETF, RFC 9110: HTTP Semantics, ενότητα 15.5.11 410 Gone.
- PostgreSQL Documentation, UPDATE.
