SAML signature valid थी। असल समस्या वो कभी थी ही नहीं।
SAML single sign-on में replay की समस्या इसके ढांचे में ही बनी हुई है, और ज़्यादातर service provider इसे ठीक उसी एक जगह पर "solve" करते हैं जहाँ एक attacker पहुँच सकता है।
सेटअप ऐसा है। आप service provider हैं। एक user आपके login पर आता है, आप उसके identity provider को एक AuthnRequest भेजते हैं, और उस request में एक random ID होता है। IdP user को authenticate करता है और InResponseTo="<that ID>" के साथ एक SAML Response वापस post करता है। आप वो ID lookup करते हैं, confirm करते हैं कि आपने ही उसे issue किया था, और उसे consume कर देते हैं ताकि वो कभी दो बार इस्तेमाल न हो सके। Request response से bound, response एक बार इस्तेमाल हुई, replay हरा दिया गया। बिल्कुल textbook।
अब वो सवाल जो इस पूरी चीज़ को तोड़ देता है: IdP ने असल में sign किया क्या था?
Response नहीं। Assertion। Entra, Okta, और बाकी ज़्यादातर vendor <Assertion> element को sign करते हैं और उसके चारों तरफ़ के <Response> envelope को unsigned छोड़ देते हैं। ये सामान्य है, और spec इसकी इजाज़त देता है। लेकिन InResponseTo तो <Response> पर रहता है। यानी जिस field पर आपकी पूरी replay defense टिकी है, वही एक field है जिसे signature cover नहीं करता। आप उसे edit करें, delete करें, जो चाहें कर लें, assertion की signature फिर भी perfectly validate होगी, क्योंकि आपने assertion को छुआ ही नहीं।
तो देखिए क्या होता है जब एक attacker वो obvious काम कर देता है।
SAML के दो flavor होते हैं। SP-initiated response उस request का जवाब होती है जो आपने भेजी थी, इसलिए उनमें InResponseTo होता है। IdP-initiated (unsolicited) response कभी माँगी ही नहीं गई थी, इसलिए उनमें InResponseTo होता ही नहीं। आपका code, वाजिब तौर पर, इसी पर branch करता है: अगर InResponseTo है, तो उसे एक stored request से match करो और consume कर दो; अगर नहीं है, तो match करने के लिए कोई request ही नहीं है, इसलिए वो check skip हो जाता है। वही skip पूरी vulnerability है।
अटैक तीन steps में है और cryptography ज़ीरो:
- एक असली, successful SAML Response capture करो। एक genuine login, एक genuine signature, एक genuine assertion।
InResponseToattribute delete कर दो। signature assertion को cover करती है, envelope को नहीं, इसलिए वो फिर भी verify होगी।- उसे वापस post कर दो।
InResponseToन होने पर आपका SP इसे IdP-initiated मान लेता है, वो branch लेता है जिसमें कोई replay check नहीं है, signature validate करता है (valid), और user को login करा देता है।
फिर उसे दोबारा post करो। और दोबारा। अब वो assertion एक skeleton key बन चुकी है। हर वो check जिस पर आपको गर्व था अब भी pass होता है: signature valid, certificate pinned, issuer correct, conditions date के अंदर। वो एक control जो replay को रोक सकता था, उस field से जुड़ा था जिसे attacker मुफ़्त में delete कर देता है।
यहाँ पूरा जाल एक लाइन में: आपने assertion को sign किया, लेकिन gate envelope पर लगाया।
इसका fix और ज़्यादा validation नहीं है। इसका fix सही चीज़ को validate करना है। replay protection को InResponseTo पर टिकाना बंद करो, जो unsigned और optional है, और उसे assertion के अपने ID पर टिकाओ, जो signed है और हमेशा मौजूद रहता है। हर assertion में एक ID होता है, वो signature के अंदर बैठा होता है, और उससे छेड़छाड़ करते ही verification टूट जाता है। assertion IDs का एक one-time-use cache रखो, हर assertion के NotOnOrAfter से bounded ताकि cache कभी बेतहाशा न बढ़े, और जो भी ID आप पहले देख चुके हैं उसे reject कर दो। अब InResponseTo delete करने से attacker को कुछ नहीं मिलता, क्योंकि जिस value को आप असल में check करते हैं उसे न तो forge किया जा सकता है और न replay।
जब वहाँ काम कर ही रहे हैं, तो request binding को primary control नहीं, बल्कि defense in depth की तरह देखो। InResponseTo जब मौजूद हो तब भी उसे match करो। अगर आप IdP-initiated login कभी offer ही नहीं करते, तो unsolicited response को सीधे reject करो। Audience और Conditions window को अब भी enforce करो। लेकिन जो load-bearing anti-replay check है, वो signed bytes पर ही टिकना चाहिए, बस।
हमें ये इसलिए पता है क्योंकि हमने इसे launch से पहले अपनी ही SAML को audit करते हुए पकड़ा, न कि इसलिए कि बाद में किसी customer ने पकड़ा। और ये सबक SAML से कहीं आगे तक लागू होता है। एक signature ठीक एक ही सवाल का जवाब देती है: क्या इस key के धारक ने ही ये bytes बनाए। वो ये नहीं बताती कि bytes fresh हैं, कि वो आपके लिए ही थे, या कि आपने उन्हें पहले एक बार accept नहीं कर लिया है। ये तीन अलग-अलग checks हैं, और इनमें से हर एक को ऐसी field पढ़नी होगी जिसे signature सचमुच cover करता है। जिस पल कोई security decision signature के बाहर पड़े हुए data पर निर्भर हो जाता है, वो security decision नहीं रह जाता और एक विनम्र request बन जाता है जिसे ठुकराने के लिए attacker आज़ाद है।
जिस field पर gate लगाते हो उसे sign करो, या जो field signed है उसी पर gate लगाओ। कोई तीसरा विकल्प नहीं है जो एक replay से बच सके।
अगर आप SAML validation खुद hand-roll करके replay case गलत नहीं करना चाहते, तो ये किसी और को इसे चलाने देने की अच्छी वजह है। Authagonal replay को signed assertion ID पर टिकाता है, इसलिए InResponseTo को strip करना कहीं नहीं पहुँचाता।