एक पहचान प्रदाता ने हमें बताया कि आप कौन हैं, और हमने मान लिया
सिंगल साइन-ऑन की एक चुपचाप मान्यता होती है: जब कोई उपयोगकर्ता किसी पहचान प्रदाता से आता है, तो वह प्रदाता पहले ही उसके लिए ज़मानत दे चुका होता है, इसलिए आप पासवर्ड को छोड़ सकते हैं और बस उसे अंदर आने दे सकते हैं। पूरी बात ही IdP पर भरोसा करने की है। जिस बग का हम वर्णन करने जा रहे हैं, वह इस फ़ासले में बसा था: "IdP पर भरोसा करो कि वह अपने ही उपयोगकर्ताओं को प्रमाणित करे" और "IdP जो भी तुम्हें बताए कि उसके उपयोगकर्ता कौन हैं, उस पर भरोसा करो"। ये दोनों एक ही वाक्य नहीं हैं, और इनका फ़र्क़ एक खाता-कब्ज़ा साबित हुआ।
स्थिति यह है। हमारे प्लैटफ़ॉर्म पर एक टेनेंट फ़ेडरेटेड कनेक्शन कॉन्फ़िगर कर सकता है: अपने कॉर्पोरेट IdP तक SAML, किसी और तक OIDC। एक उपयोगकर्ता उन्हीं कनेक्शनों में से किसी एक के ज़रिए साइन इन करता है, IdP एक assertion वापस भेजता है, और हमारे सर्वर को एक सवाल का जवाब देना होता है: यह कौन-सा स्थानीय खाता है? यह मैपिंग ग़लत कर दो और या तो तुम्हारे पास अपने ही खाते से बाहर बंद एक अजनबी होगा, या इससे कहीं बुरा — किसी और के खाते में टहलता हुआ एक अजनबी।
जोड़ने वाली कुंजी एक ईमेल थी, और ईमेल महज़ एक दावा है
हमारे कोड ने "यह कौन-सा खाता है" का जवाब सबसे ज़ाहिर तरीक़े से दिया। assertion एक ईमेल साथ लाती थी, सो हम उपयोगकर्ता को उसी से ढूँढते थे: FindByEmailAsync(assertedEmail)। अगर कोई मेल खाता खाता मौजूद होता, तो लौटने वाला फ़ेडरेटेड उपयोगकर्ता उसी पर हल हो जाता और साइन इन कर दिया जाता। साफ़, सरल, और ठीक उसी तरह जैसे बहुत सारे SSO एकीकरण लिखे जाते हैं।
समस्या यह है कि वह ईमेल असल में है क्या। यह कोई ऐसा तथ्य नहीं जिसे IdP ने सिद्ध किया हो। यह एक assertion के भीतर पड़ी एक स्ट्रिंग है, और assertion उतनी ही भरोसेमंद है जितना वह कनेक्शन भरोसेमंद है जिससे होकर वह आई। बहु-टेनेंट दुनिया में आप हर कनेक्शन को नियंत्रित नहीं करते। जो कोई एक खड़ा कर सकता है (अपना ख़ुद का SAML IdP, अपना ख़ुद का OIDC प्रदाता) वह ईमेल फ़ील्ड में मनचाही कोई भी स्ट्रिंग डाल सकता है। सो जिस कनेक्शन पर आप भरोसा नहीं करते वह दावा करता है email = [email protected], हमारी खोज असली CEO का खाता ढूँढ लेती है, और हमलावर उसी के रूप में साइन इन हो जाता है। न कोई पासवर्ड, न फ़िशिंग, न क्रिप्टोग्राफ़ी में कोई ख़ामी। assertion पर लगा हस्ताक्षर बिल्कुल वैध था। उसने बस एक ऐसे दावे की ज़मानत दी जिसे हमें पहचान के रूप में कभी नहीं लेना चाहिए था।
बेचैन करने वाली बात यह है कि हर एक अलग टुकड़ा काम कर रहा था। IdP ने अपने ही उपयोगकर्ता को सही ढंग से प्रमाणित किया। हस्ताक्षर सत्यापित हुआ। खाता मौजूद था। यह कब्ज़ा किसी जाँच की विफलता नहीं था; यह कुंजी के तौर पर ग़लत फ़ील्ड का इस्तेमाल था।
और जाँच क्यों हल नहीं है
लुभावनी प्रतिक्रिया यह है कि ईमेल को और सख़्ती से जाँचा जाए। email_verified अनिवार्य कर दो। डोमेन जाँच लो। एक नियम जोड़ दो। पर इस जाल का आकार ग़ौर से देखो: हमलावर assertion को नियंत्रित करता है, सो assertion से तुम जो भी गुण पढ़ते हो वह एक ऐसा गुण है जिसे हमलावर सेट कर सकता है। ख़ुद हमलावर के अपने कनेक्शन से email_verified = true माँगना तो ऐसा है जैसे मुर्ग़ीख़ाने की रखवाली का प्रमाणपत्र लोमड़ी से माँगना। ग़लत स्रोत पर भरोसा करने की स्थिति से तुम जाँच के बल पर बाहर नहीं निकल सकते।
ईमेल एक सुविधा के तौर पर ठीक है। यह एक भयानक प्राथमिक कुंजी है, क्योंकि यह वैश्विक है (वही पता कई प्रदाताओं के बीच दिख सकता है) और जाली बनाने योग्य है (यह वही है जो दावा करने वाला कनेक्शन कह दे)। एक जोड़ने वाली कुंजी को इनमें से कुछ भी नहीं होना चाहिए।
हल था कुंजी बदलना, न कि जाँचें जोड़ना
असली हल यह था कि ईमेल पर जोड़ना पूरी तरह बंद कर दिया जाए और उस एक चीज़ पर जोड़ा जाए जिसे कोई कनेक्शन ऐसे प्रदाता के लिए जाली नहीं बना सकता जिसका वह मालिक नहीं है: जोड़ी (provider, sub)। sub वह विषय पहचानकर्ता है जो प्रदाता उस उपयोगकर्ता के लिए जारी करता है, और जो उसी प्रदाता तक सीमित होता है। एक लौटने वाले उपयोगकर्ता को इस तरह हल किया जाता है कि इस कनेक्शन के (provider, sub) से पहले जुड़ी हुई स्थानीय पहचान को ढूँढा जाए। हमलावर के कनेक्शन का अपना प्रदाता id होता है; वह अपने ही नेमस्पेस के भीतर मनचाहा कोई भी sub गढ़ सकता है, पर वह किसी और के कनेक्शन का (provider, sub) पैदा नहीं कर सकता। नेमस्पेस ही वह सुरक्षा-खाई है।
ईमेल तब उस भूमिका में उतर आता है जो उसे हमेशा से ही होनी चाहिए थी: जोड़ने के लिए एक संकेत, हल करने के लिए कभी नहीं, और तभी जब उसकी ज़मानत हो। हम किसी दावा किए गए ईमेल को किसी मौजूदा खाते से तभी मिलाते हैं जब वह ईमेल किसी ऐसे स्रोत से सत्यापित हो जिस पर हम उस डोमेन के लिए सचमुच भरोसा करते हैं: किसी ऐसे कनेक्शन से आया email_verified जिसका डोमेन टेनेंट के AllowedDomains में हो। उसके बाहर, एक नई फ़ेडरेटेड पहचान को अपना ख़ुद का खाता मिलता है, किसी और का नहीं।
सबक़, निचोड़कर
जब तुम पहचान को फ़ेडरेट करते हो, तो दो सवालों को अलग रखो जिन्हें तुम मिला देने के लिए ललचाते हो। "क्या इस प्रदाता ने इस उपयोगकर्ता को प्रमाणित किया?" का जवाब हस्ताक्षर देता है। "मेरी प्रणाली में यह उपयोगकर्ता कौन है?" का जवाब एक ऐसी कुंजी से देना होगा जिसे दावा करने वाला पक्ष सीमाओं के पार जाली न बना सके, जिसका मतलब है एक प्रति-प्रदाता subject, न कि ईमेल जैसा कोई वैश्विक गुण। assertion के हर फ़ील्ड को हमलावर-नियंत्रित मानो, जब तक उस फ़ील्ड का विशिष्ट स्रोत ऐसा न हो जिस पर तुम उस विशिष्ट दावे के लिए भरोसा करते हो। ईमेल वह फ़ील्ड है जिस तक लोग सबसे पहले हाथ बढ़ाते हैं क्योंकि वह मानवीय है और अनोखा दिखता है। वह ठीक-ठीक ग़लत वाला है।
हमने इसे अपने ही प्रमाणीकरण सर्वर पर एक प्री-लॉन्च सुरक्षा-समीक्षा के हिस्से के रूप में जारी किया, इससे पहले कि कोई हम पर अपने लॉगिन का भरोसा करे। यह उसी क़िस्म का बग है जो हर परीक्षण पास कर लेता है, हर हस्ताक्षर सत्यापित कर लेता है, और जो भी सही प्रारूप में माँग ले उसी को चाबियाँ सौंप देता है। हल कोई बेहतर ताला नहीं था। हल यह एहसास था कि हम पहचान-पत्र की ग़लत पंक्ति पढ़ रहे थे।
पहचान को सुरक्षित ढंग से फ़ेडरेट करना अंततः एक ऐसी कुंजी पर जोड़ने में सिमट आता है जिसे दावा करने वाला पक्ष जाली न बना सके, और इसे ग़लत कर देना एक ऐसा खाता-कब्ज़ा है जो हर परीक्षण पास कर लेता है। Authagonal फ़ेडरेटेड उपयोगकर्ताओं को उनके प्रति-प्रदाता subject से हल करता है, कभी assertion में मौजूद ईमेल से नहीं।