← All posts

पाँच गलत पासवर्ड के बाद हमारे लॉकआउट ने कभी किसी को लॉक नहीं किया

Authagonal·August 5, 2026

खाता लॉकआउट एक बुनियादी ज़रूरत है। पाँच गलत पासवर्ड और खाता दस मिनट के लिए लॉक हो जाता है, जो ऑनलाइन पासवर्ड अनुमान लगाने को एक असली ख़तरे से बदलकर बेकार बना देता है: एक हमलावर को हर खाते पर हर दस मिनट में पाँच कोशिशें मिलती हैं, जो कुछ भी नहीं है। हमारे पास यह था। सीमा पाँच थी, खिड़की दस मिनट थी, जो कोड इसकी जाँच करता था वह सही था, और जब भी हमने इसे परखा यह हर बार बिल्कुल ठीक काम करता था, क्योंकि हर बार जब हमने इसे परखा तो हमने गलत पासवर्ड लगातार पाँच बार टाइप किया, एक के बाद एक, किसी इंसान की तरह।

हमलावर कोई इंसान नहीं है जो एक के बाद एक टाइप करता है। हमलावर एक ही बार में पाँच सौ भेजता है। और जब हमने ऐसा किया, तो जो काउंटर पाँच पर रुकने वाला था वह मुश्किल से हिला, और खाता कभी लॉक नहीं हुआ। आप एक ही खाते पर असीमित संख्या में अनुमान फेंक सकते थे, और ठीक इसी को रोकने के लिए बनाया गया एकमात्र नियंत्रण वहाँ बैठा कुछ नहीं कर रहा था, इसलिए नहीं कि तर्क गलत था बल्कि इसलिए कि उसने मान लिया था कि उससे एक बार में सिर्फ़ एक ही सवाल पूछा जाएगा।

एक काउंटर जो गिनती खो देता है

असफल-लॉगिन कोड ने वही किया जो स्पष्ट था। गलत पासवर्ड पर उसने उपयोगकर्ता को लोड किया, उसकी असफल-प्रयास गिनती में एक जोड़ा, जाँचा कि क्या नया मान पाँच तक पहुँच गया, और उपयोगकर्ता को वापस सहेज दिया। पढ़ो, बदलो, लिखो। जैसे कोई इंसान इसके बारे में सोचेगा वैसे लिखा जाए, तो यह सही है।

एक ही क्षण में आने वाले दो अनुरोधों के रूप में लिखा जाए, तो यह सही नहीं है। दोनों गिनती पढ़ते हैं, और दोनों एक ही मान पढ़ते हैं, मान लीजिए तीन। दोनों एक जोड़ते हैं और चार पाते हैं। दोनों चार वापस लिखते हैं। दो असफल प्रयास हुए, और गिनती तीन से चार हो गई। दोनों वृद्धियों में से एक बस ग़ायब हो गई, दूसरी ने उसे अधिलेखित कर दिया, क्योंकि दोनों एक ही पुराने शुरुआती बिंदु से काम कर रहे थे, और दूसरी लेखन पहली के ऊपर ऐसे उतरी मानो पहली कभी हुई ही न हो।

अब एक ही बार में पाँच सौ गलत पासवर्ड दागिए। वे ठीक इसी तरह एक-दूसरे पर ढेर हो जाते हैं, हर एक कम गिनती पढ़ता है और थोड़ी-सी कम-कम गिनती लिखता है, और वृद्धियाँ सैकड़ों की संख्या में वाष्पित हो जाती हैं। काउंटर थोड़ा ऊपर सरकता है और पाँच के आसपास कहीं भी नहीं रुकता। लॉकआउट सीमा कभी पार नहीं होती, इसलिए नहीं कि जाँच गलत है, उसे हर एक बार ईमानदारी से जाँचा जाता है, बल्कि इसलिए कि जिस संख्या को वह जाँचता है वह एक झूठ है जिसे अनुरोधों की भीड़ ने एक-दूसरे को अधिलेखित करते हुए गढ़ा है। ब्रूट-फोर्स सुरक्षा किसी तार्किक दोष से नहीं बल्कि समवर्तिता से हारी, जो उस तरह का दोष है जो हर उस क्रमिक परीक्षण को पास कर जाता है जो आप कभी लिखेंगे और उसी पल विफल हो जाता है जब वह एक असली हमलावर से मिलता है, जो अनुरोधों को समानांतर में भेजने का मकसद रखने वाला एकमात्र पक्ष है।

यह बग कहाँ रहता था, यह मायने रखता है। यह किसी भंडारण परत में दबा हुआ नहीं था। यह ठीक वहीं लॉगिन हैंडलर में था, सबकी नज़रों के सामने, एक ++ और एक सेव, दुनिया का सबसे स्वाभाविक कोड। उसे पढ़ने में कुछ भी ख़तरे का संकेत नहीं देता। बग को प्रकट होने के लिए आपको उसकी दो प्रतियों के एक साथ चलने के बारे में सोचना पड़ता है, और कोड को एक बार में एक पंक्ति पढ़ना ठीक वही मानसिक अवस्था है जिसमें आप ऐसा नहीं सोच रहे होते।

ऐसे स्टोर पर काउंटर को परमाणविक बनाना जो गिन नहीं सकता

समाधान को पूरे पढ़ो-बदलो-लिखो को अविभाज्य बनाना होगा: कोई अन्य प्रयास हमारे पढ़ने और हमारे लिखने के बीच में न घुस सके। दिलचस्प बात यह है कि हम एक ऐसे की-वैल्यू स्टोर पर हैं जिसमें कोई वृद्धि संक्रिया नहीं है। आप उसे यह नहीं कह सकते "इस फ़ील्ड में एक जोड़ो", जो वह आदिम संक्रिया है जो परमाणविक काउंटरों वाला कोई डेटाबेस आपको थमा देता। आप केवल एक पंक्ति पढ़ सकते हैं और एक पंक्ति लिख सकते हैं।

स्टोर आपको जो देता है वह एक सशर्त लेखन है। हर पंक्ति एक संस्करण टैग रखती है जो हर लेखन पर बदलता है, और आप कह सकते हैं: इस पंक्ति को वापस लिखो, लेकिन केवल तभी जब उसका संस्करण अब भी वही हो जो मैंने पढ़ा था। अगर इस बीच किसी और ने लिखा, तो संस्करण खिसक गया, और आपका लेखन उनके लेखन को चुपचाप कुचलने के बजाय अस्वीकार कर दिया जाता है।

यह खोई-हुई-वृद्धि की होड़ को एक पकड़े गए टकराव में बदल देता है। पंक्ति और उसका संस्करण पढ़ो। एक जोड़ो। उस संस्करण की शर्त पर वापस लिखने की कोशिश करो। अगर सफल हुई, तो आपकी वृद्धि निश्चित रूप से उस मान के ऊपर उतरी जो आपने पढ़ा था, बिना कुछ खोए। अगर अस्वीकार हुई, तो इसका मतलब है कि कोई और प्रयास आपसे आगे निकल गया, इसलिए आप अब-ऊँची गिनती को दोबारा पढ़ते हैं और फिर से कोशिश करते हैं। अस्वीकृति पर दोहराओ, सीमित मुट्ठी भर बार, और हर वृद्धि को पिछली के पीछे कतार में लगने के लिए मजबूर किया जाता है, बजाय उसे अधिलेखित करने के। गिनती फिर से सच्ची हो जाती है, और पाँच-सौ-एक-साथ वाला हमला अब काउंटर को सीधे पाँच तक चला ले जाता है और ताला गिरा देता है, जो कि पूरा मकसद है।

हमने वही आकार अपने दूसरे भंडारण बैकएंड पर पहुँचाया, जिसमें भी कोई वृद्धि नहीं है लेकिन जो एक सशर्त लेखन भी देता है, इस बार संस्करण टैग के बजाय स्वयं काउंटर के मान पर शर्त लगाकर: नई गिनती केवल तभी लिखो जब संग्रहीत गिनती अब भी वही हो जो मैंने पढ़ी थी। वही गारंटी, उस स्टोर की शब्दावली में व्यक्त। यह सबक किसी एक डेटाबेस से आगे तक फैलता है: जब आपके पास परमाणविक वृद्धि की कमी हो, तो एक सशर्त लेखन और एक पुनः-प्रयास लूप मिलकर उसे फिर से रच देते हैं, क्योंकि शर्त ही वह चीज़ है जो उस पुराने अधिलेखन को मना करती है जो आपकी गिनती खो रहा था।

एक जानबूझकर की गई असममिति ध्यान देने योग्य है। हमने विफलता वाले रास्ते को परमाणविक बनाया और सफलता वाले रास्ते को, जो सही लॉगिन पर काउंटर को शून्य पर रीसेट करता है, एक साधारण अंतिम-लेखक-जीतता-है लेखन के रूप में छोड़ दिया। समवर्तिता किसी रीसेट के लिए ख़तरा नहीं है: कई सफल लॉगिन जो होड़ में हैं सभी शून्य लिखना चाहते हैं, और इससे फ़र्क नहीं पड़ता कि कौन जीतता है। केवल वृद्धि ही विरोधी है, क्योंकि केवल वृद्धि ही वह चीज़ है जिसे एक हमलावर दबाए रखने की कोशिश कर रहा होता है। आप सशर्त लेखन और पुनः-प्रयास की लागत वहाँ ख़र्च करते हैं जहाँ कोई प्रतिद्वंद्वी धक्का दे रहा है, और वहाँ नहीं जहाँ वह नहीं दे रहा।

मान लें कि कोई गिन रहा है

इस बग का असली मूल्य वह मानसिक ढाँचा था जिसे इसने थोप दिया, क्योंकि जिस दिन हमने इसे ठीक किया उसी दिन हम हर उस दूसरी चीज़ की तलाश में निकले जो चुपचाप एक-अनुरोध-एक-बार मान लेती थी या मान लेती थी कि कोई माप नहीं रहा, और हमें भाई-बहनों का एक छोटा-सा समूह मिला।

हमारे लॉगिन के जवाब हमलावरों को बता रहे थे कि कौन-से खाते मौजूद हैं। एक ग़ैरमौजूद खाता, एक निष्क्रिय खाता, एक लॉक किया हुआ खाता, और एक गलत पासवर्ड, हर एक अलग पहचाना जा सकने वाला जवाब देता था, और कोई-ऐसा-उपयोगकर्ता-नहीं वाला रास्ता तो और भी तेज़ी से लौटता था क्योंकि वह पासवर्ड हैश को पूरी तरह छोड़ देता था, इसलिए एक हमलावर या तो स्थिति या घड़ी को देखकर जान सकता था कि किसके पास खाता है। हमने जवाबों को एक कर दिया: एक अनजान उपयोगकर्ता और एक गलत पासवर्ड अब बिल्कुल एक जैसा जवाब लौटाते हैं, और एक अनजान उपयोगकर्ता को अब भी एक तयशुदा नक़ली हैश के विरुद्ध सत्यापित किया जाता है ताकि समय एक जैसा रहे चाहे खाता मौजूद हो या न हो। खाता मौजूद है या नहीं, और आप बाहर से यह नहीं बता सकते।

पंजीकरण में वही रिसाव एक कठिन रूप में था, क्योंकि जब कोई ऐसा ईमेल पंजीकृत करने की कोशिश करता है जो पहले से लिया जा चुका है तो आप बस हमेशा 'सफल' नहीं कह सकते, क्योंकि इससे ऐसा लगेगा मानो आप उन्हें वह खाता सौंप रहे हैं। इसलिए पहले-से-पंजीकृत पते के लिए एक पंजीकरण अब ठीक वही तटस्थ सफलता लौटाता है जो एक नया पंजीकरण लौटाता है, वही पासवर्ड-हैश समय ख़र्च करता है, कुछ नहीं बनाता, और इसके बजाय असली मालिक को एक ईमेल भेजता है जिसमें लिखा होता है कि किसी ने उनका पता पंजीकृत करने की कोशिश की। हमलावर कुछ नहीं सीखता। असली मालिक को चुपचाप बता दिया जाता है।

और पासवर्ड-रीसेट फ़ॉर्म एक ईमेल तोप था: प्रति-पता कोई सीमा नहीं, इसलिए आप हमसे किसी भी पते पर बार-बार रीसेट ईमेल दगवा सकते थे, एक हमला जो किसी तीसरे पक्ष और हमारी अपनी भेजने की प्रतिष्ठा पर निशाना साधता है। यह अब प्रति लक्षित ईमेल दर सीमित करता है, और सीमा से ऊपर यह भेजना छोड़ देता है जबकि हमेशा की तरह वही सफलता का जवाब लौटाता है, ताकि दमन ख़ुद यह जाँचने का कोई नया तरीक़ा न बन जाए कि कौन-से पते मौजूद हैं।

इनमें से हर एक लॉकआउट बग का वही सबक है जो अलग कपड़े पहने हुए है। ऐसे डिज़ाइन करें मानो कोई हमलावर अनुरोधों को समानांतर में भेज रहा है, आपके जवाबों का समय माप रहा है, और गिन रहा है। क्योंकि जिस एक बात के बारे में आप निश्चित हो सकते हैं वह यह है कि जो व्यक्ति आपके लॉगिन पर हमला कर रहा है वह इसे एक बार में एक विनम्र अनुरोध के रूप में नहीं कर रहा।

क्यों एक सुरक्षा काउंटर को समवर्ती रूप से परखा जाना चाहिए

किसी सुरक्षा नियंत्रण में समवर्तिता का बग किसी और जगह के समवर्तिता बग से बदतर है, एक ख़ास वजह से: उसे छेड़ने का सबसे मज़बूत मकसद रखने वाला पक्ष हमलावर है, और जो स्थितियाँ उसे छेड़ती हैं, यानी बहुत-से एक साथ आने वाले अनुरोध, ठीक वही स्थितियाँ हैं जो एक हमला रचता है और एक परीक्षण लगभग कभी नहीं रचता। इसलिए एक लॉकआउट, एक दर सीमा, एक कोटा, कोई भी नियंत्रण जो एक काउंटर पर बना है, उसे एक लूप में बढ़ाकर और उसके गिरने को देखकर मान्य नहीं किया जा सकता। उसे समवर्ती रूप से मारा जाना चाहिए, क्योंकि समवर्तिता हमलावर का अपना माहौल है, और परीक्षण का क्रमिक संस्करण केवल यह साबित करता है कि आपका नियंत्रण एक ऐसे विरोधी के ख़िलाफ़ काम करता है जो विनम्र होने के लिए राज़ी हो गया है।

और जब आप काउंटर को सख़्त बनाते ही हैं, तो परमाणविक वृद्धियों से रहित किसी स्टोर पर औज़ार सशर्त लेखन है: संस्करण पढ़ो, केवल तभी लिखो जब वह न हिला हो, अगर हिल गया हो तो फिर से कोशिश करो। यह उस परमाणविक वृद्धि को फिर से रच देता है जो डेटाबेस ने आपको नहीं दी, उस एकमात्र गारंटी से जो उसने दी।

अगर आप बल्कि किसी ऐसे पहचान प्रदाता पर चलना चाहें जो पहले से यह मान लेता है कि हमलावर समानांतर में गिन रहा है, तो Authagonal ने इन नियंत्रणों को इस तरह बनाया कि वे सभी एक साथ मारे जा सकें, क्योंकि असल में उन्हें मारे जाने का बस यही एकमात्र तरीक़ा था।