মাত্র এক লাইন কোড আপনার পুরো অ্যাক্সেস কন্ট্রোল মডেলকে ধ্বংস করে দিতে পারে। রিকোয়েস্ট বডি সরাসরি একটি ডাটাবেস আপডেটে পাঠিয়ে দিলে আপনি ক্লায়েন্টকে আপনার স্কিমা পুনরায় লেখার জন্য একটি কলম হাতে তুলে দিচ্ছেন। এটিই হলো Mass Assignment। এটি কোনো অদ্ভুত বাগ বা প্রান্তিক ঘটনা নয়। এটি একটি ডিজাইনের ত্রুটি যা তখনই ঘটে যখন একটি API তার পেলোডকে (payload) নিজস্ব আপডেট পলিসি হিসেবে বিবেচনা করে।
await db.users.update(req.params.id, { ...req.body });
এটি দেখতে পরিচ্ছন্ন মনে হয়। এতে টাইপিংয়ের সময় বাঁচে। কিন্তু কী (keys) গুলো ক্লায়েন্টের নিয়ন্ত্রণে থাকে। একজন আক্রমণকারী একটি সাধারণ প্রোফাইল আপডেটের সাথে "role": "admin", "accountId": "someone_else", বা "credit": 99999" যোগ করে দিতে পারে। আপনার ভ্যালিডেশন লেয়ার হয়তো পরীক্ষা করে দেখবে যে সেই মানগুলো স্ট্রিং নাকি নম্বর এবং বলবে যে সেগুলো ঠিক আছে। তবে, ভ্যালিডিটি (Validity) মানেই অথরাইজেশন (Authorization) নয়। একজন ব্যবহারকারী হয়তো বৈধভাবে টার্গেট রেকর্ডের মালিক হতে পারেন। কিন্তু তার মানে এই নয় যে তার ভেতরের প্রতিটি ফিল্ড এডিট করার অধিকার তার আছে।
Mass Assignment আসলে দেখতে কেমন
বিপদ লুকিয়ে থাকে সুবিধার মাঝে। ফ্রেমওয়ার্ক এবং ORM গুলো JSON কী-গুলোকে সরাসরি ডাটাবেস কলামের সাথে ম্যাপ করা অত্যন্ত সহজ করে দেয়। যখন আপনি এটি করেন, তখন আপনি ডাটাবেসকে শুধু কীভাবে পরিবর্তন করতে হবে তা নয়, বরং কী পরিবর্তন করা উচিত সে বিষয়েও ক্লায়েন্টকে বিশ্বাস করতে বলছেন।
একজন ব্যবহারকারী যখন তার প্রোফাইল আপডেট করেন, তখন তিনি displayName এবং bio-এর জন্য সঠিক ডাটা পাঠাতে পারেন, কিন্তু তার সাথে role বা balance-ও লুকিয়ে পাঠাতে পারেন। যদি আপনার কন্ট্রোলার কেবল অবজেক্টটি ফরওয়ার্ড করে দেয়, তবে ডাটাবেস সবকিছু লিখে ফেলবে। ভ্যালিডেশন ভুল ফরম্যাটের মানগুলো ধরতে পারে, কিন্তু এটি খুব কমই ক্ষতিকারক কী (malicious keys) ধরতে পারে। যে বিজনেস রুলটি বলে "এই ব্যবহারকারী তার প্রোফাইল আপডেট করতে পারবেন", সেটি রো-এর প্রতিটি কলামের জন্য একটি ঢালাও অনুমতিতে পরিণত হয়।
এর সমাধান আরও বেশি ভ্যালিডেশন নয়। এর সমাধান হলো আরও কঠোর আর্কিটেকচার।
তিনটি গেট (The Three Gates)
একটি নিরাপদ মিউটেশন স্টোরেজে পৌঁছানোর আগে তিনটি আলাদা ধাপ বা গেট পার করে।
গৃহীত ফিল্ডসমূহ (Allowlist)
প্রথমে ঠিক করে নিন আপনি ঠিক কোন কী (keys) গুলো পরীক্ষা করবেন। যদি কোনো ফিল্ড অ্যালাউলিস্টে না থাকে, তবে রিকোয়েস্টটি প্রত্যাখ্যান করুন বা সেই কী-টি বাদ দিন। এটি ডিফল্ট অবস্থানকে উল্টে দেয়: নতুন ডাটাবেস কলামগুলো ততক্ষণ পর্যন্ত রাইটেবল (writable) হবে না যতক্ষণ না একজন ডেভেলপার স্পষ্টভাবে সেগুলোকে উন্মুক্ত করছেন। স্কিমা সময়ের সাথে সাথে বৃদ্ধি পায়। একজন সহকর্মী একটি stripeCustomerId, একটি departmentBudget, বা একটি isVerified ফ্ল্যাগ যোগ করতে পারেন। একটি অ্যালাউলিস্ট থাকলে, সেই নতুন কলামগুলো ক্লায়েন্টের রাইট থেকে স্বয়ংক্রিয়ভাবে সুরক্ষিত থাকে। অ্যালাউলিস্ট না থাকলে, প্রতিটি নতুন কলাম একটি আকস্মিক API সারফেস হয়ে দাঁড়ায়।
সঠিক মান (Valid Values)
একবার আপনি জেনে গেলে কোন ফিল্ডগুলো অনুমোদিত, তখন সেই মানগুলো যুক্তিযুক্ত কি না তা পরীক্ষা করুন। টাইমজোন স্ট্রিংটি কি আসলেই একটি স্বীকৃত টাইমজোন? ইমেলটি কি ইমেলের ফরম্যাটে আছে? সংখ্যাটি কি একটি যুক্তিসঙ্গত সীমার মধ্যে আছে? এটি হলো সিস্টেমের পরিচ্ছন্নতা বা হাইজিন (hygiene)। এটি সিস্টেমে আবর্জনা প্রবেশ করতে বাধা দেয়, কিন্তু এটি অপব্যবহার রোধ করতে পারে না। একটি নিখুঁতভাবে সঠিক "admin" স্ট্রিংও role ফিল্ডে অত্যন্ত বিপজ্জনক হতে পারে যদি ভুল ব্যক্তি এটি পাঠায়।
অনুমোদিত পরিবর্তন (Authorized Transitions)
এটি এমন একটি গেট যা বেশিরভাগ টিম এড়িয়ে যায়, অথচ আসল সুরক্ষা এখানেই থাকে। একটি সূক্ষ্ম প্রশ্ন করুন: এই নির্দিষ্ট অ্যাক্টর কি এই নির্দিষ্ট রেকর্ডের এই নির্দিষ্ট ফিল্ডটি পরিবর্তন করার অনুমতি রাখে? "ব্যবহারকারী কি একজন অ্যাডমিন?"—এমনটি নয়। "ব্যবহারকারীর কি write:users স্কোপ আছে?"—এমনটিও নয়। বরং প্রশ্নটি হবে, "এই ব্যবহারকারী কি তার নিজের displayName পরিবর্তন করার অনুমতি রাখেন, কিন্তু কখনোই তার accountId পরিবর্তন করতে পারবেন না?" প্রতিটি ফিল্ডের জন্য আলাদা অথরাইজেশন থাকলে "Editor" বা "User"-এর মতো ব্যাপক অনুমতিগুলো রো-এর প্রতিটি প্রপার্টির মাস্টার কি (master key) হয়ে উঠতে পারে না।
প্যাচ ফাংশন তৈরি করা (Building the Patch Function)
তিনটি গেটকে একটি একক পাইপলাইনে যুক্ত করুন। যখন একটি প্যাচ রিকোয়েস্ট আসে, তখন এটি ক্রমানুসারে ধাপগুলোর মধ্য দিয়ে চালান।
প্রথমত, আপনার অ্যালাউলিস্টের বিপরীতে ইনপুটটি ফিল্টার করুন। যদি এই এন্ডপয়েন্টের জন্য role একটি অনুমোদিত ফিল্ড না হয়, তবে সেখানেই থেমে যান। আপনি যে মানটি কখনোই গ্রহণ করার কথা ছিল না, সেটি ভ্যালিডেট বা অথরাইজ করার কোনো প্রয়োজন নেই।
দ্বিতীয়ত, অনুমোদিত মানগুলো ভ্যালিডেট করুন। টাইপ, ফরম্যাট এবং বিজনেস রুলগুলো পরীক্ষা করুন। একটি লোকেশন ফিল্ড অবশ্যই এমন একটি স্ট্রিং হতে হবে যা একটি বাস্তব টাইমজোনে রূপান্তরিত হয়। একটি অবতার URL অবশ্যই একটি নির্দিষ্ট দৈর্ঘ্যের অধীনে একটি বৈধ URI হতে হবে।
তৃতীয়ত, অ্যাকশনটি অথরাইজ করুন। যাচাই করুন যে অ্যাক্টর টার্গেট রেকর্ডের মালিক কি না, অথবা এই ফিল্ডের জন্য প্রয়োজনীয় সঠিক অনুমতি তার আছে কি না। ব্যক্তিগত তথ্যের জন্য মালিকানা (ownership) একটি ভালো ডিফল্ট, কিন্তু কিছু ফিল্ডের জন্য অতিরিক্ত গেটের প্রয়োজন হতে পারে। একজন ব্যবহারকারী তার প্রোফাইলের মালিক হতে পারেন, তবুও শুধুমাত্র একজন বিলিং অ্যাডমিন taxRegion পরিবর্তন করতে পারবেন।
চতুর্থত, ডাটা নরমালাইজ করুন। হোয়াইটস্পেস ট্রিম করুন, অতিরিক্ত স্পেস কমিয়ে ফেলুন, ইমেল লোয়ারকেস করুন, অথবা কন্ট্রোল ক্যারেক্টারগুলো সরিয়ে ফেলুন। এটি ভ্যালিডেশনের পরে কিন্তু স্টোরেজের আগে করুন, যাতে অথরাইজেশন চেকের সময় আপনাকে নোংরা বা অগোছালো স্ট্রিং নিয়ে তুলনা করতে না হয়।
যদি ইনপুট কোনো গেট (gate) বা শর্তে ব্যর্থ হয়, তবে পুরো মিউটেশনটি (mutation) প্রত্যাখ্যান করুন। নিরাপদ ফিল্ডগুলো আংশিকভাবে প্রয়োগ করবেন না এবং অবৈধ ফিল্ডগুলো নীরবে বাদ দেবেন না। একটি মিশ্র রেসপন্স (mixed response) ক্লায়েন্টদের এমনভাবে প্রশিক্ষণ দেয় যাতে তারা সম্ভাব্য প্রতিটি কী (key) ব্যবহার করে দেখে কোনটি কাজ করে। স্পষ্টভাবে ব্যর্থতা প্রদর্শন করুন।
সেই এজ কেসগুলো (Edge Cases) যা আসলে গুরুত্বপূর্ণ
মাস অ্যাসাইনমেন্ট (Mass assignment) প্রতিরক্ষা সেই সব খুঁটিনাটির ওপর নির্ভর করে যা ইউনিট টেস্টে (unit tests) প্রায়ই বাদ পড়ে যায়।
ডুপ্লিকেট JSON কী (Duplicate JSON keys)। আক্রমণকারীরা {"role": "user", "role": "admin"} এর মতো পেলোড (payload) পাঠাতে পারে। আপনার HTTP পার্সার (parser) এবং ফ্রেমওয়ার্কের ওপর ভিত্তি করে, আপনার অ্যাপ্লিকেশন কোড অবজেক্টটি দেখার আগেই দ্বিতীয় মানটি প্রথমটিকে ওভাররাইট (overwrite) করে দিতে পারে। পার্সার লেভেলে এই আচরণটি পরীক্ষা করুন। যদি আপনার ফ্রেমওয়ার্ক নীরবে শেষ কী-টি গ্রহণ করে, তবে আপনার অ্যালাউলিস্ট (allowlist) "user" দেখলেও ডাটাবেস "admin" গ্রহণ করতে পারে।
নেস্টেড অবজেক্ট, নাল (null), এবং অ্যারে (arrays)। পেলোডটি ফ্ল্যাট (flat) হবে বলে ধরে নেবেন না। একজন ক্লায়েন্ট একটি সীমাবদ্ধ ফিল্ডকে { "profile": { "role": "admin" } } এর মতো একটি নেস্টেড অবজেক্টের ভেতরে রাখতে পারে। আপনার স্কিমা (schema) যদি রিকার্সিভ (recursive) হয়, তবে আপনার অ্যালাউলিস্টকেও অবশ্যই রিকার্সিভ হতে হবে। একইভাবে, আপনি কীভাবে null হ্যান্ডেল করবেন তা ঠিক করে নিন। এর মানে কি "এই ফিল্ডটি উপেক্ষা করুন" নাকি "এই ফিল্ডটি মুছে ফেলুন"? আর যদি একটি অ্যারে প্রত্যাশিত হয়, তবে আপনার ভ্যালিডেটর কি অপ্রত্যাশিত স্ট্রাকচার প্রত্যাখ্যান করে, নাকি একটি সিঙ্গেল অবজেক্টকে অ্যারেতে রূপান্তর করে তা গ্রহণ করে নেয়?
ইউনিকোড নরমালাইজেশন (Unicode normalization)। দুটি স্ট্রিং মানুষের কাছে দেখতে একই রকম মনে হতে পারে, কিন্তু তাদের বাইট সিকোয়েন্স আলাদা হতে পারে। একজন ব্যবহারকারী একটি প্রি-কম্পোজড (precomposed) é অথবা একটি ডিকম্পোজড (decomposed) e এবং কম্বাইনিং অ্যাকসেন্ট পাঠাতে পারেন। যদি আপনার অথরাইজেশন চেক একবার নরমালাইজ করে কিন্তু আপনার স্টোরেজ লেয়ার ভিন্নভাবে নরমালাইজ করে, তবে আপনি অসামঞ্জস্যপূর্ণ ডেটা পেতে পারেন অথবা আরও খারাপভাবে, একটি বাইপাস (bypass) তৈরি হতে পারে যেখানে ইউজারনেম কলিশন (username collision) আপনার লজিককে ফাঁকি দিয়ে যেতে পারে। দ্রুত এবং ধারাবাহিকভাবে নরমালাইজ করুন।
রেস কন্ডিশন (Race conditions)। অথরাইজেশন সিদ্ধান্তগুলো স্থির চিত্র নয়। এগুলো একটি নির্দিষ্ট সময়ে ঘটে। দুটি রিকোয়েস্ট একই রেকর্ড পড়তে পারে, উভয়ই দেখতে পারে যে অ্যাক্টর (actor) লেখার অনুমতিপ্রাপ্ত এবং উভয়ই আপডেট ইস্যু করতে পারে। এর মাঝখানের সময়ে স্টেট (state) বা অ্যাক্টরের পারমিশন পরিবর্তিত হতে পারে। সর্বদা একটি ভার্সন নম্বর বা স্টেট মেশিন ভ্যালুর শর্তসহ ডাটাবেস আপডেট প্রয়োগ করুন। UPDATE users SET ... WHERE id = ? AND version = 5 এর মতো কিছু ব্যবহার করুন। আপনি এটি পড়ার পর যদি রো (row) পরিবর্তিত হয়ে যায়, তবে রাইট (write) অপারেশনটি ব্যর্থ হবে। রিট্রাই (retry) বা প্রত্যাখ্যান করার মাধ্যমে ব্যর্থতা হ্যান্ডেল করুন। এটি পুরনো বা স্টেল (stale) অথরাইজেশন চেক থেকে আপনার ডেটা নষ্ট হওয়া রোধ করে।
যা গুরুত্বপূর্ণ তা মনিটর করুন
আপনি যা দেখতে পান না তা সুরক্ষিত করতে পারেন না। আপনার অডিট লগিং (audit logging) শুধুমাত্র অ্যাকশনের ওপর নয়, বরং সিদ্ধান্তের ওপর ভিত্তি করে তৈরি করুন।
অ্যাক্টর আইডি (actor ID) এবং টার্গেট আইডি (target ID) লগ করুন। কোন ফিল্ডের নামগুলো গ্রহণ করা হয়েছে এবং কোনগুলো প্রত্যাখ্যান করা হয়েছে তা সঠিকভাবে লগ করুন। কোন পলিসি ভার্সন সিদ্ধান্তটি নিয়েছে এবং চূড়ান্ত ফলাফল কী ছিল তা লগ করুন। যদি কোনো ব্যবহারকারী হঠাৎ তার প্রোফাইল আপডেটে role প্রত্যাখ্যান পেতে শুরু করেন, তবে আপনি তা অবিলম্বে জানতে চান।
কখনোই বিয়ারার টোকেন (bearer tokens) লগ করবেন না। কখনোই সম্পূর্ণ রিকোয়েস্ট বডি (request bodies) আপনার লগে ঢালবেন না। একটি অডিট ট্রেইল (audit trail) আপনাকে অপব্যবহার তদন্ত করতে সাহায্য করার কথা, এটি ক্রেডেনশিয়াল (credentials) এবং ব্যক্তিগত তথ্যের ভাণ্ডার হয়ে উঠবে না।
আপনার প্রয়োজনীয় একমাত্র নিয়ম
একটি রিকোয়েস্ট বডি ডেটা প্রস্তাব করে। এটি কখনোই তার নিজস্ব কর্তৃত্ব (authority) নির্ধারণ করে না। ক্লায়েন্ট যেকোনো কিছু চাইতে পারে। আপনার সার্ভার ফিল্ড বাই ফিল্ড এবং রো বাই রো সিদ্ধান্ত নেয় যে স্থায়ী স্টোরেজে কী জমা রাখা অনুমোদিত। এই বিভাজন মাথায় রেখে আপনার প্যাচগুলো (patches) তৈরি করুন, এবং মাস অ্যাসাইনমেন্ট এমন একটি সমস্যা হয়ে দাঁড়াবে যা আপনার অথরাইজেশন লেয়ারে পৌঁছানোর অনেক আগেই আপনি সমাধান করে ফেলেছেন।
