JWT की गलतियाँ जो उल्लंघन बन जाती हैं
अंतिम अपडेट:
JSON Web Token एक हस्ताक्षरित कथन है, मुहरबंद लिफ़ाफ़ा नहीं। यह बिंदुओं से जुड़े तीन Base64url-एन्कोड किए गए खंड हैं — हस्ताक्षर एल्गोरिथ्म का नाम बताने वाला header, किसी उपयोगकर्ता या सत्र के बारे में claims का payload, और पहले दो पर हस्ताक्षर। हस्ताक्षर यह साबित करता है कि टोकन कुंजी धारक ने जारी किया था और तब से वह बदला नहीं गया। गोपनीयता के बारे में यह कुछ साबित नहीं करता: जिसके पास टोकन हो, वह payload को decode करके उसके हर claim को पढ़ सकता है, इसीलिए उसे decode करने के लिए password नहीं, paste चाहिए। जब JWT उत्पादन में विफल होते हैं, तो लगभग कभी cryptography नहीं टूटती। जो टूटता है वह कोई जाँच होती है जिसे किसी ने छोड़ दिया — टोकन से ही भरोसे पर लिया गया एल्गोरिथ्म, वह समाप्ति जिसे किसी ने सत्यापित नहीं किया, वह साझा secret जिसे कोई इंसान याद से टाइप कर सकता था। कोई भी टोकन JWT Decoder में paste कीजिए, और उसका header और claims सामने खुल जाते हैं। यह लेख उन इंजीनियरिंग गलतियों से गुज़रता है जो उन छोड़ी गई जाँचों को उल्लंघनों में बदल देती हैं, और उस रक्षात्मक नियम से भी जो हर एक को बंद करता है।
हस्ताक्षरित होना गुप्त होने जैसा नहीं है
Payload Base64url है — एक एन्कोडिंग, साधारण Base64 के ही परिवार की — एन्क्रिप्शन नहीं। उसमें मौजूद सब कुछ उन सभी के लिए पठनीय है जिनसे होकर टोकन गुज़रता है: ईमेल पते, भूमिकाएँ, tenant ID, आंतरिक खाता संख्याएँ, और कभी-कभी कहीं बदतर चीज़ें, जैसे password-reset flags या व्यक्तिगत डेटा के टुकड़े जो किसी developer ने debugging के लिए जोड़े और कभी हटाए नहीं। टोकन developers की कल्पना से भी आगे यात्रा करते हैं: वे browser storage में पड़े रहते हैं, proxy और application logs से गुज़रते हैं, crash reports में copy होते हैं और bug tickets व chat threads में paste किए जाते हैं। कार्य-नियम सीधे इसी से निकलता है: टोकन में कभी ऐसा claim न डालें जिसे log file में छपा देखने में आपको असहजता हो, क्योंकि देर-सबेर वह वहाँ दिखाई देगा।
ग़लतफ़हमी समझ में आने वाली है — एक compact टोकन ciphertext जैसा दिखता है, सारे अबोले अक्षर और बिंदु। लेकिन हस्ताक्षर टोकन को केवल छेड़छाड़-साक्ष्य बनाता है: कोई claim बदलिए और सत्यापन विफल हो जाता है। गोपनीयता उसी मानक परिवार की एक अलग रचना है (एन्क्रिप्ट किए गए टोकन, JWE), और एक साधारण access token शायद ही कभी वह होता है। इसलिए payload को हस्ताक्षर वाला सार्वजनिक पाठ मानकर design करें: केवल पहचानकर्ता और प्राधिकरण तथ्य, संवेदनशील डेटा सर्वर-पक्ष पर ID से लाया गया, और छोटी आयु, ताकि जो कुछ भी रिस जाए वह जल्दी समाप्त हो जाए।
टोकन को अपना एल्गोरिथ्म खुद चुनने देना
Header एक alg घोषित करता है, और JWT की किताब की सबसे पुरानी विफलता वह verifier है जो उस पर विश्वास कर लेता है। घोषित मानों में से एक है none — एक अहस्ताक्षरित टोकन, जिसमें कोई हस्ताक्षर ही नहीं। कोई library या हाथ से बनाई जाँच, जो टोकन द्वारा स्वयं घोषित एल्गोरिथ्म का पालन करती है, खाली हस्ताक्षर वाला टोकन पाकर उसे असली मान सकती है। सर्वोत्तम-अभ्यास RFC इस बारे में दोटूक है: अहस्ताक्षरित टोकन केवल वहाँ स्वीकार्य हैं जहाँ channel स्वयं पहले से अखंडता की गारंटी देता हो, और सार्वजनिक internet से आने वाला bearer token वह स्थिति नहीं है। Header पर भरोसा करने वाली वही खामी एल्गोरिथ्म भ्रम पैदा करती है। RS256 टोकन जारी करने वाला system निजी कुंजी रखता है और सत्यापन के लिए उससे मेल खाती सार्वजनिक कुंजी प्रकाशित करता है। यदि verifier को टोकन को उसके बजाय HS256 के रूप में संसाधित करने के लिए राज़ी किया जा सके, तो सत्यापन सामग्री सार्वजनिक कुंजी बन जाती है — design से ही सार्वजनिक, सबको ज्ञात — जो सममित secret के रूप में प्रयुक्त होती है, और फिर कोई भी जो हस्ताक्षर बना सकता है, वे सत्यापित होने लगते हैं। दोनों विफलताओं में कोई कुंजी चोरी नहीं होती; verifier ने बस भेजने वाले के लिखे निर्देशों का पालन किया।
Header एक दूसरा, शांत जोखिम भी लिए होता है: कुंजी सूचक। kid (कुंजी ID), jku और x5u (वे URL जहाँ कुंजी या प्रमाणपत्र रहता है) जैसे parameter verifier को बताते हैं कि सत्यापन कुंजी कहाँ मिलेगी — और वे हमलावर के प्रभाव वाले टोकन के भीतर आते हैं। यदि कोई kid मान बिना कड़ी जाँच के file path, database query या कुंजी लाने की क्रिया में बह जाए, तो वह खोज स्वयं आक्रमण सतह बन जाती है। रक्षात्मक नियमों का आकार सबका एक जैसा है: अपेक्षित एल्गोरिथ्म को server-पक्ष configuration में पिन करें और बाकी सब अस्वीकार करें; कुंजियाँ अपनी registry से हल करें, कुंजी ID की अनुमति-सूची से जाँच हो; टोकन द्वारा दिए गए URL से कुंजी सामग्री कभी न लाएँ; और हर header field को अविश्वसनीय input मानें, क्योंकि वह है ही।
हस्ताक्षर पास हो गया; claims कभी जाँचे ही नहीं गए
हस्ताक्षर की जाँच केवल एक प्रश्न का उत्तर देती है — क्या यह टोकन कुंजी धारक ने जारी किया था और अछूता छोड़ा था? यह इस बारे में कुछ नहीं कहती कि टोकन को यहाँ, अभी माना जाना चाहिए या नहीं। वह claims का काम है, और claim सत्यापन वह जगह है जहाँ उत्पादन प्रणालियाँ चुपचाप कदम छोड़ देती हैं। पहले exp: जो verifier समाप्ति कभी नहीं जाँचता, वह हर टोकन को स्थायी credential बना देता है, इसलिए तीन साल पहले log में रिसा टोकन आज भी प्रमाणित करता है। फिर iss: केवल उन्हीं जारीकर्ताओं के टोकन स्वीकारें जिन्हें आपने configure किया है, न कि हर उस का जो संयोग से वही एल्गोरिथ्म साझा करता हो। और aud, सबसे महँगी चूक: संगठन अक्सर कई सेवाएँ एक ही हस्ताक्षर कुंजी पर चलाते हैं, और तब कम-मूल्य वाली सेवा का टोकन — मुफ़्त-स्तरीय API, staging के पास का कोई tool — ठीक से हस्ताक्षरित टोकन बन जाता है जिसे उच्च-मूल्य सेवा स्वीकार कर लेती है, क्योंकि हस्ताक्षर असली है और किसी ने पूछा ही नहीं कि टोकन किस audience के लिए बनाया गया था।
इसमें से कुछ भी दुर्लभ नहीं है। यह एक checklist है, और विफलता का ढंग यह है कि libraries हस्ताक्षर तो उत्साह से सत्यापित करती हैं, जबकि claim जाँचों को वैकल्पिक, अक्षम या call करने वाले की समस्या छोड़ देती हैं। रक्षात्मक रुख यह है कि हर सत्यापन पर exp, iss और aud अनिवार्य हों, किसी भी कमी या बेमेल पर fail बंद हो, और घड़ी के अंतर की सहनशीलता घंटों में नहीं, मिनटों में हो — सहनशीलता अपूर्ण घड़ियों के लिए होती है, और काफी दूर तक खींची जाए तो वह समाप्त टोकनों को चुपके से फिर जीवित कर देती है। फिर नकारात्मक मामलों को वैसे ही परखें जैसे आप login form को परखते: समाप्त टोकन, ग़लत-audience वाला टोकन और अज्ञात जारीकर्ता का टोकन — सभी अस्वीकार होने चाहिए, और वे अस्वीकृतियाँ उस सुरक्षा संकेत के रूप में log करने योग्य हैं जो वे हैं।
जो secrets इंसान याद रख सके, वही secrets हमलावर आज़मा सकता है
HS256 सममित है: एक साझा secret हस्ताक्षर भी करता है और सत्यापन भी। असली codebases में वह secret बहुत बार कोई शब्द या छोटा वाक्यांश होता है — कंपनी का नाम, tutorial से copy किया गया framework default, कुछ ऐसा जो developer याद से टाइप कर सके। इसे घातक बनाने वाला सिद्धांत यह है, और यह किसी tool की नहीं, design की ही विशेषता है: पकड़ा गया टोकन अनुमानित secret को offline आज़माने के लिए ज़रूरी सब कुछ लिए होता है। संदेश ज्ञात है, हस्ताक्षर ज्ञात है, इसलिए अनुमानों को स्थानीय रूप से, पूरी गति से जाँचा जा सकता है, बिना server को कोई प्रयास दिखे और बिना कोई rate limit लागू हुए। इसलिए पूरी योजना की सुरक्षा उस एक string की entropy पर सिमट जाती है — और इंसानों द्वारा चुनी गई strings की entropy बहुत कम होती है।
सुधार बिना चमक-दमक के हैं। हस्ताक्षर secrets मशीन से बनवाएँ, उचित लंबाई पर — HMAC के लिए कम से कम 256 bits entropy — और उन्हें उन्हीं उत्पादन credentials की तरह रखें जो वे हैं। उन्हें हर environment के लिए अलग रखें, ताकि रिसी हुई staging configuration का secret उत्पादन में कुछ न खरीद सके, और development default को असली deployment में कभी जीवित न रहने दें। जहाँ कई सेवाएँ टोकन सत्यापित करती हैं, वहाँ RS256 या ES256 जैसे असममित एल्गोरिथ्म पसंद करें: तब verifiers के पास केवल सार्वजनिक कुंजियाँ रहती हैं, इसलिए समझौता किया गया verifier कुछ ऐसा नहीं रिसाता जिससे टोकन गढ़े जा सकें, और हस्ताक्षर कुंजी एक ही जगह रहती है। और ज़रूरत पड़ने से पहले rotation की कहानी तैयार रखें, क्योंकि सममित secret के रिसने पर कोई अलार्म नहीं बजता — पहला संकेत आमतौर पर वह टोकन होता है जो आपने जारी नहीं किया, पर आपका मानकर स्वीकार किया जा रहा है।
Browser टोकन कहाँ रखता है, यही तय करता है कि उसे कौन पढ़ सकता है
Client पक्ष पर, storage का चुनाव एक सुरक्षा निर्णय है, चाहे किसी ने उसे सोचकर लिया हो या नहीं। localStorage में रखा टोकन पृष्ठ पर चलने वाली हर script द्वारा पठनीय है: आपका अपना code, हर third-party tag और analytics snippet जिसे आपने प्रवेश दिया है — और कोई भी cross-site scripting payload जो कभी execute हो जाए, application के किसी भी कोने के किसी भी unescaped comment field से आया हो। Bearer token एक copy की जा सकने वाली credential है: जो उसे पढ़ ले, वह उसे कहीं से भी पेश कर सकता है और उसकी समाप्ति तक वह उपयोगकर्ता बन सकता है। इसलिए script द्वारा पढ़े जा सकने वाले storage में रखे लंबे-आयु वाले टोकन ठीक उतने ही मूल्यवान हैं जितना आपका सबसे बुरा XSS bug, और अधिकांश applications में कभी न कभी एक हो ही जाता है।
सामान्य विकल्प, एक HttpOnly cookie, script की पहुँच पूरी तरह हटा देता है — XSS payload अब टोकन को पढ़कर बाहर नहीं भेज सकता — लेकिन browser cookie को स्वचालित रूप से जोड़ देता है, जिससे cross-site request forgery वह ख़तरा बन जाती है जिसके विरुद्ध SameSite attributes और CSRF tokens से इंजीनियरिंग करनी होती है। टोकन केवल memory में रखना दोनों समस्याओं से बचता है और हर refresh पर session खो देता है। कोई सार्वभौमिक रूप से सही उत्तर नहीं है, और ऐसा होने का दिखावा करना वह तरीका है जिससे टीमें हर विकल्प का सबसे बुरा हिस्सा पा लेती हैं। Storage को उल्लंघन में बदलने वाली चीज़ वह संयोजन है जिसे किसी ने जानबूझकर नहीं चुना: script-पठनीय storage, लंबी आयु, विस्तृत scopes और copy का मूल्य घटाने के लिए refresh-token rotation का अभाव। समझबूझकर समझौता चुनें, किसी भी स्थिति में access tokens की आयु छोटी रखें, और तर्क को वहाँ लिख दें जहाँ अगला developer उसे पाएगा।