कोड की एक लाइन आपके पूरे एक्सेस कंट्रोल मॉडल को खत्म कर सकती है। यदि आप रिक्वेस्ट बॉडी (request body) को सीधे डेटाबेस अपडेट में डाल देते हैं, तो आप क्लाइंट को आपके स्कीमा (schema) को फिर से लिखने के लिए कलम थमा देते हैं। इसे ही mass assignment कहते हैं। यह कोई अजीब बग या दुर्लभ मामला नहीं है। यह एक डिज़ाइन की विफलता है जो तब सामने आती है जब कोई API पेलोड (payload) को अपनी स्वयं की अपडेट पॉलिसी मान लेती है।

await db.users.update(req.params.id, { ...req.body });

यह दिखने में साफ-सुथरा लगता है। इससे टाइपिंग बचती है। लेकिन कुंजियों (keys) पर क्लाइंट का नियंत्रण होता है। एक हमलावर एक सामान्य प्रोफाइल अपडेट में "role": "admin", "accountId": "someone_else", या "credit": 99999" जोड़ सकता है। आपका वैलिडेशन लेयर (validation layer) यह जांच सकता है कि वे मान स्ट्रिंग हैं या नंबर, और कह सकता है कि वे ठीक दिख रहे हैं। हालाँकि, वैधता (validity) का अर्थ अधिकृत होना (authorization) नहीं है। एक उपयोगकर्ता के पास लक्षित रिकॉर्ड का वैध स्वामित्व हो सकता है। इसका मतलब यह नहीं है कि उसे उसके अंदर के हर फ़ील्ड को संपादित करने का अधिकार होना चाहिए।

Mass Assignment वास्तव में कैसा दिखता है

खतरा सुविधा में छिपा होता है। फ्रेमवर्क और ORMs, JSON कुंजियों को सीधे डेटाबेस कॉलम पर मैप करना बहुत आसान बना देते हैं। जब आप ऐसा करते हैं, तो आप डेटाबेस को यह भरोसा दिला रहे होते हैं कि क्या बदलना चाहिए, न कि केवल यह कि उसे कैसे बदलना चाहिए।

अपनी प्रोफाइल अपडेट करने वाला उपयोगकर्ता displayName और bio के लिए वैध डेटा भेज सकता है, लेकिन उनके साथ role या balance भी चुपके से डाल सकता है। यदि आपका कंट्रोलर (controller) बस उस ऑब्जेक्ट को आगे भेज देता है, तो डेटाबेस सब कुछ लिख देता है। वैलिडेशन गलत तरीके से बनाए गए मानों (malformed values) को पकड़ लेता है। यह शायद ही कभी दुर्भावनापूर्ण कुंजियों (malicious keys) को पकड़ पाता है। वह बिजनेस रूल जो कहता है कि "इस उपयोगकर्ता को अपनी प्रोफाइल अपडेट करने की अनुमति है", पंक्ति (row) के हर कॉलम पर एक व्यापक अनुमति बन जाता है।

इसका समाधान अधिक वैलिडेशन नहीं है। इसके लिए सख्त आर्किटेक्चर की आवश्यकता है।

तीन द्वार (The Three Gates)

एक सुरक्षित म्यूटेशन (mutation) स्टोरेज को छूने से पहले तीन अलग-अलग जांचों से गुजरता है।

स्वीकृत फ़ील्ड्स (Allowlist)

सबसे पहले यह तय करें कि आप किन कुंजियों (keys) को देखेंगे। यदि कोई फ़ील्ड allowlist में नहीं है, तो अनुरोध को अस्वीकार कर दें या उस कुंजी को हटा दें। यह डिफ़ॉल्ट स्थिति को बदल देता है: नए डेटाबेस कॉलम तब तक लिखने योग्य (writable) नहीं होते जब तक कि कोई डेवलपर उन्हें स्पष्ट रूप से एक्सपोज़ न कर दे। समय के साथ स्कीमा बढ़ते हैं। एक टीम का साथी stripeCustomerId, departmentBudget, या isVerified फ्लैग जोड़ सकता है। Allowlist के साथ, वे नए कॉलम क्लाइंट राइट्स (client writes) से स्वचालित रूप से सुरक्षित रहते हैं। इसके बिना, प्रत्येक नया कॉलम अनजाने में एक API सतह (surface) बन जाता है।

मान्य मान (Valid Values)

एक बार जब आप जान लेते हैं कि कौन से फ़ील्ड अनुमत हैं, तो जांचें कि क्या वे मान तर्कसंगत हैं। क्या टाइमज़ोन स्ट्रिंग वास्तव में एक मान्यता प्राप्त टाइमज़ोन है? क्या ईमेल का फॉर्मेट ईमेल जैसा है? क्या संख्या एक उचित सीमा के भीतर है? यह स्वच्छता (hygiene) है। यह कचरे को आपके सिस्टम में प्रवेश करने से रोकता है, लेकिन यह दुरुपयोग को नहीं रोकता। role फ़ील्ड में एक पूरी तरह से वैध "admin" स्ट्रिंग भी खतरनाक है यदि इसे गलत व्यक्ति भेजता है।

अधिकृत ट्रांज़िशन (Authorized Transitions)

यह वह द्वार है जिसे अधिकांश टीमें छोड़ देती हैं, और यहीं असली सुरक्षा निहित है। एक सूक्ष्म (granular) प्रश्न पूछें: क्या इस विशिष्ट कर्ता (actor) के पास इस विशिष्ट रिकॉर्ड पर इस विशिष्ट फ़ील्ड को बदलने की अनुमति है? न कि "क्या उपयोगकर्ता एडमिन है?" न कि "क्या उपयोगकर्ता के पास write:users स्कोप है?" बल्कि, "क्या इस उपयोगकर्ता को अपना displayName बदलने की अनुमति है, लेकिन कभी भी अपना accountId नहीं?" प्रति-फ़ील्ड अधिकृतिकरण (Per-field authorization) "Editor" या "User" जैसी व्यापक अनुमति को पंक्ति के हर गुण (property) की मास्टर कुंजी बनने से रोकता है।

पैच फ़ंक्शन (Patch Function) बनाना

तीनों द्वारों को एक एकल पाइपलाइन में बांधें। जब एक पैच अनुरोध आता है, तो इसे क्रमवार चरणों से गुज़ारें।

सबसे पहले, अपने allowlist के विरुद्ध इनपुट को फ़िल्टर करें। यदि इस एंडपॉइंट के लिए role एक अनुमत फ़ील्ड नहीं है, तो वहीं रुक जाएं। उस मान को मान्य या अधिकृत करने का कोई कारण नहीं है जिसे आपको कभी प्राप्त ही नहीं करना चाहिए था।

दूसरा, अनुमत मानों को मान्य करें। प्रकार (types), फॉर्मेट और बिजनेस नियमों की जांच करें। एक लोकेशन फ़ील्ड को एक स्ट्रिंग होना चाहिए जो एक वास्तविक टाइमज़ोन को दर्शाता हो। एक अवतार URL एक निश्चित लंबाई के भीतर एक वैध URI होना चाहिए।

तीसरा, क्रिया को अधिकृत करें। सत्यापित करें कि कर्ता (actor) लक्षित रिकॉर्ड का स्वामी है, या उसके पास इस फ़ील्ड के लिए आवश्यक सटीक अनुमति है। व्यक्तिगत डेटा के लिए स्वामित्व एक अच्छा डिफ़ॉल्ट है, लेकिन कुछ फ़ील्ड्स को अभी भी अतिरिक्त द्वारों की आवश्यकता होती है। एक उपयोगकर्ता अपनी प्रोफाइल का स्वामी हो सकता है, फिर भी केवल एक बिलिंग एडमिन को ही taxRegion को छूना चाहिए।

चौथा, डेटा को सामान्य (normalize) करें। व्हाइटस्पेस को ट्रिम करें, बार-बार आने वाले स्पेस को कम करें, ईमेल को लोअरकेस करें, या कंट्रोल कैरेक्टर्स को हटा दें। ऐसा वैलिडेशन के बाद लेकिन स्टोरेज से पहले करें ताकि आप अधिकृतिकरण जांच के दौरान गंदे स्ट्रिंग्स (dirty strings) की तुलना न कर रहे हों।

यदि इनपुट किसी भी गेट (gate) में विफल रहता है, तो पूरे म्यूटेशन (mutation) को अस्वीकार कर दें। सुरक्षित फ़ील्ड्स को आंशिक रूप से लागू न करें और खराब फ़ील्ड्स को चुपचाप छोड़ न दें। एक मिश्रित प्रतिक्रिया (mixed response) क्लाइंट्स को हर उस की (key) को आज़माने के लिए प्रशिक्षित करती है जो वे सोच सकते हैं और यह देखने के लिए कि क्या काम करता है। स्पष्ट रूप से विफल (Fail explicitly) हों।

वे एज केस (Edge Cases) जो वास्तव में मायने रखते हैं

मास असाइनमेंट (Mass assignment) सुरक्षा उन विवरणों पर टिकी होती है जिन्हें यूनिट टेस्ट अक्सर छोड़ देते हैं।

डुप्लिकेट JSON कीज़ (Duplicate JSON keys)। हमलावर {"role": "user", "role": "admin"} जैसे पेलोड भेज सकते हैं। आपके HTTP पार्सर और फ्रेमवर्क के आधार पर, आपके एप्लिकेशन कोड द्वारा ऑब्जेक्ट देखे जाने से पहले ही दूसरा मान पहले वाले को ओवरराइट कर सकता है। इस व्यवहार का पार्सर स्तर पर परीक्षण करें। यदि आपका फ्रेमवर्क चुपचाप अंतिम की (key) को स्वीकार कर लेता है, तो आपकी अलालिस्ट (allowlist) "user" को देख रही हो सकती है जबकि डेटाबेस को "admin" प्राप्त होता है।

नेस्टेड ऑब्जेक्ट्स, नल (nulls), और एरेज़ (arrays)। यह न मानें कि पेलोड फ्लैट (flat) है। एक क्लाइंट प्रतिबंधित फ़ील्ड को { "profile": { "role": "admin" } } जैसे नेस्टेड ऑब्जेक्ट के अंदर रख सकता है। यदि आपका स्कीमा रिकर्सिव (recurse) है, तो आपकी अलालिस्ट को भी होना चाहिए। इसी तरह, तय करें कि आप null को कैसे संभालते हैं। क्या इसका मतलब "इस फ़ील्ड को अनदेखा करें" है या "इस फ़ील्ड को हटा दें"? और यदि एक एरे (array) की अपेक्षा की जाती है, तो क्या आपका वैलिडेटर अप्रत्याशित संरचनाओं को अस्वीकार कर देता है, या यह एक सिंगल ऑब्जेक्ट को एरे में बदल देता है और उसे जाने देता है?

यूनिकोड नॉर्मलाइजेशन (Unicode normalization)। दो स्ट्रिंग्स एक इंसान को समान दिख सकती हैं, जबकि वे बाइट्स के अलग-अलग अनुक्रम (sequences) हो सकती हैं। एक उपयोगकर्ता प्रीकंपोज़्ड é या डीकंपोज़्ड e प्लस कॉम्बाइनिंग एक्सेंट भेज सकता है। यदि आपका ऑथोराइजेशन चेक एक बार नॉर्मलाइज़ करता है लेकिन आपका स्टोरेज लेयर अलग तरह से नॉर्मलाइज़ करता है, तो आपके पास असंगत डेटा हो सकता है, या इससे भी बुरा, एक बाईपास हो सकता है जहाँ यूजरनेम कोलिजन (username collision) आपके लॉजिक से बच निकलता है। शुरुआत में ही नॉर्मलाइज़ करें और लगातार नॉर्मलाइज़ करें।

रेस कंडीशंस (Race conditions)। ऑथोराइजेशन के निर्णय स्थिर (freeze frames) नहीं होते हैं। वे एक समय बिंदु पर होते हैं। दो अनुरोध एक ही रिकॉर्ड पढ़ सकते हैं, दोनों देख सकते हैं कि एक्टर को लिखने की अनुमति है, और दोनों अपडेट जारी कर सकते हैं। इस बीच, स्टेट या एक्टर की अनुमतियाँ बदल सकती हैं। हमेशा वर्जन नंबर या स्टेट मशीन वैल्यू पर कंडीशन के साथ डेटाबेस अपडेट लागू करें। UPDATE users SET ... WHERE id = ? AND version = 5 जैसा कुछ उपयोग करें। यदि आपके पढ़ने के बाद से रो (row) बदल गई है, तो राइट (write) विफल हो जाएगा। विफल होने पर उसे फिर से प्रयास करके या अस्वीकार करके संभालें। यह पुराने (stale) ऑथोराइजेशन चेक को आपके डेटा को दूषित करने से रोकता है।

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

आप उसे सुरक्षित नहीं कर सकते जिसे आप देख नहीं सकते। अपने ऑडिट लॉगिंग (audit logging) को केवल एक्शन के बजाय निर्णय के इर्द-गिर्द बनाएं।

एक्टर आईडी (actor ID) और टारगेट आईडी (target ID) को लॉग करें। उन सटीक फ़ील्ड नामों को लॉग करें जिन्हें स्वीकार किया गया था और जिन्हें अस्वीकार कर दिया गया था। उस पॉलिसी वर्जन को लॉग करें जिसने निर्णय लिया और अंतिम परिणाम को भी। यदि किसी उपयोगकर्ता के प्रोफाइल अपडेट में अचानक role को अस्वीकार किया जाने लगता है, तो आप तुरंत जानना चाहेंगे।

बेयरर टोकन (bearer tokens) को कभी भी लॉग न करें। पूरे रिक्वेस्ट बॉडी (request bodies) को अपने लॉग में कभी न डालें। एक ऑडिट ट्रेल को दुरुपयोग की जांच करने में आपकी मदद करनी चाहिए, न कि क्रेडेंशियल्स और व्यक्तिगत डेटा का भंडार बनना चाहिए।

एकमात्र नियम जिसकी आपको आवश्यकता है

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