JWT alg:none: το bug που άνοιξε 17 τρισεκατομμύρια rows
Ένας 16χρονος ερευνητής μπήκε σε μια εσωτερική υπηρεσία analytics της Microsoft χωρίς κωδικό. Δεν έσπασε κρυπτογραφία και δεν έκλεψε credentials. Έστειλε ένα token χωρίς υπογραφή, και ο server το δέχτηκε.
{"alg":"none"}. Αν ο server δεχτεί αυτό το token ως έγκυρο, ο καθένας μπορεί να γίνει admin.
Τι έγινε
Ο ερευνητής, με το ψευδώνυμο Faav, βρήκε ότι η Titan, μια εσωτερική υπηρεσία analytics της Microsoft, έλεγχε τα περιεχόμενα ενός JWT (tenant, audience, app ID, χρήστη) αλλά δεν επαλήθευε ποτέ την υπογραφή.
Έστειλε token με alg ίσο με none, κενή υπογραφή και claim χρήστη admin. Η υπηρεσία τον αντιμετώπισε ως διαχειριστή και δέχτηκε SQL queries προς 17 βάσεις ClickHouse.
Η εύρεση βοηθήθηκε από ένα εργαλείο που έφτιαξε ο ίδιος, το Antares, το οποίο έκανε τη δουλειά της επανάληψης και του ελέγχου του login για μέρες. Το τελικό βήμα ήταν ανθρώπινη διαίσθηση.
Χρονολόγιο: αναφορά στις 5 Σεπτεμβρίου, περιορισμός πρόσβασης από τη Microsoft στις 9 Σεπτεμβρίου, bounty 5.000$ στις 17 Σεπτεμβρίου. Δεν υπάρχουν δημόσια στοιχεία κακόβουλης εκμετάλλευσης πριν τη διόρθωση.
Πώς είναι φτιαγμένο ένα JWT
Ένα JSON Web Token είναι τρία κομμάτια σε base64url, χωρισμένα με τελείες. Το base64url δεν είναι κρυπτογράφηση, οποιοσδήποτε το διαβάζει. Το μόνο που κάνει το token αξιόπιστο είναι η υπογραφή.
alg) και ο τύπος.Η ρίζα του προβλήματος
Το πρότυπο JWT επιτρέπει «unsecured» tokens με alg: none. Κάποιες βιβλιοθήκες και εφαρμογές εμπιστεύονται το alg που δηλώνει το ίδιο το token. Αυτό είναι το λάθος: ο επιτιθέμενος γράφει το header, άρα διαλέγει ο ίδιος πώς θα ελεγχθεί.
Στην Titan υπήρχαν πολλοί έλεγχοι (tenant, audience, app ID, χρήστης), και όλοι ήταν άχρηστοι. Όταν ο επιτιθέμενος ελέγχει τα claims και δεν χρειάζεται να αποδείξει ποιος τα εξέδωσε, κάθε έλεγχος από κάτω δεν προστατεύει τίποτα. Το web interface ήταν μόνο πίσω από VPN, αλλά υπήρχε ξεχωριστό API endpoint με δημόσια τεκμηρίωση.
Δοκίμασέ το στον browser σου
Ο inspector τρέχει τοπικά, τίποτα δεν στέλνεται πουθενά. Επικόλλησε ένα token ή φτιάξε ένα δικό σου unsigned για να δεις τι εντοπίζεται.
Λάθος και σωστός κώδικας
Ευάλωτο
// Αποκωδικοποιεί και εμπιστεύεται
const p = JSON.parse(
atob(token.split('.')[1])
);
if (p.aud === 'titan' && p.upn) {
user = p.upn; // ← κανείς δεν
} // έλεγξε υπογραφή
Σωστό
// jsonwebtoken (Node)
const p = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // allowlist
issuer: 'https://login.example',
audience: 'titan',
clockTolerance: 30
});
user = p.sub;
Checklist άμυνας
- Επαλήθευε πάντα την υπογραφή πριν διαβάσεις οποιοδήποτε claim. Το
decode()δεν είναι έλεγχος. - Allowlist αλγορίθμων στον server. Ποτέ μην αφήνεις το token να επιλέξει. Απέρριψε το
noneρητά. - Έλεγξε issuer και audience μετά την υπογραφή, όχι αντί για αυτήν.
- Μην αντιστοιχίζεις claims που ελέγχει ο χρήστης απευθείας σε προνομιούχους λογαριασμούς (όπως ένα
upnίσο μεadmin). - Fail closed: αν κάτι στην επαλήθευση αποτύχει ή λείπει, η απάντηση είναι 401.
- Το VPN δεν αντικαθιστά authentication. Έλεγξε ανεξάρτητα κάθε endpoint που φτάνει από έξω, ακόμα κι αν η εφαρμογή θεωρείται εσωτερική.
- Πρόσθεσε τεστ: στείλε unsigned token και token με αλλαγμένο payload και ίδια υπογραφή. Και τα δύο πρέπει να αποτυγχάνουν.
Δεν είναι καινούργιο: γνωστό από το 2015
Το 2015 ο ερευνητής Tim McLean δημοσίευσε ότι πολλές βιβλιοθήκες JWT δέχονταν alg: none και ότι μπορούσαν να εξαπατηθούν σε «σύγχυση αλγορίθμου». Οι περισσότερες διορθώθηκαν τότε. Το πρότυπο (RFC 7519) όμως εξακολουθεί να ορίζει τα «unsecured JWT», και το RFC 8725 (JWT Best Current Practices, 2020) συστήνει ρητά allowlist αλγορίθμων και απόρριψη ό,τι δεν αναμένεται.
Το ότι το ίδιο λάθος εμφανίζεται ξανά το 2026, σε εσωτερική υπηρεσία μεγάλης εταιρείας, δείχνει ότι το πρόβλημα δεν είναι τεχνικό. Είναι ζήτημα ροής εργασίας: ο κώδικας γράφεται, δοκιμάζεται με έγκυρα tokens και δεν δοκιμάζεται ποτέ με κακόβουλα.
Τρεις παραλλαγές της ίδιας ιδέας
Όλες βασίζονται στην ίδια αδυναμία: ο server αφήνει το token να επηρεάσει το πώς θα ελεγχθεί.
1. alg: none
Το token δηλώνει ότι δεν έχει υπογραφή και ο server δεν διαμαρτύρεται. Είναι η περίπτωση της Titan.
2. Σύγχυση αλγορίθμου (RS256 → HS256)
Ο server έχει δημόσιο κλειδί για RS256 (ασύμμετρη υπογραφή). Αν η βιβλιοθήκη ακολουθεί το alg του token, ο επιτιθέμενος το αλλάζει σε HS256 (συμμετρική υπογραφή) και υπογράφει με το δημόσιο κλειδί ως «μυστικό». Το δημόσιο κλειδί είναι δημόσιο, άρα ο server επαληθεύει επιτυχώς μια πλαστογραφία.
3. Header που δείχνει το κλειδί
Τα πεδία kid, jku, x5u και jwk λένε στον server ποιο κλειδί να χρησιμοποιήσει. Αν ο κώδικας τα εμπιστεύεται, ο επιτιθέμενος μπορεί να δείξει σε δικό του κλειδί ή, με ένα kid που μπαίνει ανεπεξέργαστο σε αναζήτηση αρχείου ή βάσης, να προκαλέσει path traversal ή injection.
| Επίθεση | Τι ελέγχει ο επιτιθέμενος | Η άμυνα |
|---|---|---|
| alg none | Το alg στο header και την απουσία υπογραφής | Allowlist αλγορίθμων, απόρριψη του none |
| RS256 → HS256 | Το alg, και ξέρει το δημόσιο κλειδί | Κλειδί δεμένο με αλγόριθμο, όχι με βάση το token |
| kid / jku / jwk | Από πού φορτώνεται το κλειδί | Σταθερή λίστα εμπιστευμένων κλειδιών και URL |
| Αδύναμο μυστικό HS256 | Τίποτα, σπάει offline | Μακρύ τυχαίο μυστικό ή ασύμμετρα κλειδιά |
| Χωρίς exp | Η διάρκεια ζωής | Σύντομη λήξη και έλεγχος exp |
Γιατί τέτοια bugs επιβιώνουν
- Οι έλεγχοι φαίνονται πλήρεις. Η Titan έλεγχε tenant, audience, app ID και χρήστη. Ένας reviewer που βλέπει τέσσερις ελέγχους ηρεμεί, ενώ δεν έλειπε κάποιος από τους τέσσερις αλλά ο πρώτος και πιο βασικός.
- «Εσωτερική» σημαίνει «αξιόπιστη». Όταν το UI είναι πίσω από VPN, το API που το τροφοδοτεί συχνά δεν σκέφτεται ότι μπορεί να φτάσει από έξω.
- Custom κώδικας αντί για δοκιμασμένη βιβλιοθήκη. Οι μεγάλες βιβλιοθήκες έχουν διορθώσει τα κλασικά λάθη. Το χειροποίητο parsing όχι.
- Τεστ μόνο για τη χαρούμενη διαδρομή. Λίγοι γράφουν τεστ που στέλνουν unsigned token και περιμένουν 401.
Πώς το ψάχνει ένας ερευνητής
Στη Titan, η ένδειξη ήταν ότι το payload άλλαζε ενώ η υπογραφή έμενε ίδια, και ο server συνέχιζε να δέχεται τα νέα claims. Αυτό αρκεί για να καταλάβεις ότι η υπογραφή δεν ελέγχεται. Το επόμενο βήμα είναι το token με alg: none.
Το εργαλείο Antares έκανε το επαναλαμβανόμενο κομμάτι: εντοπισμός endpoints, δοκιμές του login και καταγραφή αποκρίσεων, σε διάστημα δέκα ημερών. Η ανακάλυψη δεν ήταν καθαρά αυτόματη, αφού το τελικό βήμα ήταν ανθρώπινη υπόθεση για το πώς η εφαρμογή ερμηνεύει το πεδίο του χρήστη. Το μοτίβο έχει σημασία: το AI κάνει την υπομονετική δουλειά, ο άνθρωπος βάζει την ιδέα.
Δοκίμασε τη δική σου εφαρμογή
Αν έχεις API με JWT, τρία τεστ σε δικό σου περιβάλλον αρκούν. Και τα τρία πρέπει να επιστρέφουν 401 ή 403.
# Μόνο για δικά σου συστήματα
import base64, json, requests
def b64(d): return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=").decode()
url = "https://api.dikos-sou.example/me"
claims = {"sub": "test-user", "aud": "my-api"}
tests = {
"unsigned": b64({"alg":"none","typ":"JWT"}) + "." + b64(claims) + ".",
"alg NONE": b64({"alg":"NONE","typ":"JWT"}) + "." + b64(claims) + ".",
"garbage sig": b64({"alg":"HS256","typ":"JWT"}) + "." + b64(claims) + ".AAAA",
}
for name, tok in tests.items():
r = requests.get(url, headers={"Authorization": "Bearer " + tok})
print(name, r.status_code, "ΠΡΟΒΛΗΜΑ" if r.status_code == 200 else "ok")
Ανίχνευση και όριο ζημιάς
- Κατέγραψε τις απορρίψεις: ποιο
algζητούσε το token και από ποια IP. Μια ξαφνική αύξηση αιτημάτων μεnoneείναι σήμα σάρωσης. - Σύντομη διάρκεια ζωής: access tokens λεπτών, όχι ημερών, με refresh ξεχωριστά.
- Ελάχιστα δικαιώματα: ακόμα κι ένας έγκυρος admin δεν χρειάζεται πρόσβαση σε 17 βάσεις. Το blast radius της Titan ήταν τόσο μεγάλο επειδή ένας ρόλος έφτανε παντού.
- Κυκλοφορία κλειδιών: προγραμμάτισε rotation, ώστε μια διαρροή να έχει ημερομηνία λήξης.
Συχνές ερωτήσεις
Είναι το JWT ανασφαλές;
Όχι από μόνο του. Είναι ένα φορμάτ, και η ασφάλεια εξαρτάται από το πώς ελέγχεται. Οι περισσότερες αστοχίες προέρχονται από την υλοποίηση, όχι από το πρότυπο.
Πρέπει να το αποφύγω και να χρησιμοποιώ sessions;
Όχι απαραίτητα, αλλά αν δεν χρειάζεσαι stateless tokens για συγκεκριμένο λόγο (πολλές υπηρεσίες, τρίτα μέρη), ένα απλό server-side session είναι λιγότερο επιρρεπές σε τέτοια λάθη.
Αρκεί να χρησιμοποιώ μια γνωστή βιβλιοθήκη;
Βοηθάει πολύ, αλλά όχι αν την καλείς λάθος. Δώσε πάντα ρητά τους αλγορίθμους, τον issuer και το audience, και μην περνάς αυτό που λέει το token ως παράμετρο επαλήθευσης.
Γιατί μας νοιάζει
Το ενδιαφέρον δεν είναι το exploit, που είναι γνωστό anti-pattern εδώ και χρόνια. Είναι ότι σε μια εταιρεία με τεράστιους πόρους ασφάλειας, ένα ξεχασμένο βήμα εξουδετέρωσε πολλά στρώματα ελέγχου, και το βρήκε ένας έφηβος με ένα εργαλείο αυτοματοποίησης. Οι ίδιες μέθοδοι είναι διαθέσιμες σε όποιον ψάχνει, από τις δύο πλευρές.